diff options
Diffstat (limited to 'src/core/devices')
119 files changed, 74201 insertions, 0 deletions
diff --git a/src/core/devices/adsl/meson.build b/src/core/devices/adsl/meson.build new file mode 100644 index 00000000..95f61d95 --- /dev/null +++ b/src/core/devices/adsl/meson.build @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +libnm_device_plugin_adsl = shared_module( + 'nm-device-plugin-adsl', + sources: files( + 'nm-atm-manager.c', + 'nm-device-adsl.c', + ), + dependencies: core_plugin_dep, + c_args: daemon_c_flags, + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, +) + +core_plugins += libnm_device_plugin_adsl + +test( + 'check-local-devices-adsl', + check_exports, + args: [ + libnm_device_plugin_adsl.full_path(), + linker_script_devices, + ], +) diff --git a/src/core/devices/adsl/nm-atm-manager.c b/src/core/devices/adsl/nm-atm-manager.c new file mode 100644 index 00000000..9be9b5ce --- /dev/null +++ b/src/core/devices/adsl/nm-atm-manager.c @@ -0,0 +1,271 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2013 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <gmodule.h> +#include <libudev.h> + +#include "nm-setting-adsl.h" +#include "nm-device-adsl.h" +#include "devices/nm-device-factory.h" +#include "platform/nm-platform.h" +#include "nm-udev-aux/nm-udev-utils.h" + +/*****************************************************************************/ + +#define NM_TYPE_ATM_MANAGER (nm_atm_manager_get_type()) +#define NM_ATM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ATM_MANAGER, NMAtmManager)) +#define NM_ATM_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_ATM_MANAGER, NMAtmManagerClass)) +#define NM_IS_ATM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_ATM_MANAGER)) +#define NM_IS_ATM_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_ATM_MANAGER)) +#define NM_ATM_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_ATM_MANAGER, NMAtmManagerClass)) + +typedef struct { + NMUdevClient *udev_client; + GSList * devices; +} NMAtmManagerPrivate; + +typedef struct { + NMDeviceFactory parent; + NMAtmManagerPrivate _priv; +} NMAtmManager; + +typedef struct { + NMDeviceFactoryClass parent; +} NMAtmManagerClass; + +static GType nm_atm_manager_get_type(void); + +G_DEFINE_TYPE(NMAtmManager, nm_atm_manager, NM_TYPE_DEVICE_FACTORY); + +#define NM_ATM_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMAtmManager, NM_IS_ATM_MANAGER) + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES( + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_ADSL_SETTING_NAME)); + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + return g_object_new(NM_TYPE_ATM_MANAGER, NULL); +} + +/*****************************************************************************/ + +static gboolean +dev_get_attrs(struct udev_device *udev_device, const char **out_path, char **out_driver) +{ + struct udev_device *parent = NULL; + const char * driver, *path; + + g_return_val_if_fail(udev_device != NULL, FALSE); + g_return_val_if_fail(out_path != NULL, FALSE); + g_return_val_if_fail(out_driver != NULL, FALSE); + + path = udev_device_get_syspath(udev_device); + if (!path) { + nm_log_warn(LOGD_PLATFORM, "couldn't determine device path; ignoring..."); + return FALSE; + } + + driver = udev_device_get_driver(udev_device); + if (!driver) { + /* Try the parent */ + parent = udev_device_get_parent(udev_device); + if (parent) + driver = udev_device_get_driver(parent); + } + + *out_path = path; + *out_driver = g_strdup(driver); + + return TRUE; +} + +static void +device_destroyed(gpointer user_data, GObject *dead) +{ + NMAtmManager * self = NM_ATM_MANAGER(user_data); + NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self); + + priv->devices = g_slist_remove(priv->devices, dead); +} + +static void +adsl_add(NMAtmManager *self, struct udev_device *udev_device) +{ + NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self); + const char * ifname, *sysfs_path = NULL; + char * driver = NULL; + gs_free char * atm_index_path = NULL; + int atm_index; + NMDevice * device; + + g_return_if_fail(udev_device != NULL); + + ifname = udev_device_get_sysname(udev_device); + if (!ifname) { + nm_log_warn(LOGD_PLATFORM, "failed to get device's interface name"); + return; + } + + nm_log_dbg(LOGD_PLATFORM, "(%s): found ATM device", ifname); + + atm_index_path = + g_strdup_printf("/sys/class/atm/%s/atmindex", NM_ASSERT_VALID_PATH_COMPONENT(ifname)); + atm_index = (int) nm_platform_sysctl_get_int_checked(NM_PLATFORM_GET, + NMP_SYSCTL_PATHID_ABSOLUTE(atm_index_path), + 10, + 0, + G_MAXINT, + -1); + if (atm_index < 0) { + nm_log_warn(LOGD_PLATFORM, "(%s): failed to get ATM index", ifname); + return; + } + + if (!dev_get_attrs(udev_device, &sysfs_path, &driver)) { + nm_log_warn(LOGD_PLATFORM, "(%s): failed to get ATM attributes", ifname); + return; + } + + g_assert(sysfs_path); + + device = nm_device_adsl_new(sysfs_path, ifname, driver, atm_index); + g_assert(device); + + priv->devices = g_slist_prepend(priv->devices, device); + g_object_weak_ref(G_OBJECT(device), device_destroyed, self); + + g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device); + g_object_unref(device); + + g_free(driver); +} + +static void +adsl_remove(NMAtmManager *self, struct udev_device *udev_device) +{ + NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self); + const char * iface = udev_device_get_sysname(udev_device); + GSList * iter; + + nm_log_dbg(LOGD_PLATFORM, "(%s): removing ATM device", iface); + + for (iter = priv->devices; iter; iter = iter->next) { + NMDevice *device = iter->data; + + /* Match 'iface' not 'ip_iface' to the ATM device instead of the + * NAS bridge interface or PPPoE interface. + */ + if (g_strcmp0(nm_device_get_iface(device), iface) != 0) + continue; + + g_object_weak_unref(G_OBJECT(iter->data), device_destroyed, self); + priv->devices = g_slist_remove(priv->devices, device); + g_signal_emit_by_name(device, NM_DEVICE_REMOVED); + break; + } +} + +static void +start(NMDeviceFactory *factory) +{ + NMAtmManager * self = NM_ATM_MANAGER(factory); + NMAtmManagerPrivate * priv = NM_ATM_MANAGER_GET_PRIVATE(self); + struct udev_enumerate * enumerate; + struct udev_list_entry *devices; + + enumerate = nm_udev_client_enumerate_new(priv->udev_client); + udev_enumerate_add_match_is_initialized(enumerate); + udev_enumerate_scan_devices(enumerate); + devices = udev_enumerate_get_list_entry(enumerate); + for (; devices; devices = udev_list_entry_get_next(devices)) { + struct udev_device *udevice; + + udevice = udev_device_new_from_syspath(udev_enumerate_get_udev(enumerate), + udev_list_entry_get_name(devices)); + if (udevice) { + adsl_add(self, udevice); + udev_device_unref(udevice); + } + } + udev_enumerate_unref(enumerate); +} + +static void +handle_uevent(NMUdevClient *client, struct udev_device *device, gpointer user_data) +{ + NMAtmManager *self = NM_ATM_MANAGER(user_data); + const char * subsys; + const char * ifindex; + guint64 seqnum; + const char * action; + + action = udev_device_get_action(device); + + g_return_if_fail(action != NULL); + + /* A bit paranoid */ + subsys = udev_device_get_subsystem(device); + g_return_if_fail(!g_strcmp0(subsys, "atm")); + + ifindex = udev_device_get_property_value(device, "IFINDEX"); + seqnum = udev_device_get_seqnum(device); + nm_log_dbg(LOGD_PLATFORM, + "UDEV event: action '%s' subsys '%s' device '%s' (%s); seqnum=%" G_GUINT64_FORMAT, + action, + subsys, + udev_device_get_sysname(device), + ifindex ?: "unknown", + seqnum); + + if (!strcmp(action, "add")) + adsl_add(self, device); + else if (!strcmp(action, "remove")) + adsl_remove(self, device); +} + +/*****************************************************************************/ + +static void +nm_atm_manager_init(NMAtmManager *self) +{ + NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self); + + priv->udev_client = nm_udev_client_new(NM_MAKE_STRV("atm"), handle_uevent, self); +} + +static void +dispose(GObject *object) +{ + NMAtmManager * self = NM_ATM_MANAGER(object); + NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self); + GSList * iter; + + for (iter = priv->devices; iter; iter = iter->next) + g_object_weak_unref(G_OBJECT(iter->data), device_destroyed, self); + nm_clear_pointer(&priv->devices, g_slist_free); + + priv->udev_client = nm_udev_client_destroy(priv->udev_client); + + G_OBJECT_CLASS(nm_atm_manager_parent_class)->dispose(object); +} + +static void +nm_atm_manager_class_init(NMAtmManagerClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + object_class->dispose = dispose; + + factory_class->get_supported_types = get_supported_types; + factory_class->start = start; +} diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c new file mode 100644 index 00000000..34c062a8 --- /dev/null +++ b/src/core/devices/adsl/nm-device-adsl.c @@ -0,0 +1,720 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Pantelis Koukousoulas <pktoss@gmail.com> + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-adsl.h" + +#include <sys/socket.h> +#include <linux/atmdev.h> +#include <linux/atmbr2684.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <unistd.h> +#include <stdlib.h> + +#include "nm-ip4-config.h" +#include "devices/nm-device-private.h" +#include "platform/nm-platform.h" +#include "ppp/nm-ppp-manager-call.h" +#include "ppp/nm-ppp-status.h" +#include "nm-setting-adsl.h" +#include "nm-utils.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceAdsl +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_ATM_INDEX, ); + +typedef struct { + guint carrier_poll_id; + int atm_index; + + /* PPP */ + NMPPPManager *ppp_manager; + + /* RFC 2684 bridging (PPPoE over ATM) */ + int brfd; + int nas_ifindex; + char *nas_ifname; + guint nas_update_id; + guint nas_update_count; +} NMDeviceAdslPrivate; + +struct _NMDeviceAdsl { + NMDevice parent; + NMDeviceAdslPrivate _priv; +}; + +struct _NMDeviceAdslClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceAdsl, nm_device_adsl, NM_TYPE_DEVICE) + +#define NM_DEVICE_ADSL_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceAdsl, NM_IS_DEVICE_ADSL, NMDevice) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return (NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER + | NM_DEVICE_CAP_IS_NON_KERNEL); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingAdsl *s_adsl; + const char * protocol; + + if (!NM_DEVICE_CLASS(nm_device_adsl_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_adsl = nm_connection_get_setting_adsl(connection); + + protocol = nm_setting_adsl_get_protocol(s_adsl); + if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_IPOATM)) { + /* FIXME: we don't yet support IPoATM */ + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "IPoATM protocol is not yet supported"); + return FALSE; + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingAdsl *s_adsl; + + /* + * We can't telepathically figure out the username, so if + * it wasn't given, we can't complete the connection. + */ + s_adsl = nm_connection_get_setting_adsl(connection); + if (s_adsl && !nm_setting_verify(NM_SETTING(s_adsl), NULL, error)) + return FALSE; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_ADSL_SETTING_NAME, + existing_connections, + NULL, + _("ADSL connection"), + NULL, + NULL, + FALSE); /* No IPv6 yet by default */ + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +br2684_assign_vcc(NMDeviceAdsl *self, NMSettingAdsl *s_adsl) +{ + NMDeviceAdslPrivate * priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + struct sockaddr_atmpvc addr; + struct atm_backend_br2684 be; + struct atm_qos qos; + int errsv, err, bufsize = 8192; + const char * encapsulation; + gboolean is_llc; + + g_return_val_if_fail(priv->brfd == -1, FALSE); + g_return_val_if_fail(priv->nas_ifname != NULL, FALSE); + + priv->brfd = socket(PF_ATMPVC, SOCK_DGRAM | SOCK_CLOEXEC, ATM_AAL5); + if (priv->brfd < 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to open ATM control socket (%d)", errsv); + priv->brfd = -1; + return FALSE; + } + + err = setsockopt(priv->brfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + if (err != 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to set SNDBUF option (%d)", errsv); + goto error; + } + + /* QoS */ + memset(&qos, 0, sizeof(qos)); + qos.aal = ATM_AAL5; + qos.txtp.traffic_class = ATM_UBR; + qos.txtp.max_sdu = 1524; + qos.txtp.pcr = ATM_MAX_PCR; + qos.rxtp = qos.txtp; + + err = setsockopt(priv->brfd, SOL_ATM, SO_ATMQOS, &qos, sizeof(qos)); + if (err != 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to set QoS (%d)", errsv); + goto error; + } + + encapsulation = nm_setting_adsl_get_encapsulation(s_adsl); + + /* VPI/VCI */ + memset(&addr, 0, sizeof(addr)); + addr.sap_family = AF_ATMPVC; + addr.sap_addr.itf = priv->atm_index; + addr.sap_addr.vpi = (guint16) nm_setting_adsl_get_vpi(s_adsl); + addr.sap_addr.vci = (int) nm_setting_adsl_get_vci(s_adsl); + + _LOGD(LOGD_ADSL, + "assigning address %d.%d.%d encapsulation %s", + priv->atm_index, + addr.sap_addr.vpi, + addr.sap_addr.vci, + encapsulation ?: "(none)"); + + err = connect(priv->brfd, (struct sockaddr *) &addr, sizeof(addr)); + if (err != 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to set VPI/VCI (%d)", errsv); + goto error; + } + + /* And last attach the VCC to the interface */ + is_llc = (g_strcmp0(encapsulation, "llc") == 0); + + memset(&be, 0, sizeof(be)); + be.backend_num = ATM_BACKEND_BR2684; + be.ifspec.method = BR2684_FIND_BYIFNAME; + nm_utils_ifname_cpy(be.ifspec.spec.ifname, priv->nas_ifname); + be.fcs_in = BR2684_FCSIN_NO; + be.fcs_out = BR2684_FCSOUT_NO; + be.encaps = is_llc ? BR2684_ENCAPS_LLC : BR2684_ENCAPS_VC; + err = ioctl(priv->brfd, ATM_SETBACKEND, &be); + if (err != 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to attach VCC (%d)", errsv); + goto error; + } + + return TRUE; + +error: + nm_close(priv->brfd); + priv->brfd = -1; + return FALSE; +} + +static void +link_changed_cb(NMPlatform * platform, + int obj_type_i, + int ifindex, + NMPlatformLink *info, + int change_type_i, + NMDeviceAdsl * self) +{ + const NMPlatformSignalChangeType change_type = change_type_i; + + if (change_type == NM_PLATFORM_SIGNAL_REMOVED) { + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + + /* This only gets called for PPPoE connections and "nas" interfaces */ + + if (priv->nas_ifindex > 0 && ifindex == priv->nas_ifindex) { + /* NAS device went away for some reason; kill the connection */ + _LOGD(LOGD_ADSL, "br2684 interface disappeared"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BR2684_FAILED); + } + } +} + +static gboolean +pppoe_vcc_config(NMDeviceAdsl *self) +{ + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMSettingAdsl * s_adsl; + + s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL); + + g_return_val_if_fail(s_adsl, FALSE); + + /* Set up the VCC */ + if (!br2684_assign_vcc(self, s_adsl)) + return FALSE; + + /* Watch for the 'nas' interface going away */ + g_signal_connect(nm_device_get_platform(device), + NM_PLATFORM_SIGNAL_LINK_CHANGED, + G_CALLBACK(link_changed_cb), + self); + + _LOGD(LOGD_ADSL, "ATM setup successful"); + + /* otherwise we're good for stage3 */ + nm_platform_link_set_up(nm_device_get_platform(device), priv->nas_ifindex, NULL); + + return TRUE; +} + +static gboolean +nas_update_cb(gpointer user_data) +{ + NMDeviceAdsl * self = NM_DEVICE_ADSL(user_data); + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + + nm_assert(priv->nas_ifname); + + priv->nas_update_count++; + + nm_assert(priv->nas_ifindex <= 0); + priv->nas_ifindex = + nm_platform_link_get_ifindex(nm_device_get_platform(device), priv->nas_ifname); + if (priv->nas_ifindex <= 0) { + if (priv->nas_update_count <= 10) { + /* Keep waiting for it to appear */ + return G_SOURCE_CONTINUE; + } + priv->nas_update_id = 0; + _LOGW(LOGD_ADSL, + "failed to find br2684 interface %s ifindex after timeout", + priv->nas_ifname); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BR2684_FAILED); + return G_SOURCE_REMOVE; + } + + priv->nas_update_id = 0; + _LOGD(LOGD_ADSL, "using br2684 iface '%s' index %d", priv->nas_ifname, priv->nas_ifindex); + + if (!pppoe_vcc_config(self)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BR2684_FAILED); + return G_SOURCE_REMOVE; + } + + nm_device_activate_schedule_stage2_device_config(device, TRUE); + return G_SOURCE_REMOVE; +} + +static gboolean +br2684_create_iface(NMDeviceAdsl *self) +{ + NMDeviceAdslPrivate * priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + struct atm_newif_br2684 ni; + nm_auto_close int fd = -1; + int err, errsv; + guint num = 0; + + if (nm_clear_g_source(&priv->nas_update_id)) + nm_assert_not_reached(); + + fd = socket(PF_ATMPVC, SOCK_DGRAM | SOCK_CLOEXEC, ATM_AAL5); + if (fd < 0) { + errsv = errno; + _LOGE(LOGD_ADSL, "failed to open ATM control socket (%d)", errsv); + return FALSE; + } + + memset(&ni, 0, sizeof(ni)); + ni.backend_num = ATM_BACKEND_BR2684; + ni.media = BR2684_MEDIA_ETHERNET; + ni.mtu = 1500; + + /* Loop attempting to create an interface that doesn't exist yet. The + * kernel can create one for us automatically, but due to API issues it + * cannot return that name to us. Since we want to know the name right + * away, just brute-force it. + */ + while (TRUE) { + memset(&ni.ifname, 0, sizeof(ni.ifname)); + g_snprintf(ni.ifname, sizeof(ni.ifname), "nas%u", num++); + + err = ioctl(fd, ATM_NEWBACKENDIF, &ni); + if (err != 0) { + errsv = errno; + if (errsv == EEXIST) + continue; + + _LOGW(LOGD_ADSL, "failed to create br2684 interface (%d)", errsv); + return FALSE; + } + + nm_utils_strdup_reset(&priv->nas_ifname, ni.ifname); + _LOGD(LOGD_ADSL, "waiting for br2684 iface '%s' to appear", priv->nas_ifname); + priv->nas_update_count = 0; + priv->nas_update_id = g_timeout_add(100, nas_update_cb, self); + return TRUE; + } +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceAdsl * self = NM_DEVICE_ADSL(device); + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + NMSettingAdsl * s_adsl; + const char * protocol; + + s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL); + + g_return_val_if_fail(s_adsl, NM_ACT_STAGE_RETURN_FAILURE); + + protocol = nm_setting_adsl_get_protocol(s_adsl); + _LOGD(LOGD_ADSL, "using ADSL protocol '%s'", protocol); + + if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_PPPOA)) { + /* PPPoA doesn't need anything special */ + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_PPPOE)) { + /* PPPoE needs RFC2684 bridging before we can do PPP over it */ + if (priv->nas_ifindex <= 0) { + if (priv->nas_update_id == 0) { + if (!br2684_create_iface(self)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_BR2684_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + return NM_ACT_STAGE_RETURN_POSTPONE; + } + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + _LOGW(LOGD_ADSL, "unhandled ADSL protocol '%s'", protocol); + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + switch (status) { + case NM_PPP_STATUS_DISCONNECT: + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_PPP_DISCONNECT); + break; + case NM_PPP_STATUS_DEAD: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED); + break; + default: + break; + } +} + +static void +ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + if (!nm_device_set_ip_ifindex(device, ifindex)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } +} + +static void +ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + /* Ignore PPP IP4 events that come in after initial configuration */ + if (nm_device_activate_ip4_state_in_conf(device)) + nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config)); +} + +static NMActStageReturn +act_stage3_ip4_config_start(NMDevice * device, + NMIP4Config ** out_config, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceAdsl * self = NM_DEVICE_ADSL(device); + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + NMSettingAdsl * s_adsl; + NMActRequest * req; + GError * err = NULL; + const char * ppp_iface; + + req = nm_device_get_act_request(device); + + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL); + + g_return_val_if_fail(s_adsl, NM_ACT_STAGE_RETURN_FAILURE); + + /* PPPoE uses the NAS interface, not the ATM interface */ + if (nm_streq0(nm_setting_adsl_get_protocol(s_adsl), NM_SETTING_ADSL_PROTOCOL_PPPOE)) { + nm_assert(priv->nas_ifname); + ppp_iface = priv->nas_ifname; + + _LOGD(LOGD_ADSL, "starting PPPoE on br2684 interface %s", priv->nas_ifname); + } else { + ppp_iface = nm_device_get_iface(device); + _LOGD(LOGD_ADSL, "starting PPPoA"); + } + + priv->ppp_manager = nm_ppp_manager_create(ppp_iface, &err); + + if (priv->ppp_manager) { + nm_ppp_manager_set_route_parameters(priv->ppp_manager, + nm_device_get_route_table(device, AF_INET), + nm_device_get_route_metric(device, AF_INET), + nm_device_get_route_table(device, AF_INET6), + nm_device_get_route_metric(device, AF_INET6)); + } + + if (!priv->ppp_manager + || !nm_ppp_manager_start(priv->ppp_manager, + req, + nm_setting_adsl_get_username(s_adsl), + 30, + 0, + &err)) { + _LOGW(LOGD_ADSL, "PPP failed to start: %s", err->message); + g_error_free(err); + + g_clear_object(&priv->ppp_manager); + + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_STATE_CHANGED, + G_CALLBACK(ppp_state_changed), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IFINDEX_SET, + G_CALLBACK(ppp_ifindex_set), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IP4_CONFIG, + G_CALLBACK(ppp_ip4_config), + self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + if (addr_family == AF_INET) + return act_stage3_ip4_config_start(device, (NMIP4Config **) out_config, out_failure_reason); + + return NM_DEVICE_CLASS(nm_device_adsl_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static void +adsl_cleanup(NMDeviceAdsl *self) +{ + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + + if (priv->ppp_manager) { + g_signal_handlers_disconnect_by_func(priv->ppp_manager, + G_CALLBACK(ppp_state_changed), + self); + g_signal_handlers_disconnect_by_func(priv->ppp_manager, G_CALLBACK(ppp_ip4_config), self); + nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL); + g_clear_object(&priv->ppp_manager); + } + + g_signal_handlers_disconnect_by_func(nm_device_get_platform(NM_DEVICE(self)), + G_CALLBACK(link_changed_cb), + self); + + nm_close(priv->brfd); + priv->brfd = -1; + + nm_clear_g_source(&priv->nas_update_id); + + /* FIXME: kernel has no way of explicitly deleting the 'nasX' interface yet, + * so it gets leaked. It does get destroyed when it's no longer in use, + * but we have no control over that. + */ + priv->nas_ifindex = 0; + nm_clear_g_free(&priv->nas_ifname); +} + +static void +deactivate(NMDevice *device) +{ + adsl_cleanup(NM_DEVICE_ADSL(device)); +} + +/*****************************************************************************/ + +static gboolean +carrier_update_cb(gpointer user_data) +{ + NMDeviceAdsl *self = NM_DEVICE_ADSL(user_data); + int carrier; + char * path; + + path = g_strdup_printf("/sys/class/atm/%s/carrier", + NM_ASSERT_VALID_PATH_COMPONENT(nm_device_get_iface(NM_DEVICE(self)))); + carrier = (int) nm_platform_sysctl_get_int_checked(nm_device_get_platform(NM_DEVICE(self)), + NMP_SYSCTL_PATHID_ABSOLUTE(path), + 10, + 0, + 1, + -1); + g_free(path); + + if (carrier != -1) + nm_device_set_carrier(NM_DEVICE(self), carrier); + return TRUE; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + case PROP_ATM_INDEX: + g_value_set_int(value, NM_DEVICE_ADSL_GET_PRIVATE(object)->atm_index); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + case PROP_ATM_INDEX: + /* construct-only */ + NM_DEVICE_ADSL_GET_PRIVATE(object)->atm_index = g_value_get_int(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_adsl_init(NMDeviceAdsl *self) +{} + +static void +constructed(GObject *object) +{ + NMDeviceAdsl * self = NM_DEVICE_ADSL(object); + NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self); + + G_OBJECT_CLASS(nm_device_adsl_parent_class)->constructed(object); + + priv->carrier_poll_id = g_timeout_add_seconds(5, carrier_update_cb, self); + + _LOGD(LOGD_ADSL, "ATM device index %d", priv->atm_index); + + g_return_if_fail(priv->atm_index >= 0); +} + +NMDevice * +nm_device_adsl_new(const char *udi, const char *iface, const char *driver, int atm_index) +{ + g_return_val_if_fail(udi != NULL, NULL); + g_return_val_if_fail(atm_index >= 0, NULL); + + return g_object_new(NM_TYPE_DEVICE_ADSL, + NM_DEVICE_UDI, + udi, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_DRIVER, + driver, + NM_DEVICE_ADSL_ATM_INDEX, + atm_index, + NM_DEVICE_TYPE_DESC, + "ADSL", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_ADSL, + NULL); +} + +static void +dispose(GObject *object) +{ + adsl_cleanup(NM_DEVICE_ADSL(object)); + + nm_clear_g_source(&NM_DEVICE_ADSL_GET_PRIVATE(object)->carrier_poll_id); + + G_OBJECT_CLASS(nm_device_adsl_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_adsl = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_ADSL, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", + "b", + NM_DEVICE_CARRIER), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_adsl_class_init(NMDeviceAdslClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_adsl); + + device_class->connection_type_check_compatible = NM_SETTING_ADSL_SETTING_NAME; + + device_class->get_generic_capabilities = get_generic_capabilities; + + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->deactivate = deactivate; + + obj_properties[PROP_ATM_INDEX] = + g_param_spec_int(NM_DEVICE_ADSL_ATM_INDEX, + "", + "", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/adsl/nm-device-adsl.h b/src/core/devices/adsl/nm-device-adsl.h new file mode 100644 index 00000000..c5c18901 --- /dev/null +++ b/src/core/devices/adsl/nm-device-adsl.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Author: Pantelis Koukousoulas <pktoss@gmail.com> + * Copyright (C) 2009 - 2011 Red Hat Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_ADSL_H__ +#define __NETWORKMANAGER_DEVICE_ADSL_H__ + +#include "devices/nm-device.h" + +#define NM_TYPE_DEVICE_ADSL (nm_device_adsl_get_type()) +#define NM_DEVICE_ADSL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdsl)) +#define NM_DEVICE_ADSL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ADSL, NMDeviceAdslClass)) +#define NM_IS_DEVICE_ADSL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ADSL)) +#define NM_IS_DEVICE_ADSL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_ADSL)) +#define NM_DEVICE_ADSL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdslClass)) + +#define NM_DEVICE_ADSL_ATM_INDEX "atm-index" + +typedef struct _NMDeviceAdsl NMDeviceAdsl; +typedef struct _NMDeviceAdslClass NMDeviceAdslClass; + +GType nm_device_adsl_get_type(void); + +NMDevice *nm_device_adsl_new(const char *udi, const char *iface, const char *driver, int atm_index); + +#endif /* NM_DEVICE_ADSL_H */ diff --git a/src/core/devices/bluetooth/meson.build b/src/core/devices/bluetooth/meson.build new file mode 100644 index 00000000..d5f26068 --- /dev/null +++ b/src/core/devices/bluetooth/meson.build @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +libnm_device_plugin_bluetooth_static = static_library( + 'nm-device-plugin-bluetooth-static', + sources: files( + 'nm-bluez-manager.c', + 'nm-bt-error.c', + 'nm-device-bt.c', + ) + (enable_bluez5_dun ? files('nm-bluez5-dun.c') : files()), + dependencies: [ + core_default_dep, + libnm_wwan_dep, + bluez5_dep, + ], + c_args: daemon_c_flags, +) + +libnm_device_plugin_bluetooth_static_dep = declare_dependency( + link_whole: libnm_device_plugin_bluetooth_static, +) + +libnm_device_plugin_bluetooth = shared_module( + 'nm-device-plugin-bluetooth', + dependencies: [ + core_plugin_dep, + libnm_wwan_dep, + bluez5_dep, + libnm_device_plugin_bluetooth_static_dep, + ], + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, + install_rpath: nm_plugindir, +) + +core_plugins += libnm_device_plugin_bluetooth + +test( + 'check-local-devices-bluetooth', + check_exports, + args: [ + libnm_device_plugin_bluetooth.full_path(), + linker_script_devices + ], +) + +if enable_tests + executable( + 'nm-bt-test', + 'tests/nm-bt-test.c', + dependencies: [ + libNetworkManagerTest_dep, + core_default_dep, + libnm_wwan_dep, + bluez5_dep, + libnm_device_plugin_bluetooth_static_dep, + ], + c_args: test_c_flags, + ) +endif diff --git a/src/core/devices/bluetooth/nm-bluez-common.h b/src/core/devices/bluetooth/nm-bluez-common.h new file mode 100644 index 00000000..868a985e --- /dev/null +++ b/src/core/devices/bluetooth/nm-bluez-common.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_BLUEZ_COMMON_H__ +#define __NETWORKMANAGER_BLUEZ_COMMON_H__ + +#define BLUETOOTH_CONNECT_DUN "dun" +#define BLUETOOTH_CONNECT_NAP "nap" + +#define NM_BLUEZ_SERVICE "org.bluez" + +#define NM_BLUEZ_MANAGER_PATH "/" + +#define NM_BLUEZ5_ADAPTER_INTERFACE "org.bluez.Adapter1" +#define NM_BLUEZ5_DEVICE_INTERFACE "org.bluez.Device1" +#define NM_BLUEZ5_NETWORK_INTERFACE "org.bluez.Network1" +#define NM_BLUEZ5_NETWORK_SERVER_INTERFACE "org.bluez.NetworkServer1" + +#define NM_BLUEZ_MANAGER_BDADDR_ADDED "bdaddr-added" +#define NM_BLUEZ_MANAGER_NETWORK_SERVER_ADDED "network-server-added" + +#endif /* NM_BLUEZ_COMMON_H */ diff --git a/src/core/devices/bluetooth/nm-bluez-manager.c b/src/core/devices/bluetooth/nm-bluez-manager.c new file mode 100644 index 00000000..dd998d29 --- /dev/null +++ b/src/core/devices/bluetooth/nm-bluez-manager.c @@ -0,0 +1,2896 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-bluez-manager.h" + +#include <signal.h> +#include <stdlib.h> +#include <gmodule.h> +#include <linux/if_ether.h> + +#include "nm-glib-aux/nm-dbus-aux.h" +#include "nm-glib-aux/nm-c-list.h" +#include "nm-dbus-manager.h" +#include "devices/nm-device-factory.h" +#include "devices/nm-device-bridge.h" +#include "nm-setting-bluetooth.h" +#include "settings/nm-settings.h" +#include "nm-bluez-common.h" +#include "nm-device-bt.h" +#include "nm-manager.h" +#include "nm-bluez5-dun.h" +#include "nm-core-internal.h" +#include "platform/nm-platform.h" +#include "nm-std-aux/nm-dbus-compat.h" + +/*****************************************************************************/ + +#if WITH_BLUEZ5_DUN + #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_DUN +#else + #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_NONE +#endif +#define _NM_BT_CAPABILITY_SUPPORTED (NM_BT_CAPABILITY_NAP | _NM_BT_CAPABILITY_SUPPORTED_DUN) + +typedef struct { + const char * bdaddr; + CList lst_head; + NMBluetoothCapabilities bt_type : 8; + char bdaddr_data[]; +} ConnDataHead; + +typedef struct { + NMSettingsConnection *sett_conn; + ConnDataHead * cdata_hd; + CList lst; +} ConnDataElem; + +typedef struct { + GCancellable * ext_cancellable; + GCancellable * int_cancellable; + NMBtVTableRegisterCallback callback; + gpointer callback_user_data; + gulong ext_cancelled_id; +} NetworkServerRegisterReqData; + +typedef struct { + GCancellable * ext_cancellable; + GCancellable * int_cancellable; + NMBluezManagerConnectCb callback; + gpointer callback_user_data; + char * device_name; + gulong ext_cancelled_id; + guint timeout_id; + guint timeout_wait_connect_id; +} DeviceConnectReqData; + +typedef struct { + const char *object_path; + + NMBluezManager *self; + + /* Fields name with "d_" prefix are purely cached values from BlueZ's + * ObjectManager D-Bus interface. There is no logic whatsoever about + * them. + */ + + CList process_change_lst; + + struct { + char *address; + } d_adapter; + + struct { + char *address; + char *name; + char *adapter; + } d_device; + + struct { + char *interface; + } d_network; + + struct { + CList lst; + char * adapter_address; + NMDevice * device_br; + NetworkServerRegisterReqData *r_req_data; + } x_network_server; + + struct { + NMSettingsConnection *panu_connection; + NMDeviceBt * device_bt; + DeviceConnectReqData *c_req_data; + NMBluez5DunContext * connect_dun_context; + gulong device_bt_signal_id; + } x_device; + + /* indicate whether the D-Bus object has the particular D-Bus interface. */ + bool d_has_adapter_iface : 1; + bool d_has_device_iface : 1; + bool d_has_network_iface : 1; + bool d_has_network_server_iface : 1; + + /* cached D-Bus properties for Device1 ("d_device*"). */ + NMBluetoothCapabilities d_device_capabilities : 6; + bool d_device_connected : 1; + bool d_device_paired : 1; + + /* cached D-Bus properties for Network1 ("d_network*"). */ + bool d_network_connected : 1; + + /* cached D-Bus properties for Adapter1 ("d_adapter*"). */ + bool d_adapter_powered : 1; + + /* properties related to device ("x_device*"). */ + NMBluetoothCapabilities x_device_connect_bt_type : 6; + bool x_device_is_usable : 1; + bool x_device_is_connected : 1; + + bool x_device_panu_connection_allow_create : 1; + + /* flag to remember last time when we checked wether the object + * was a suitable adapter that is usable to a device. */ + bool was_usable_adapter_for_device_before : 1; + + char _object_path_intern[]; +} BzDBusObj; + +typedef struct { + NMManager * manager; + NMSettings *settings; + + GDBusConnection *dbus_connection; + + NMBtVTableNetworkServer vtable_network_server; + + GCancellable *name_owner_get_cancellable; + GCancellable *get_managed_objects_cancellable; + + GHashTable *bzobjs; + + char *name_owner; + + GHashTable *conn_data_heads; + GHashTable *conn_data_elems; + + CList network_server_lst_head; + + CList process_change_lst_head; + + guint name_owner_changed_id; + + guint managed_objects_changed_id; + + guint properties_changed_id; + + guint process_change_idle_id; + + bool settings_registered : 1; +} NMBluezManagerPrivate; + +struct _NMBluezManager { + NMDeviceFactory parent; + NMBluezManagerPrivate _priv; +}; + +struct _NMBluezManagerClass { + NMDeviceFactoryClass parent; +}; + +G_DEFINE_TYPE(NMBluezManager, nm_bluez_manager, NM_TYPE_DEVICE_FACTORY); + +#define NM_BLUEZ_MANAGER_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMBluezManager, NM_IS_BLUEZ_MANAGER) + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES( + NM_LINK_TYPE_BNEP) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BLUETOOTH_SETTING_NAME)) + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + return g_object_new(NM_TYPE_BLUEZ_MANAGER, NULL); +} + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_BT +#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "bluez", __VA_ARGS__) + +/*****************************************************************************/ + +static NMBluetoothCapabilities +convert_uuids_to_capabilities(const char *const *strv) +{ + NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE; + + if (strv) { + for (; strv[0]; strv++) { + gs_free char *s_part1 = NULL; + const char * str = strv[0]; + const char * s; + + s = strchr(str, '-'); + if (!s) + continue; + + s_part1 = g_strndup(str, s - str); + switch (_nm_utils_ascii_str_to_int64(s_part1, 16, 0, G_MAXINT, -1)) { + case 0x1103: + capabilities |= NM_BT_CAPABILITY_DUN; + break; + case 0x1116: + capabilities |= NM_BT_CAPABILITY_NAP; + break; + default: + break; + } + } + } + + return capabilities; +} + +/*****************************************************************************/ + +static void _cleanup_for_name_owner(NMBluezManager *self); +static void _connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason); +static gboolean _bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered); +static gboolean _bzobjs_is_dead(const BzDBusObj *bzobj); +static gboolean _bzobjs_device_is_usable(const BzDBusObj *bzobj, + BzDBusObj ** out_adapter_bzobj, + gboolean * out_create_panu_connection); +static gboolean _bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj); +static ConnDataHead * +_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr); +static void _process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj); +static void +_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason); +static gboolean _connect_timeout_wait_connected_cb(gpointer user_data); + +/*****************************************************************************/ + +static void +_dbus_call_complete_cb_nop(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + /* we don't do anything at all. The only reason to register this + * callback is so that GDBusConnection keeps the cancellable alive + * long enough until the call completes. + * + * Note that this cancellable in turn is registered via + * nm_shutdown_wait_obj_register_*(), to block shutdown until + * we are done. */ +} + +/*****************************************************************************/ + +static void +_network_server_register_req_data_complete(NetworkServerRegisterReqData *r_req_data, GError *error) +{ + nm_clear_g_signal_handler(r_req_data->ext_cancellable, &r_req_data->ext_cancelled_id); + + nm_clear_g_cancellable(&r_req_data->int_cancellable); + + if (r_req_data->callback) { + gs_free_error GError *error_cancelled = NULL; + + if (g_cancellable_set_error_if_cancelled(r_req_data->ext_cancellable, &error_cancelled)) + error = error_cancelled; + + r_req_data->callback(error, r_req_data->callback_user_data); + } + + g_object_unref(r_req_data->ext_cancellable); + nm_g_slice_free(r_req_data); +} + +static void +_device_connect_req_data_complete(DeviceConnectReqData *c_req_data, + NMBluezManager * self, + const char * device_name, + GError * error) +{ + nm_assert((!!device_name) != (!!error)); + + nm_clear_g_signal_handler(c_req_data->ext_cancellable, &c_req_data->ext_cancelled_id); + + nm_clear_g_cancellable(&c_req_data->int_cancellable); + nm_clear_g_source(&c_req_data->timeout_id); + nm_clear_g_source(&c_req_data->timeout_wait_connect_id); + + if (c_req_data->callback) { + gs_free_error GError *error_cancelled = NULL; + + if (g_cancellable_set_error_if_cancelled(c_req_data->ext_cancellable, &error_cancelled)) { + error = error_cancelled; + device_name = NULL; + } + + c_req_data->callback(self, TRUE, device_name, error, c_req_data->callback_user_data); + } + + g_object_unref(c_req_data->ext_cancellable); + nm_clear_g_free(&c_req_data->device_name); + nm_g_slice_free(c_req_data); +} + +/*****************************************************************************/ + +static BzDBusObj * +_bz_dbus_obj_new(NMBluezManager *self, const char *object_path) +{ + BzDBusObj *bzobj; + gsize l; + + nm_assert(NM_IS_BLUEZ_MANAGER(self)); + + l = strlen(object_path) + 1; + + bzobj = g_malloc(sizeof(BzDBusObj) + l); + *bzobj = (BzDBusObj){ + .object_path = bzobj->_object_path_intern, + .self = self, + .x_network_server.lst = C_LIST_INIT(bzobj->x_network_server.lst), + .process_change_lst = C_LIST_INIT(bzobj->process_change_lst), + .x_device_panu_connection_allow_create = TRUE, + }; + memcpy(bzobj->_object_path_intern, object_path, l); + + return bzobj; +} + +static void +_bz_dbus_obj_free(BzDBusObj *bzobj) +{ + nm_assert(bzobj); + nm_assert(NM_IS_BLUEZ_MANAGER(bzobj->self)); + nm_assert(!bzobj->x_network_server.device_br); + nm_assert(!bzobj->x_network_server.r_req_data); + nm_assert(!bzobj->x_device.c_req_data); + + c_list_unlink_stale(&bzobj->process_change_lst); + c_list_unlink_stale(&bzobj->x_network_server.lst); + g_free(bzobj->x_network_server.adapter_address); + g_free(bzobj->d_adapter.address); + g_free(bzobj->d_network.interface); + g_free(bzobj->d_device.address); + g_free(bzobj->d_device.name); + g_free(bzobj->d_device.adapter); + g_free(bzobj); +} + +/*****************************************************************************/ + +static const char * +_bzobj_to_string(const BzDBusObj *bzobj, char *buf, gsize len) +{ + char * buf0 = buf; + const char *prefix = ""; + gboolean device_is_usable; + gboolean create_panu_connection = FALSE; + gboolean network_server_is_usable; + char sbuf_cap[100]; + + if (len > 0) + buf[0] = '\0'; + + if (bzobj->d_has_adapter_iface) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append_str(&buf, &len, "Adapter1 {"); + if (bzobj->d_adapter.address) { + nm_utils_strbuf_append(&buf, &len, " d.address: \"%s\"", bzobj->d_adapter.address); + if (bzobj->d_adapter_powered) + nm_utils_strbuf_append_str(&buf, &len, ","); + } + if (bzobj->d_adapter_powered) + nm_utils_strbuf_append(&buf, &len, " d.powered: 1"); + nm_utils_strbuf_append_str(&buf, &len, " }"); + } + + if (bzobj->d_has_device_iface) { + const char *prefix1 = ""; + + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append_str(&buf, &len, "Device1 {"); + if (bzobj->d_device.address) { + nm_utils_strbuf_append(&buf, + &len, + "%s d.address: \"%s\"", + prefix1, + bzobj->d_device.address); + prefix1 = ","; + } + if (bzobj->d_device.name) { + nm_utils_strbuf_append(&buf, &len, "%s d.name: \"%s\"", prefix1, bzobj->d_device.name); + prefix1 = ","; + } + if (bzobj->d_device.adapter) { + nm_utils_strbuf_append(&buf, + &len, + "%s d.adapter: \"%s\"", + prefix1, + bzobj->d_device.adapter); + prefix1 = ","; + } + if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) { + nm_utils_strbuf_append(&buf, + &len, + "%s d.capabilities: \"%s\"", + prefix1, + nm_bluetooth_capability_to_string(bzobj->d_device_capabilities, + sbuf_cap, + sizeof(sbuf_cap))); + prefix1 = ","; + } + if (bzobj->d_device_connected) { + nm_utils_strbuf_append(&buf, &len, "%s d.connected: 1", prefix1); + prefix1 = ","; + } + if (bzobj->d_device_paired) { + nm_utils_strbuf_append(&buf, &len, "%s d.paired: 1", prefix1); + prefix1 = ","; + } + nm_utils_strbuf_append_str(&buf, &len, " }"); + } + + network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE); + + if (bzobj->d_has_network_server_iface + || network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst)) + || !c_list_is_empty(&bzobj->x_network_server.lst) + || !nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL, + bzobj->x_network_server.adapter_address) + || bzobj->x_network_server.device_br || bzobj->x_network_server.r_req_data) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + + nm_utils_strbuf_append(&buf, &len, "NetworkServer1 { "); + + if (!bzobj->d_has_network_server_iface) + nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, "); + + if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst))) + nm_utils_strbuf_append(&buf, + &len, + "usable: %d, used: %d", + !!network_server_is_usable, + !network_server_is_usable); + else if (network_server_is_usable) + nm_utils_strbuf_append(&buf, &len, "used: 1"); + else + nm_utils_strbuf_append(&buf, &len, "usable: 0"); + + if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL, + bzobj->x_network_server.adapter_address)) { + if (bzobj->x_network_server.adapter_address) + nm_utils_strbuf_append(&buf, + &len, + ", adapter-address: \"%s\"", + bzobj->x_network_server.adapter_address); + else + nm_utils_strbuf_append(&buf, &len, ", adapter-address: <NULL>"); + } + + if (bzobj->x_network_server.device_br) + nm_utils_strbuf_append(&buf, &len, ", bridge-device: 1"); + + if (bzobj->x_network_server.r_req_data) + nm_utils_strbuf_append(&buf, &len, ", register-in-progress: 1"); + + nm_utils_strbuf_append_str(&buf, &len, " }"); + } + + device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection); + + if (bzobj->d_has_network_iface || bzobj->d_network.interface || bzobj->d_network_connected + || create_panu_connection || bzobj->x_device.panu_connection + || device_is_usable != bzobj->x_device_is_usable || bzobj->x_device.device_bt + || bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE + || bzobj->x_device.connect_dun_context || bzobj->x_device.c_req_data + || bzobj->x_device_is_connected != bzobj->d_network_connected) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append_str(&buf, &len, "Network1 {"); + if (bzobj->d_network.interface) + nm_utils_strbuf_append(&buf, + &len, + " d.interface: \"%s\", ", + bzobj->d_network.interface); + if (bzobj->d_network_connected) + nm_utils_strbuf_append(&buf, &len, " d.connected: %d, ", !!bzobj->d_network_connected); + if (!bzobj->d_has_network_iface) + nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, "); + if (device_is_usable != bzobj->x_device_is_usable) + nm_utils_strbuf_append(&buf, + &len, + " usable: %d, used: %d", + !!device_is_usable, + !device_is_usable); + else if (device_is_usable) + nm_utils_strbuf_append(&buf, &len, " used: 1"); + else + nm_utils_strbuf_append(&buf, &len, " usable: 0"); + + if (create_panu_connection) + nm_utils_strbuf_append(&buf, &len, ", create-panu-connection: 1"); + + if (bzobj->x_device.panu_connection) + nm_utils_strbuf_append(&buf, &len, ", has-panu-connection: 1"); + + if (bzobj->x_device.device_bt) + nm_utils_strbuf_append(&buf, &len, ", has-device: 1"); + + if (bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE + || bzobj->x_device.connect_dun_context) { + nm_utils_strbuf_append( + &buf, + &len, + ", connect: %s%s", + nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type, + sbuf_cap, + sizeof(sbuf_cap)), + bzobj->x_device.connect_dun_context ? ",with-dun-context" : ""); + } + + if (bzobj->x_device.c_req_data) + nm_utils_strbuf_append(&buf, &len, ", connecting: 1"); + + if (bzobj->x_device_is_connected != bzobj->d_network_connected) + nm_utils_strbuf_append(&buf, &len, ", connected: %d", !!bzobj->x_device_is_connected); + + nm_utils_strbuf_append_str(&buf, &len, " }"); + } + + if (_bzobjs_is_dead(bzobj)) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append_str(&buf, &len, "dead: 1"); + } + + if (!c_list_is_empty(&bzobj->process_change_lst)) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append(&buf, &len, "change-pending-on-idle: 1"); + } + + if (_bzobjs_adapter_is_usable_for_device(bzobj) + != bzobj->was_usable_adapter_for_device_before) { + nm_utils_strbuf_append_str(&buf, &len, prefix); + prefix = ", "; + nm_utils_strbuf_append(&buf, &len, "change-usable-adapter-for-device: 1"); + } + + return buf0; +} + +#define _LOG_bzobj(bzobj, context) \ + G_STMT_START \ + { \ + const BzDBusObj *const _bzobj = (bzobj); \ + char _buf[500]; \ + \ + _LOGT("change %-21s %s : { %s }", \ + (context), \ + _bzobj->object_path, \ + _bzobj_to_string(_bzobj, _buf, sizeof(_buf))); \ + } \ + G_STMT_END + +static gboolean +_bzobjs_is_dead(const BzDBusObj *bzobj) +{ + return !bzobj->d_has_adapter_iface && !bzobj->d_has_device_iface && !bzobj->d_has_network_iface + && !bzobj->d_has_network_server_iface && c_list_is_empty(&bzobj->process_change_lst); +} + +static BzDBusObj * +_bzobjs_get(NMBluezManager *self, const char *object_path) +{ + return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->bzobjs, &object_path); +} + +static BzDBusObj * +_bzobjs_add(NMBluezManager *self, const char *object_path) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + BzDBusObj * bzobj; + + bzobj = _bz_dbus_obj_new(self, object_path); + if (!g_hash_table_add(priv->bzobjs, bzobj)) + nm_assert_not_reached(); + return bzobj; +} + +static void +_bzobjs_del(BzDBusObj *bzobj) +{ + nm_assert(bzobj); + nm_assert(bzobj == _bzobjs_get(bzobj->self, bzobj->object_path)); + + if (!g_hash_table_remove(NM_BLUEZ_MANAGER_GET_PRIVATE(bzobj->self)->bzobjs, bzobj)) + nm_assert_not_reached(); +} + +static void +_bzobjs_del_if_dead(BzDBusObj *bzobj) +{ + if (_bzobjs_is_dead(bzobj)) + _bzobjs_del(bzobj); +} + +static BzDBusObj * +_bzobjs_init(NMBluezManager *self, BzDBusObj **inout, const char *object_path) +{ + nm_assert(NM_IS_BLUEZ_MANAGER(self)); + nm_assert(object_path); + nm_assert(inout); + + if (!*inout) { + *inout = _bzobjs_get(self, object_path); + if (!*inout) + *inout = _bzobjs_add(self, object_path); + } + + nm_assert(nm_streq((*inout)->object_path, object_path)); + nm_assert(*inout == _bzobjs_get(self, object_path)); + return *inout; +} + +static gboolean +_bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj) +{ + return bzobj->d_has_adapter_iface && bzobj->d_adapter.address && bzobj->d_adapter_powered; +} + +static gboolean +_bzobjs_device_is_usable(const BzDBusObj *bzobj, + BzDBusObj ** out_adapter_bzobj, + gboolean * out_create_panu_connection) +{ + NMBluezManager * self; + NMBluezManagerPrivate *priv; + gboolean usable_dun = FALSE; + gboolean usable_nap = FALSE; + BzDBusObj * bzobj_adapter; + gboolean create_panu_connection = FALSE; + + if (!bzobj->d_has_device_iface + || !NM_FLAGS_ANY((NMBluetoothCapabilities) bzobj->d_device_capabilities, + _NM_BT_CAPABILITY_SUPPORTED) + || !bzobj->d_device.name || !bzobj->d_device.address || !bzobj->d_device_paired + || !bzobj->d_device.adapter) + goto out_unusable; + + self = bzobj->self; + + priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + if (!priv->settings_registered) + goto out_unusable; + + bzobj_adapter = _bzobjs_get(self, bzobj->d_device.adapter); + if (!bzobj_adapter || !_bzobjs_adapter_is_usable_for_device(bzobj_adapter)) + goto out_unusable; + +#if WITH_BLUEZ5_DUN + if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_DUN)) { + if (_conn_track_find_head(self, NM_BT_CAPABILITY_DUN, bzobj->d_device.address)) + usable_dun = TRUE; + } +#endif + + if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)) { + if (!bzobj->d_has_network_iface) + usable_nap = FALSE; + else if (_conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)) + usable_nap = TRUE; + else if (bzobj->x_device_panu_connection_allow_create) { + /* We didn't yet try to create a connection. Presume we are going to create + * it when the time comes... */ + usable_nap = TRUE; + create_panu_connection = TRUE; + } + } + + if (!usable_dun && !usable_nap) { + if (bzobj->x_device.device_bt + && nm_device_get_state(NM_DEVICE(bzobj->x_device.device_bt)) + > NM_DEVICE_STATE_DISCONNECTED) { + /* The device is still activated... the absence of a profile does not + * render it unusable (yet). But since there is no more profile, the + * device is probably about to disconnect. */ + } else + goto out_unusable; + } + + NM_SET_OUT(out_create_panu_connection, create_panu_connection); + NM_SET_OUT(out_adapter_bzobj, bzobj_adapter); + return TRUE; + +out_unusable: + NM_SET_OUT(out_create_panu_connection, FALSE); + NM_SET_OUT(out_adapter_bzobj, NULL); + return FALSE; +} + +static gboolean +_bzobjs_device_is_connected(const BzDBusObj *bzobj) +{ + nm_assert(_bzobjs_device_is_usable(bzobj, NULL, NULL)); + + if (!bzobj->d_has_device_iface || !bzobj->d_device_connected) + return FALSE; + + if (bzobj->d_has_network_iface && bzobj->d_network_connected) + return TRUE; + if (bzobj->x_device.connect_dun_context) { + /* As long as we have a dun-context, we consider it connected. + * + * We require NMDeviceBt to try to connect to the modem, and if that fails, + * it will disconnect. */ + return TRUE; + } + return FALSE; +} + +static gboolean +_bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered) +{ + return bzobj->d_has_network_server_iface && bzobj->d_has_adapter_iface + && bzobj->d_adapter.address && (!require_powered || bzobj->d_adapter_powered); +} + +/*****************************************************************************/ + +static ConnDataHead * +_conn_data_head_new(NMBluetoothCapabilities bt_type, const char *bdaddr) +{ + ConnDataHead *cdata_hd; + gsize l; + + nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP)); + nm_assert(bdaddr); + + l = strlen(bdaddr) + 1; + cdata_hd = g_malloc(sizeof(ConnDataHead) + l); + *cdata_hd = (ConnDataHead){ + .bdaddr = cdata_hd->bdaddr_data, + .lst_head = C_LIST_INIT(cdata_hd->lst_head), + .bt_type = bt_type, + }; + memcpy(cdata_hd->bdaddr_data, bdaddr, l); + + nm_assert(cdata_hd->bt_type == bt_type); + + return cdata_hd; +} + +static guint +_conn_data_head_hash(gconstpointer ptr) +{ + const ConnDataHead *cdata_hd = ptr; + NMHashState h; + + nm_hash_init(&h, 520317467u); + nm_hash_update_val(&h, (NMBluetoothCapabilities) cdata_hd->bt_type); + nm_hash_update_str(&h, cdata_hd->bdaddr); + return nm_hash_complete(&h); +} + +static gboolean +_conn_data_head_equal(gconstpointer a, gconstpointer b) +{ + const ConnDataHead *cdata_hd_a = a; + const ConnDataHead *cdata_hd_b = b; + + return cdata_hd_a->bt_type == cdata_hd_b->bt_type + && nm_streq(cdata_hd_a->bdaddr, cdata_hd_b->bdaddr); +} + +static ConnDataHead * +_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr) +{ + ConnDataHead cdata_hd = { + .bt_type = bt_type, + .bdaddr = bdaddr, + }; + + return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_heads, &cdata_hd); +} + +static ConnDataElem * +_conn_track_find_elem(NMBluezManager *self, NMSettingsConnection *sett_conn) +{ + G_STATIC_ASSERT(G_STRUCT_OFFSET(ConnDataElem, sett_conn) == 0); + + return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_elems, &sett_conn); +} + +static gboolean +_conn_track_is_relevant_connection(NMConnection * connection, + NMBluetoothCapabilities *out_bt_type, + const char ** out_bdaddr) +{ + NMSettingBluetooth * s_bt; + NMBluetoothCapabilities bt_type; + const char * bdaddr; + const char * b_type; + + s_bt = nm_connection_get_setting_bluetooth(connection); + if (!s_bt) + return FALSE; + + if (!nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) + return FALSE; + + bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt); + if (!bdaddr) + return FALSE; + + b_type = nm_setting_bluetooth_get_connection_type(s_bt); + + if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_DUN)) + bt_type = NM_BT_CAPABILITY_DUN; + else if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_PANU)) + bt_type = NM_BT_CAPABILITY_NAP; + else + return FALSE; + + NM_SET_OUT(out_bt_type, bt_type); + NM_SET_OUT(out_bdaddr, bdaddr); + return TRUE; +} + +static gboolean +_conn_track_is_relevant_sett_conn(NMSettingsConnection * sett_conn, + NMBluetoothCapabilities *out_bt_type, + const char ** out_bdaddr) +{ + NMConnection *connection; + + connection = nm_settings_connection_get_connection(sett_conn); + if (!connection) + return FALSE; + + return _conn_track_is_relevant_connection(connection, out_bt_type, out_bdaddr); +} + +static gboolean +_conn_track_is_relevant_for_sett_conn(NMSettingsConnection * sett_conn, + NMBluetoothCapabilities bt_type, + const char * bdaddr) +{ + NMBluetoothCapabilities x_bt_type; + const char * x_bdaddr; + + return bdaddr && _conn_track_is_relevant_sett_conn(sett_conn, &x_bt_type, &x_bdaddr) + && x_bt_type == bt_type && nm_streq(x_bdaddr, bdaddr); +} + +static void +_conn_track_schedule_notify(NMBluezManager * self, + NMBluetoothCapabilities bt_type, + const char * bdaddr) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + GHashTableIter iter; + BzDBusObj * bzobj; + + g_hash_table_iter_init(&iter, priv->bzobjs); + while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) { + gboolean device_is_usable; + + device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL); + if (bzobj->x_device_is_usable != device_is_usable) + _process_change_idle_schedule(self, bzobj); + } +} + +static void +_conn_track_update(NMBluezManager * self, + NMSettingsConnection *sett_conn, + gboolean track, + gboolean * out_changed, + gboolean * out_changed_usable, + ConnDataElem ** out_conn_data_elem) +{ + NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + ConnDataHead * cdata_hd; + ConnDataElem * cdata_el; + ConnDataElem * cdata_el_remove = NULL; + NMBluetoothCapabilities bt_type; + const char * bdaddr; + gboolean changed = FALSE; + gboolean changed_usable = FALSE; + char sbuf_cap[100]; + + nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn)); + + cdata_el = _conn_track_find_elem(self, sett_conn); + + if (track) + track = _conn_track_is_relevant_sett_conn(sett_conn, &bt_type, &bdaddr); + + if (!track) { + cdata_el_remove = g_steal_pointer(&cdata_el); + goto out_remove; + } + + if (cdata_el) { + cdata_hd = cdata_el->cdata_hd; + if (cdata_hd->bt_type != bt_type || !nm_streq(cdata_hd->bdaddr, bdaddr)) + cdata_el_remove = g_steal_pointer(&cdata_el); + } + + if (!cdata_el) { + _LOGT("connection: track for %s, %s: %s (%s)", + nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)), + bdaddr, + nm_settings_connection_get_uuid(sett_conn), + nm_settings_connection_get_id(sett_conn)); + changed = TRUE; + cdata_hd = _conn_track_find_head(self, bt_type, bdaddr); + if (!cdata_hd) { + changed_usable = TRUE; + cdata_hd = _conn_data_head_new(bt_type, bdaddr); + if (!g_hash_table_add(priv->conn_data_heads, cdata_hd)) + nm_assert_not_reached(); + _conn_track_schedule_notify(self, bt_type, bdaddr); + } + cdata_el = g_slice_new(ConnDataElem); + cdata_el->sett_conn = sett_conn; + cdata_el->cdata_hd = cdata_hd; + c_list_link_tail(&cdata_hd->lst_head, &cdata_el->lst); + if (!g_hash_table_add(priv->conn_data_elems, cdata_el)) + nm_assert_not_reached(); + } + +out_remove: + if (cdata_el_remove) { + GHashTableIter iter; + BzDBusObj * bzobj; + + _LOGT("connection: untrack for %s, %s: %s (%s)", + nm_bluetooth_capability_to_string(cdata_el_remove->cdata_hd->bt_type, + sbuf_cap, + sizeof(sbuf_cap)), + cdata_el_remove->cdata_hd->bdaddr, + nm_settings_connection_get_uuid(sett_conn), + nm_settings_connection_get_id(sett_conn)); + + g_hash_table_iter_init(&iter, priv->bzobjs); + while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) { + if (bzobj->x_device.panu_connection == sett_conn) + bzobj->x_device.panu_connection = NULL; + } + + changed = TRUE; + cdata_hd = cdata_el_remove->cdata_hd; + c_list_unlink_stale(&cdata_el_remove->lst); + if (!g_hash_table_remove(priv->conn_data_elems, cdata_el_remove)) + nm_assert_not_reached(); + if (c_list_is_empty(&cdata_hd->lst_head)) { + changed_usable = TRUE; + _conn_track_schedule_notify(self, cdata_hd->bt_type, cdata_hd->bdaddr); + if (!g_hash_table_remove(priv->conn_data_heads, cdata_hd)) + nm_assert_not_reached(); + } + } + + NM_SET_OUT(out_changed, changed); + NM_SET_OUT(out_changed_usable, changed_usable); + NM_SET_OUT(out_conn_data_elem, cdata_el); +} + +/*****************************************************************************/ + +static void +cp_connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self) +{ + _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL); +} + +static void +cp_connection_updated(NMSettings * settings, + NMSettingsConnection *sett_conn, + guint update_reason_u, + NMBluezManager * self) +{ + _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL); +} + +static void +cp_connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self) +{ + _conn_track_update(self, sett_conn, FALSE, NULL, NULL, NULL); +} + +/*****************************************************************************/ + +static NMBluezManager * +_network_server_get_bluez_manager(const NMBtVTableNetworkServer *vtable_network_server) +{ + NMBluezManager *self; + + self = (NMBluezManager *) (((char *) vtable_network_server) + - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server)); + + g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), NULL); + + return self; +} + +static BzDBusObj * +_network_server_find_has_device(NMBluezManagerPrivate *priv, NMDevice *device) +{ + BzDBusObj *bzobj; + + c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) { + if (bzobj->x_network_server.device_br == device) + return bzobj; + } + return NULL; +} + +static BzDBusObj * +_network_server_find_available(NMBluezManagerPrivate *priv, + const char * addr, + NMDevice * device_accept_busy) +{ + BzDBusObj *bzobj; + + c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) { + if (bzobj->x_network_server.device_br) { + if (bzobj->x_network_server.device_br != device_accept_busy) + continue; + } + if (addr && !nm_streq(addr, bzobj->d_adapter.address)) + continue; + nm_assert(!bzobj->x_network_server.r_req_data); + return bzobj; + } + return NULL; +} + +static gboolean +_network_server_vt_is_available(const NMBtVTableNetworkServer *vtable, + const char * addr, + NMDevice * device_accept_busy) +{ + NMBluezManager * self = _network_server_get_bluez_manager(vtable); + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + return !!_network_server_find_available(priv, addr, device_accept_busy); +} + +static void +_network_server_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + gs_unref_variant GVariant *ret = NULL; + gs_free_error GError *error = NULL; + BzDBusObj * bzobj; + + ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); + if (!ret && nm_utils_error_is_cancelled(error)) + return; + + bzobj = user_data; + + if (!ret) { + _LOGT("NAP: [%s]: registering failed: %s", bzobj->object_path, error->message); + } else + _LOGT("NAP: [%s]: registration successful", bzobj->object_path); + + g_clear_object(&bzobj->x_network_server.r_req_data->int_cancellable); + _network_server_register_req_data_complete(g_steal_pointer(&bzobj->x_network_server.r_req_data), + error); +} + +static void +_network_server_register_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj) +{ + _network_server_unregister_bridge(bzobj->self, bzobj, "registration cancelled"); +} + +static gboolean +_network_server_vt_register_bridge(const NMBtVTableNetworkServer *vtable, + const char * addr, + NMDevice * device, + GCancellable * cancellable, + NMBtVTableRegisterCallback callback, + gpointer callback_user_data, + GError ** error) +{ + NMBluezManager * self = _network_server_get_bluez_manager(vtable); + NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + NetworkServerRegisterReqData *r_req_data; + BzDBusObj * bzobj; + const char * ifname; + + g_return_val_if_fail(NM_IS_DEVICE(device), FALSE); + g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE); + + nm_assert(!g_cancellable_is_cancelled(cancellable)); + nm_assert(!_network_server_find_has_device(priv, device)); + + ifname = nm_device_get_iface(device); + g_return_val_if_fail(ifname, FALSE); + + g_return_val_if_fail(ifname, FALSE); + + bzobj = _network_server_find_available(priv, addr, NULL); + if (!bzobj) { + /* The device checked that a network server is available, before + * starting the activation, but for some reason it no longer is. + * Indicate that the activation should not proceed. */ + if (addr) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "adapter %s is not available for %s", + addr, + ifname); + } else { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "no adapter available for %s", + ifname); + } + return FALSE; + } + + _LOGD("NAP: [%s]: registering \"%s\" on adapter %s", + bzobj->object_path, + ifname, + bzobj->d_adapter.address); + + r_req_data = g_slice_new(NetworkServerRegisterReqData); + *r_req_data = (NetworkServerRegisterReqData){ + .int_cancellable = g_cancellable_new(), + .ext_cancellable = g_object_ref(cancellable), + .callback = callback, + .callback_user_data = callback_user_data, + .ext_cancelled_id = g_signal_connect(cancellable, + "cancelled", + G_CALLBACK(_network_server_register_cancelled_cb), + bzobj), + }; + + bzobj->x_network_server.device_br = g_object_ref(device); + bzobj->x_network_server.r_req_data = r_req_data; + + g_dbus_connection_call(priv->dbus_connection, + priv->name_owner, + bzobj->object_path, + NM_BLUEZ5_NETWORK_SERVER_INTERFACE, + "Register", + g_variant_new("(ss)", BLUETOOTH_CONNECT_NAP, ifname), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + bzobj->x_network_server.r_req_data->int_cancellable, + _network_server_register_cb, + bzobj); + return TRUE; +} + +static void +_network_server_unregister_bridge_complete_on_idle_cb(gpointer user_data, GCancellable *cancellable) +{ + gs_free_error GError * error = NULL; + gs_free char * reason = NULL; + NetworkServerRegisterReqData *r_req_data; + + nm_utils_user_data_unpack(user_data, &r_req_data, &reason); + + nm_utils_error_set(&error, + NM_UTILS_ERROR_UNKNOWN, + "registration was aborted due to %s", + reason); + _network_server_register_req_data_complete(r_req_data, error); +} + +static void +_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason) +{ + NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + _nm_unused gs_unref_object NMDevice *device = NULL; + NetworkServerRegisterReqData * r_req_data; + + nm_assert(NM_IS_DEVICE(bzobj->x_network_server.device_br)); + + _LOGD("NAP: [%s]: unregistering \"%s\" (%s)", + bzobj->object_path, + nm_device_get_iface(bzobj->x_network_server.device_br), + reason); + + device = g_steal_pointer(&bzobj->x_network_server.device_br); + + r_req_data = g_steal_pointer(&bzobj->x_network_server.r_req_data); + + if (priv->name_owner) { + gs_unref_object GCancellable *cancellable = NULL; + + cancellable = g_cancellable_new(); + + nm_shutdown_wait_obj_register_cancellable_full( + cancellable, + g_strdup_printf("bt-unregister-nap[%s]", bzobj->object_path), + TRUE); + + g_dbus_connection_call(priv->dbus_connection, + priv->name_owner, + bzobj->object_path, + NM_BLUEZ5_NETWORK_SERVER_INTERFACE, + "Unregister", + g_variant_new("(s)", BLUETOOTH_CONNECT_NAP), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + cancellable, + _dbus_call_complete_cb_nop, + NULL); + } + + if (r_req_data) { + nm_clear_g_cancellable(&r_req_data->int_cancellable); + nm_utils_invoke_on_idle(r_req_data->ext_cancellable, + _network_server_unregister_bridge_complete_on_idle_cb, + nm_utils_user_data_pack(r_req_data, g_strdup(reason))); + } + + _nm_device_bridge_notify_unregister_bt_nap(device, reason); +} + +static gboolean +_network_server_vt_unregister_bridge(const NMBtVTableNetworkServer *vtable, NMDevice *device) +{ + NMBluezManager * self = _network_server_get_bluez_manager(vtable); + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + BzDBusObj * bzobj; + + g_return_val_if_fail(NM_IS_DEVICE(device), FALSE); + + bzobj = _network_server_find_has_device(priv, device); + if (bzobj) + _network_server_unregister_bridge(self, bzobj, "disconnecting"); + + return TRUE; +} + +static void +_network_server_process_change(BzDBusObj *bzobj, gboolean *out_emit_device_availability_changed) +{ + NMBluezManager * self = bzobj->self; + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + gboolean network_server_is_usable; + gboolean emit_device_availability_changed = FALSE; + + network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE); + + if (!network_server_is_usable) { + if (!c_list_is_empty(&bzobj->x_network_server.lst)) { + emit_device_availability_changed = TRUE; + c_list_unlink(&bzobj->x_network_server.lst); + } + + nm_clear_g_free(&bzobj->x_network_server.adapter_address); + + if (bzobj->x_network_server.device_br) { + _network_server_unregister_bridge(self, + bzobj, + _bzobjs_network_server_is_usable(bzobj, FALSE) + ? "adapter disabled" + : "adapter disappeared"); + } + + } else { + if (!nm_streq0(bzobj->x_network_server.adapter_address, bzobj->d_adapter.address)) { + emit_device_availability_changed = TRUE; + g_free(bzobj->x_network_server.adapter_address); + bzobj->x_network_server.adapter_address = g_strdup(bzobj->d_adapter.address); + } + + if (c_list_is_empty(&bzobj->x_network_server.lst)) { + emit_device_availability_changed = TRUE; + c_list_link_tail(&priv->network_server_lst_head, &bzobj->x_network_server.lst); + } + } + + if (emit_device_availability_changed) + NM_SET_OUT(out_emit_device_availability_changed, TRUE); +} + +/*****************************************************************************/ + +static void +_conn_create_panu_connection(NMBluezManager *self, BzDBusObj *bzobj) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + gs_unref_object NMConnection *connection = NULL; + NMSettingsConnection * added; + NMSetting * setting; + gs_free char * id = NULL; + char uuid[37]; + gs_free_error GError *error = NULL; + + nm_utils_uuid_generate_buf(uuid); + id = g_strdup_printf(_("%s Network"), bzobj->d_device.name); + + connection = nm_simple_connection_new(); + + setting = nm_setting_connection_new(); + g_object_set(setting, + NM_SETTING_CONNECTION_ID, + id, + NM_SETTING_CONNECTION_UUID, + uuid, + NM_SETTING_CONNECTION_AUTOCONNECT, + FALSE, + NM_SETTING_CONNECTION_TYPE, + NM_SETTING_BLUETOOTH_SETTING_NAME, + NULL); + nm_connection_add_setting(connection, setting); + + setting = nm_setting_bluetooth_new(); + g_object_set(setting, + NM_SETTING_BLUETOOTH_BDADDR, + bzobj->d_device.address, + NM_SETTING_BLUETOOTH_TYPE, + NM_SETTING_BLUETOOTH_TYPE_PANU, + NULL); + nm_connection_add_setting(connection, setting); + + if (!nm_connection_normalize(connection, NULL, NULL, &error)) { + _LOGE("connection: couldn't generate a connection for NAP device: %s", error->message); + g_return_if_reached(); + } + + nm_assert(_conn_track_is_relevant_connection(connection, NULL, NULL)); + + _LOGT("connection: create in-memory PANU connection %s (%s) for device \"%s\" (%s)", + uuid, + id, + bzobj->d_device.name, + bzobj->d_device.address); + + nm_settings_add_connection(priv->settings, + connection, + NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY, + NM_SETTINGS_CONNECTION_ADD_REASON_NONE, + NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED, + &added, + &error); + if (!added) { + _LOGW("connection: couldn't add new Bluetooth connection for NAP device: '%s' (%s): %s", + id, + uuid, + error->message); + return; + } + + if (!_conn_track_is_relevant_for_sett_conn(added, NM_BT_CAPABILITY_NAP, bzobj->d_device.address) + || !_conn_track_find_elem(self, added) || bzobj->x_device.panu_connection) { + _LOGE("connection: something went wrong creating PANU connection %s (%s) for device '%s'", + uuid, + id, + bzobj->d_device.address); + g_return_if_reached(); + } + + bzobj->x_device.panu_connection = added; +} + +/*****************************************************************************/ + +static void +_device_state_changed_cb(NMDevice *device, + guint new_state_u, + guint old_state_u, + guint reason_u, + gpointer user_data) +{ + BzDBusObj *bzobj = user_data; + + if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) { + /* the device got unusable? Need to revisit it... */ + _process_change_idle_schedule(bzobj->self, bzobj); + } +} + +static void +_device_process_change(BzDBusObj *bzobj) +{ + NMBluezManager *self = bzobj->self; + gs_unref_object NMDeviceBt *device_added = NULL; + gs_unref_object NMDeviceBt *device_deleted = NULL; + gboolean device_is_usable; + gboolean create_panu_connection = FALSE; + + device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection); + + if (create_panu_connection) { + bzobj->x_device_panu_connection_allow_create = FALSE; + _conn_create_panu_connection(self, bzobj); + device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL); + } else { + if (device_is_usable && bzobj->x_device_panu_connection_allow_create + && NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP) + && _conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)) { + /* We have a useable device and also a panu-connection. We block future attemps + * to generate a connection. */ + bzobj->x_device_panu_connection_allow_create = FALSE; + } + if (bzobj->x_device.panu_connection) { + if (!NM_FLAGS_HAS(nm_settings_connection_get_flags(bzobj->x_device.panu_connection), + NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) { + /* the connection that we generated earlier still exists, but it's not longer the same + * as it was when we created it. Forget about it, so that we don't delete the profile later... */ + bzobj->x_device.panu_connection = NULL; + } else { + if (!device_is_usable + || !_conn_track_is_relevant_for_sett_conn(bzobj->x_device.panu_connection, + NM_BT_CAPABILITY_NAP, + bzobj->d_device.address)) { + _LOGT("connection: delete in-memory PANU connection %s (%s) as device %s", + nm_settings_connection_get_uuid(bzobj->x_device.panu_connection), + nm_settings_connection_get_id(bzobj->x_device.panu_connection), + !device_is_usable ? "is now unusable" : "no longer matches"); + bzobj->x_device_panu_connection_allow_create = TRUE; + nm_settings_connection_delete(g_steal_pointer(&bzobj->x_device.panu_connection), + FALSE); + } + } + } + } + + bzobj->x_device_is_connected = device_is_usable && _bzobjs_device_is_connected(bzobj); + + bzobj->x_device_is_usable = device_is_usable; + + if (bzobj->x_device.device_bt) { + const char *device_to_delete_msg; + + if (!device_is_usable) + device_to_delete_msg = "device became unusable"; + else if (!_nm_device_bt_for_same_device(bzobj->x_device.device_bt, + bzobj->object_path, + bzobj->d_device.address, + NULL, + bzobj->d_device_capabilities)) + device_to_delete_msg = "device is no longer compatible"; + else + device_to_delete_msg = NULL; + + if (device_to_delete_msg) { + nm_clear_g_signal_handler(bzobj->x_device.device_bt, + &bzobj->x_device.device_bt_signal_id); + + device_deleted = g_steal_pointer(&bzobj->x_device.device_bt); + + _LOGD("[%s]: drop device because %s", bzobj->object_path, device_to_delete_msg); + + _connect_disconnect(self, bzobj, device_to_delete_msg); + } + } + + if (device_is_usable) { + if (!bzobj->x_device.device_bt) { + bzobj->x_device.device_bt = nm_device_bt_new(self, + bzobj->object_path, + bzobj->d_device.address, + bzobj->d_device.name, + bzobj->d_device_capabilities); + device_added = g_object_ref(bzobj->x_device.device_bt); + bzobj->x_device.device_bt_signal_id = + g_signal_connect(device_added, + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(_device_state_changed_cb), + bzobj); + } else + _nm_device_bt_notify_set_name(bzobj->x_device.device_bt, bzobj->d_device.name); + + _nm_device_bt_notify_set_connected(bzobj->x_device.device_bt, bzobj->x_device_is_connected); + } + + if (bzobj->x_device.c_req_data && !bzobj->x_device.c_req_data->int_cancellable + && bzobj->x_device_is_connected) { + gs_free char *device_name = g_steal_pointer(&bzobj->x_device.c_req_data->device_name); + + _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data), + self, + device_name, + NULL); + } + + if (device_added) + g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device_added); + + if (device_deleted) + _nm_device_bt_notify_removed(device_deleted); +} + +/*****************************************************************************/ + +static void +_process_change_idle_all(NMBluezManager *self, gboolean *out_emit_device_availability_changed) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + BzDBusObj * bzobj; + + while ( + (bzobj = + c_list_first_entry(&priv->process_change_lst_head, BzDBusObj, process_change_lst))) { + c_list_unlink(&bzobj->process_change_lst); + + _LOG_bzobj(bzobj, "before-processing"); + + _device_process_change(bzobj); + + _network_server_process_change(bzobj, out_emit_device_availability_changed); + + _LOG_bzobj(bzobj, "after-processing"); + + _bzobjs_del_if_dead(bzobj); + } + + nm_clear_g_source(&priv->process_change_idle_id); +} + +static gboolean +_process_change_idle_cb(gpointer user_data) +{ + NMBluezManager * self = user_data; + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + gboolean emit_device_availability_changed = FALSE; + + _process_change_idle_all(self, &emit_device_availability_changed); + + if (emit_device_availability_changed) + nm_manager_notify_device_availability_maybe_changed(priv->manager); + + return G_SOURCE_CONTINUE; +} + +static void +_process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst); + if (priv->process_change_idle_id == 0) + priv->process_change_idle_id = + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1, _process_change_idle_cb, self, NULL); +} + +static void +_dbus_process_changes(NMBluezManager *self, BzDBusObj *bzobj, const char *log_reason) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + gboolean network_server_is_usable; + gboolean adapter_is_usable_for_device; + gboolean device_is_usable; + gboolean changes = FALSE; + gboolean recheck_devices_for_adapter = FALSE; + + nm_assert(bzobj); + + _LOG_bzobj(bzobj, log_reason); + + device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL); + + if (bzobj->x_device_is_usable != device_is_usable) + changes = TRUE; + else if (bzobj->x_device.device_bt) { + if (!device_is_usable) + changes = TRUE; + else { + if (bzobj->x_device_is_connected != _bzobjs_device_is_connected(bzobj) + || !_nm_device_bt_for_same_device(bzobj->x_device.device_bt, + bzobj->object_path, + bzobj->d_device.address, + bzobj->d_device.name, + bzobj->d_device_capabilities)) + changes = TRUE; + } + } + + adapter_is_usable_for_device = _bzobjs_adapter_is_usable_for_device(bzobj); + if (adapter_is_usable_for_device != bzobj->was_usable_adapter_for_device_before) { + /* this function does not modify bzobj in any other cases except here. + * Usually changes are processed delayed, in the idle handler. + * + * But the bzobj->was_usable_adapter_for_device_before only exists to know whether + * we need to re-check device availability. It is correct to set the flag + * here, right before we checked. */ + bzobj->was_usable_adapter_for_device_before = adapter_is_usable_for_device; + recheck_devices_for_adapter = TRUE; + changes = TRUE; + } + + if (!changes) { + network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE); + + if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst))) + changes = TRUE; + else if (bzobj->x_network_server.device_br && !network_server_is_usable) + changes = TRUE; + else if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL, + bzobj->x_network_server.adapter_address)) + changes = TRUE; + } + + if (changes) + _process_change_idle_schedule(self, bzobj); + + if (recheck_devices_for_adapter) { + GHashTableIter iter; + BzDBusObj * bzobj2; + + /* we got a change to the availability of an adapter. We might need to recheck + * all devices that use this adapter... */ + g_hash_table_iter_init(&iter, priv->bzobjs); + while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj2, NULL)) { + if (bzobj2 == bzobj) + continue; + if (!nm_streq0(bzobj2->d_device.adapter, bzobj->object_path)) + continue; + if (c_list_is_empty(&bzobj2->process_change_lst)) + _dbus_process_changes(self, bzobj2, "adapter-changed"); + else + nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj2->process_change_lst); + } + } + + _bzobjs_del_if_dead(bzobj); +} + +/*****************************************************************************/ + +#define ALL_RELEVANT_INTERFACE_NAMES \ + NM_MAKE_STRV(NM_BLUEZ5_ADAPTER_INTERFACE, \ + NM_BLUEZ5_DEVICE_INTERFACE, \ + NM_BLUEZ5_NETWORK_INTERFACE, \ + NM_BLUEZ5_NETWORK_SERVER_INTERFACE) + +static gboolean +_dbus_handle_properties_changed(NMBluezManager * self, + const char * object_path, + const char * interface_name, + GVariant * changed_properties, + const char *const *invalidated_properties, + BzDBusObj ** inout_bzobj) +{ + BzDBusObj * bzobj = NULL; + gboolean changed = FALSE; + const char * property_name; + GVariant * property_value; + GVariantIter iter_prop; + gsize i; + + if (!invalidated_properties) + invalidated_properties = NM_PTRARRAY_EMPTY(const char *); + + nm_assert(g_variant_is_of_type(changed_properties, G_VARIANT_TYPE("a{sv}"))); + + if (inout_bzobj) { + bzobj = *inout_bzobj; + nm_assert(!bzobj || nm_streq(object_path, bzobj->object_path)); + } + + if (changed_properties) + g_variant_iter_init(&iter_prop, changed_properties); + + if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) { + _bzobjs_init(self, &bzobj, object_path); + if (!bzobj->d_has_adapter_iface) { + changed = TRUE; + bzobj->d_has_adapter_iface = TRUE; + } + + while (changed_properties + && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) { + _nm_unused gs_unref_variant GVariant *property_value_free = property_value; + + if (nm_streq(property_name, "Address")) { + gs_free char *s = + g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING) + ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL), + ETH_ALEN) + : NULL; + + if (!nm_streq0(bzobj->d_adapter.address, s)) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_adapter.address); + bzobj->d_adapter.address = g_steal_pointer(&s); + } + continue; + } + if (nm_streq(property_name, "Powered")) { + bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN) + && g_variant_get_boolean(property_value); + + if (bzobj->d_adapter_powered != v) { + changed = TRUE; + bzobj->d_adapter_powered = v; + } + continue; + } + } + + for (i = 0; (property_name = invalidated_properties[i]); i++) { + if (nm_streq(property_name, "Address")) { + if (bzobj->d_adapter.address) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_adapter.address); + } + continue; + } + if (nm_streq(property_name, "Powered")) { + if (bzobj->d_adapter_powered) { + changed = TRUE; + bzobj->d_adapter_powered = FALSE; + } + continue; + } + } + + } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) { + _bzobjs_init(self, &bzobj, object_path); + if (!bzobj->d_has_device_iface) { + changed = TRUE; + bzobj->d_has_device_iface = TRUE; + } + + while (changed_properties + && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) { + _nm_unused gs_unref_variant GVariant *property_value_free = property_value; + + if (nm_streq(property_name, "Address")) { + gs_free char *s = + g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING) + ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL), + ETH_ALEN) + : NULL; + + if (!nm_streq0(bzobj->d_device.address, s)) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.address); + bzobj->d_device.address = g_steal_pointer(&s); + } + continue; + } + if (nm_streq(property_name, "Name")) { + const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING) + ? g_variant_get_string(property_value, NULL) + : NULL; + + if (!nm_streq0(bzobj->d_device.name, s)) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.name); + bzobj->d_device.name = g_strdup(s); + } + continue; + } + if (nm_streq(property_name, "Adapter")) { + const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_OBJECT_PATH) + ? g_variant_get_string(property_value, NULL) + : NULL; + + if (!nm_streq0(bzobj->d_device.adapter, s)) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.adapter); + bzobj->d_device.adapter = g_strdup(s); + } + continue; + } + if (nm_streq(property_name, "UUIDs")) { + NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE; + + if (g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING_ARRAY)) { + gs_free const char **s = g_variant_get_strv(property_value, NULL); + + capabilities = convert_uuids_to_capabilities(s); + } + if (bzobj->d_device_capabilities != capabilities) { + changed = TRUE; + bzobj->d_device_capabilities = capabilities; + nm_assert(bzobj->d_device_capabilities == capabilities); + } + continue; + } + if (nm_streq(property_name, "Connected")) { + bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN) + && g_variant_get_boolean(property_value); + + if (bzobj->d_device_connected != v) { + changed = TRUE; + bzobj->d_device_connected = v; + } + continue; + } + if (nm_streq(property_name, "Paired")) { + bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN) + && g_variant_get_boolean(property_value); + + if (bzobj->d_device_paired != v) { + changed = TRUE; + bzobj->d_device_paired = v; + } + continue; + } + } + + for (i = 0; (property_name = invalidated_properties[i]); i++) { + if (nm_streq(property_name, "Address")) { + if (bzobj->d_device.address) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.address); + } + continue; + } + if (nm_streq(property_name, "Name")) { + if (bzobj->d_device.name) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.name); + } + continue; + } + if (nm_streq(property_name, "Adapter")) { + if (bzobj->d_device.adapter) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.adapter); + } + continue; + } + if (nm_streq(property_name, "UUIDs")) { + if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) { + changed = TRUE; + bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE; + } + continue; + } + if (nm_streq(property_name, "Connected")) { + if (bzobj->d_device_connected) { + changed = TRUE; + bzobj->d_device_connected = FALSE; + } + continue; + } + if (nm_streq(property_name, "Paired")) { + if (bzobj->d_device_paired) { + changed = TRUE; + bzobj->d_device_paired = FALSE; + } + continue; + } + } + + } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) { + _bzobjs_init(self, &bzobj, object_path); + if (!bzobj->d_has_network_iface) { + changed = TRUE; + bzobj->d_has_network_iface = TRUE; + } + + while (changed_properties + && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) { + _nm_unused gs_unref_variant GVariant *property_value_free = property_value; + + if (nm_streq(property_name, "Interface")) { + const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING) + ? g_variant_get_string(property_value, NULL) + : NULL; + + if (!nm_streq0(bzobj->d_network.interface, s)) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_network.interface); + bzobj->d_network.interface = g_strdup(s); + } + continue; + } + if (nm_streq(property_name, "Connected")) { + bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN) + && g_variant_get_boolean(property_value); + + if (bzobj->d_network_connected != v) { + changed = TRUE; + bzobj->d_network_connected = v; + } + continue; + } + } + + for (i = 0; (property_name = invalidated_properties[i]); i++) { + if (nm_streq(property_name, "Interface")) { + if (bzobj->d_network.interface) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_network.interface); + } + continue; + } + if (nm_streq(property_name, "Connected")) { + if (bzobj->d_network_connected) { + changed = TRUE; + bzobj->d_network_connected = FALSE; + } + continue; + } + } + + } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) { + _bzobjs_init(self, &bzobj, object_path); + if (!bzobj->d_has_network_server_iface) { + changed = TRUE; + bzobj->d_has_network_server_iface = TRUE; + } + } + + nm_assert(!changed || bzobj); + + if (inout_bzobj) + *inout_bzobj = bzobj; + + return changed; +} + +static void +_dbus_handle_interface_added(NMBluezManager *self, + const char * object_path, + GVariant * ifaces, + gboolean initial_get_managed_objects) +{ + BzDBusObj * bzobj = NULL; + gboolean changed = FALSE; + const char * interface_name; + GVariant * changed_properties; + GVariantIter iter_ifaces; + + nm_assert(g_variant_is_of_type(ifaces, G_VARIANT_TYPE("a{sa{sv}}"))); + + g_variant_iter_init(&iter_ifaces, ifaces); + while (g_variant_iter_next(&iter_ifaces, "{&s@a{sv}}", &interface_name, &changed_properties)) { + _nm_unused gs_unref_variant GVariant *changed_properties_free = changed_properties; + + if (_dbus_handle_properties_changed(self, + object_path, + interface_name, + changed_properties, + NULL, + &bzobj)) + changed = TRUE; + } + + if (changed) { + _dbus_process_changes(self, + bzobj, + initial_get_managed_objects ? "dbus-init" : "dbus-iface-added"); + } +} + +static gboolean +_dbus_handle_interface_removed(NMBluezManager * self, + const char * object_path, + BzDBusObj ** inout_bzobj, + const char *const *removed_interfaces) +{ + gboolean changed = FALSE; + BzDBusObj *bzobj; + gsize i; + + if (inout_bzobj && *inout_bzobj) { + bzobj = *inout_bzobj; + nm_assert(bzobj == _bzobjs_get(self, object_path)); + } else { + bzobj = _bzobjs_get(self, object_path); + if (!bzobj) + return FALSE; + NM_SET_OUT(inout_bzobj, bzobj); + } + + for (i = 0; removed_interfaces[i]; i++) { + const char *interface_name = removed_interfaces[i]; + + if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) { + if (bzobj->d_has_adapter_iface) { + changed = TRUE; + bzobj->d_has_adapter_iface = FALSE; + } + if (bzobj->d_adapter.address) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_adapter.address); + } + if (bzobj->d_adapter_powered) { + changed = TRUE; + bzobj->d_adapter_powered = FALSE; + } + } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) { + if (bzobj->d_has_device_iface) { + changed = TRUE; + bzobj->d_has_device_iface = FALSE; + } + if (bzobj->d_device.address) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.address); + } + if (bzobj->d_device.name) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.name); + } + if (bzobj->d_device.adapter) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_device.adapter); + } + if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) { + changed = TRUE; + bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE; + } + if (bzobj->d_device_connected) { + changed = TRUE; + bzobj->d_device_connected = FALSE; + } + if (bzobj->d_device_paired) { + changed = TRUE; + bzobj->d_device_paired = FALSE; + } + } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) { + if (bzobj->d_has_network_iface) { + changed = TRUE; + bzobj->d_has_network_iface = FALSE; + } + if (bzobj->d_network.interface) { + changed = TRUE; + nm_clear_g_free(&bzobj->d_network.interface); + } + if (bzobj->d_network_connected) { + changed = TRUE; + bzobj->d_network_connected = FALSE; + } + } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) { + if (bzobj->d_has_network_server_iface) { + changed = TRUE; + bzobj->d_has_network_server_iface = FALSE; + } + } + } + + return changed; +} + +static void +_dbus_managed_objects_changed_cb(GDBusConnection *connection, + const char * sender_name, + const char * arg_object_path, + const char * interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + NMBluezManager * self = user_data; + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + BzDBusObj * bzobj = NULL; + gboolean changed; + + nm_assert(nm_streq0(interface_name, DBUS_INTERFACE_OBJECT_MANAGER)); + + if (priv->get_managed_objects_cancellable) { + /* we still wait for the initial GetManagedObjects(). Ignore the event. */ + return; + } + + if (nm_streq(signal_name, "InterfacesAdded")) { + gs_unref_variant GVariant *interfaces_and_properties = NULL; + const char * object_path; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oa{sa{sv}})"))) + return; + + g_variant_get(parameters, "(&o@a{sa{sv}})", &object_path, &interfaces_and_properties); + + _dbus_handle_interface_added(self, object_path, interfaces_and_properties, FALSE); + return; + } + + if (nm_streq(signal_name, "InterfacesRemoved")) { + gs_free const char **interfaces = NULL; + const char * object_path; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oas)"))) + return; + + g_variant_get(parameters, "(&o^a&s)", &object_path, &interfaces); + + changed = _dbus_handle_interface_removed(self, object_path, &bzobj, interfaces); + if (changed) + _dbus_process_changes(self, bzobj, "dbus-iface-removed"); + return; + } +} + +static void +_dbus_properties_changed_cb(GDBusConnection *connection, + const char * sender_name, + const char * object_path, + const char * signal_interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + NMBluezManager * self = user_data; + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + const char * interface_name; + gs_unref_variant GVariant *changed_properties = NULL; + gs_free const char ** invalidated_properties = NULL; + BzDBusObj * bzobj = NULL; + + if (priv->get_managed_objects_cancellable) { + /* we still wait for the initial GetManagedObjects(). Ignore the event. */ + return; + } + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)"))) + return; + + g_variant_get(parameters, + "(&s@a{sv}^a&s)", + &interface_name, + &changed_properties, + &invalidated_properties); + + if (_dbus_handle_properties_changed(self, + object_path, + interface_name, + changed_properties, + invalidated_properties, + &bzobj)) + _dbus_process_changes(self, bzobj, "dbus-property-changed"); +} + +static void +_dbus_get_managed_objects_cb(GVariant *result, GError *error, gpointer user_data) +{ + NMBluezManager * self; + NMBluezManagerPrivate *priv; + GVariantIter iter; + const char * object_path; + GVariant * ifaces; + + if (!result && nm_utils_error_is_cancelled(error)) + return; + + self = user_data; + priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + g_clear_object(&priv->get_managed_objects_cancellable); + + if (!result) { + _LOGT("initial GetManagedObjects() call failed: %s", error->message); + _cleanup_for_name_owner(self); + return; + } + + _LOGT("initial GetManagedObjects call succeeded"); + + g_variant_iter_init(&iter, result); + while (g_variant_iter_next(&iter, "{&o@a{sa{sv}}}", &object_path, &ifaces)) { + _nm_unused gs_unref_variant GVariant *ifaces_free = ifaces; + + _dbus_handle_interface_added(self, object_path, ifaces, TRUE); + } +} + +/*****************************************************************************/ + +static void +_cleanup_for_name_owner(NMBluezManager *self) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + gboolean emit_device_availability_changed = FALSE; + GHashTableIter iter; + BzDBusObj * bzobj; + gboolean first = TRUE; + + nm_clear_g_cancellable(&priv->get_managed_objects_cancellable); + + nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->managed_objects_changed_id); + nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id); + + nm_clear_g_free(&priv->name_owner); + + g_hash_table_iter_init(&iter, priv->bzobjs); + while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) { + if (first) { + first = FALSE; + _LOGT("drop all objects form D-Bus cache..."); + } + _dbus_handle_interface_removed(self, + bzobj->object_path, + &bzobj, + ALL_RELEVANT_INTERFACE_NAMES); + nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst); + } + _process_change_idle_all(self, &emit_device_availability_changed); + nm_assert(g_hash_table_size(priv->bzobjs) == 0); + + if (emit_device_availability_changed) + nm_manager_notify_device_availability_maybe_changed(priv->manager); +} + +static void +name_owner_changed(NMBluezManager *self, const char *owner) +{ + _nm_unused gs_unref_object NMBluezManager *self_keep_alive = g_object_ref(self); + NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + owner = nm_str_not_empty(owner); + + if (!owner) + _LOGT("D-Bus name for bluez has no owner"); + else + _LOGT("D-Bus name for bluez has owner %s", owner); + + nm_clear_g_cancellable(&priv->name_owner_get_cancellable); + + if (nm_streq0(priv->name_owner, owner)) + return; + + _cleanup_for_name_owner(self); + + if (!owner) + return; + + priv->name_owner = g_strdup(owner); + + priv->get_managed_objects_cancellable = g_cancellable_new(); + + priv->managed_objects_changed_id = + nm_dbus_connection_signal_subscribe_object_manager(priv->dbus_connection, + priv->name_owner, + NM_BLUEZ_MANAGER_PATH, + NULL, + _dbus_managed_objects_changed_cb, + self, + NULL); + + priv->properties_changed_id = + nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection, + priv->name_owner, + NULL, + NULL, + _dbus_properties_changed_cb, + self, + NULL); + + nm_dbus_connection_call_get_managed_objects(priv->dbus_connection, + priv->name_owner, + NM_BLUEZ_MANAGER_PATH, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 20000, + priv->get_managed_objects_cancellable, + _dbus_get_managed_objects_cb, + self); +} + +static void +name_owner_changed_cb(GDBusConnection *connection, + const char * sender_name, + const char * object_path, + const char * interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + NMBluezManager *self = user_data; + const char * new_owner; + + if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) + return; + + g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); + + name_owner_changed(self, new_owner); +} + +static void +name_owner_get_cb(const char *name_owner, GError *error, gpointer user_data) +{ + if (name_owner || !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + name_owner_changed(user_data, name_owner); +} + +/*****************************************************************************/ + +static void +_cleanup_all(NMBluezManager *self) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + priv->settings_registered = FALSE; + + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_added, self); + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_updated, self); + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_removed, self); + + g_hash_table_remove_all(priv->conn_data_elems); + g_hash_table_remove_all(priv->conn_data_heads); + + _cleanup_for_name_owner(self); + + nm_clear_g_cancellable(&priv->name_owner_get_cancellable); + + nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id); +} + +static void +start(NMDeviceFactory *factory) +{ + NMBluezManager * self; + NMBluezManagerPrivate * priv; + NMSettingsConnection *const *sett_conns; + guint n_sett_conns; + guint i; + + g_return_if_fail(NM_IS_BLUEZ_MANAGER(factory)); + + self = NM_BLUEZ_MANAGER(factory); + priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + _cleanup_all(self); + + if (!priv->dbus_connection) { + _LOGI("no D-Bus connection available"); + return; + } + + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_ADDED, + G_CALLBACK(cp_connection_added), + self); + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_UPDATED, + G_CALLBACK(cp_connection_updated), + self); + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_REMOVED, + G_CALLBACK(cp_connection_removed), + self); + + priv->settings_registered = TRUE; + + sett_conns = nm_settings_get_connections(priv->settings, &n_sett_conns); + for (i = 0; i < n_sett_conns; i++) + _conn_track_update(self, sett_conns[i], TRUE, NULL, NULL, NULL); + + priv->name_owner_changed_id = + nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection, + NM_BLUEZ_SERVICE, + name_owner_changed_cb, + self, + NULL); + + priv->name_owner_get_cancellable = g_cancellable_new(); + + nm_dbus_connection_call_get_name_owner(priv->dbus_connection, + NM_BLUEZ_SERVICE, + 10000, + priv->name_owner_get_cancellable, + name_owner_get_cb, + self); +} + +/*****************************************************************************/ + +static void +_connect_returned(NMBluezManager * self, + BzDBusObj * bzobj, + NMBluetoothCapabilities bt_type, + const char * device_name, + NMBluez5DunContext * dun_context, + GError * error) +{ + char sbuf_cap[100]; + + if (error) { + nm_assert(!device_name); + nm_assert(!dun_context); + + _LOGI("%s [%s]: connect failed: %s", + nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type, + sbuf_cap, + sizeof(sbuf_cap)), + bzobj->object_path, + error->message); + + _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data), + self, + NULL, + error); + _connect_disconnect(self, bzobj, "cleanup after connect failure"); + return; + } + + nm_assert(bzobj->x_device_connect_bt_type == bt_type); + nm_assert(device_name); + nm_assert((bt_type == NM_BT_CAPABILITY_DUN) == (!!dun_context)); + nm_assert(bzobj->x_device.c_req_data); + + g_clear_object(&bzobj->x_device.c_req_data->int_cancellable); + + bzobj->x_device.connect_dun_context = dun_context; + + _LOGD("%s [%s]: connect successful to device %s", + nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type, + sbuf_cap, + sizeof(sbuf_cap)), + bzobj->object_path, + device_name); + + /* we already have another over-all timer running. But after we connected the device, + * we still need to wait for bluez to acknowledge the connected state (via D-Bus, for NAP). + * For DUN profiles we likely are already fully connected by now. + * + * Anyway, schedule another timeout that is possibly shorter than the overall, original + * timeout. Now this should go down fast. */ + bzobj->x_device.c_req_data->timeout_wait_connect_id = + g_timeout_add(5000, _connect_timeout_wait_connected_cb, bzobj), + bzobj->x_device.c_req_data->device_name = g_strdup(device_name); + + if (_bzobjs_device_is_usable(bzobj, NULL, NULL) && _bzobjs_device_is_connected(bzobj)) { + /* We are now connected. Schedule the task that completes the state. */ + _process_change_idle_schedule(self, bzobj); + } +} + +#if WITH_BLUEZ5_DUN +static void +_connect_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data) +{ + BzDBusObj *bzobj = user_data; + + _connect_disconnect(bzobj->self, bzobj, "DUN connection hung up"); +} + +static void +_connect_dun_step2_cb(NMBluez5DunContext *context, + const char * rfcomm_dev, + GError * error, + gpointer user_data) +{ + BzDBusObj *bzobj; + + if (nm_utils_error_is_cancelled(error)) + return; + + bzobj = user_data; + + if (rfcomm_dev) { + /* We want to early notify about the rfcomm path. That is because we might still delay + * to signal full activation longer (asynchronously). But the earliest time the callback + * is invoked with the rfcomm path, we just created the device synchronously. + * + * By already notifying the caller about the path early, it avoids a race where ModemManager + * would find the modem before the bluetooth code considers the profile fully activated. */ + + nm_assert(!error); + nm_assert(bzobj->x_device.c_req_data); + + if (!g_cancellable_is_cancelled(bzobj->x_device.c_req_data->ext_cancellable)) + bzobj->x_device.c_req_data->callback(bzobj->self, + FALSE, + rfcomm_dev, + NULL, + bzobj->x_device.c_req_data->callback_user_data); + + if (!context) { + /* No context set. This means, we just got notified about the rfcomm path and need to wait + * longer, for the next callback. */ + return; + } + } + + _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, rfcomm_dev, context, error); +} + +static void +_connect_dun_step1_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + gs_unref_variant GVariant *ret = NULL; + gs_free_error GError *error = NULL; + BzDBusObj * bzobj_adapter; + BzDBusObj * bzobj; + + ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); + + if (!ret && nm_utils_error_is_cancelled(error)) + return; + + bzobj = user_data; + + if (error) { + _LOGT("DUN: [%s]: bluetooth device connect failed: %s", bzobj->object_path, error->message); + /* we actually ignore this error. Let's try, maybe we still can connect via DUN. */ + g_clear_error(&error); + } else + _LOGT("DUN: [%s]: bluetooth device connected successfully", bzobj->object_path); + + if (!_bzobjs_device_is_usable(bzobj, &bzobj_adapter, NULL)) { + nm_utils_error_set(&error, + NM_UTILS_ERROR_UNKNOWN, + "device %s is not usable for DUN after connect", + bzobj->object_path); + _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error); + return; + } + + if (!nm_bluez5_dun_connect(bzobj_adapter->d_adapter.address, + bzobj->d_device.address, + bzobj->x_device.c_req_data->int_cancellable, + _connect_dun_step2_cb, + bzobj, + _connect_dun_notify_tty_hangup_cb, + bzobj, + &error)) { + _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error); + return; + } +} +#endif + +static void +_connect_nap_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + gs_unref_variant GVariant *ret = NULL; + const char * network_iface_name = NULL; + gs_free_error GError *error = NULL; + BzDBusObj * bzobj; + + ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); + + if (!ret && nm_utils_error_is_cancelled(error)) + return; + + if (ret) + g_variant_get(ret, "(&s)", &network_iface_name); + + bzobj = user_data; + + _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_NAP, network_iface_name, NULL, error); +} + +static void +_connect_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj) +{ + _connect_disconnect(bzobj->self, bzobj, "connect cancelled"); +} + +static gboolean +_connect_timeout_wait_connected_cb(gpointer user_data) +{ + BzDBusObj *bzobj = user_data; + + bzobj->x_device.c_req_data->timeout_wait_connect_id = 0; + _connect_disconnect(bzobj->self, bzobj, "timeout waiting for connected"); + return G_SOURCE_REMOVE; +} + +static gboolean +_connect_timeout_cb(gpointer user_data) +{ + BzDBusObj *bzobj = user_data; + + bzobj->x_device.c_req_data->timeout_id = 0; + _connect_disconnect(bzobj->self, bzobj, "timeout connecting"); + return G_SOURCE_REMOVE; +} + +static void +_connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + DeviceConnectReqData * c_req_data; + char sbuf_cap[100]; + gboolean bt_type; + + if (bzobj->x_device_connect_bt_type == NM_BT_CAPABILITY_NONE) { + nm_assert(!bzobj->x_device.c_req_data); + return; + } + + bt_type = bzobj->x_device_connect_bt_type; + nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP)); + bzobj->x_device_connect_bt_type = NM_BT_CAPABILITY_NONE; + + c_req_data = g_steal_pointer(&bzobj->x_device.c_req_data); + + _LOGD("%s [%s]: disconnect due to %s", + nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)), + bzobj->object_path, + reason); + + if (c_req_data) + nm_clear_g_cancellable(&c_req_data->int_cancellable); + + if (bt_type == NM_BT_CAPABILITY_DUN) { + /* For DUN devices, we also called org.bluez.Device1.Connect() (because in order + * for nm_bluez5_dun_connect() to succeed, we need to be already connected *why??). + * + * But upon disconnect we don't call Disconnect() because we don't know whether somebody + * else also uses the bluetooth device for other purposes. During disconnect we only + * terminate the DUN connection, but don't disconnect entirely. I think that's the + * best we can do. */ +#if WITH_BLUEZ5_DUN + nm_clear_pointer(&bzobj->x_device.connect_dun_context, nm_bluez5_dun_disconnect); +#else + nm_assert_not_reached(); +#endif + } else { + if (priv->name_owner) { + gs_unref_object GCancellable *cancellable = NULL; + + cancellable = g_cancellable_new(); + + nm_shutdown_wait_obj_register_cancellable_full( + cancellable, + g_strdup_printf("bt-disconnect-nap[%s]", bzobj->object_path), + TRUE); + + g_dbus_connection_call(priv->dbus_connection, + priv->name_owner, + bzobj->object_path, + NM_BLUEZ5_NETWORK_INTERFACE, + "Disconnect", + g_variant_new("()"), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + cancellable, + _dbus_call_complete_cb_nop, + NULL); + } + } + + if (c_req_data) { + gs_free_error GError *error = NULL; + + nm_utils_error_set(&error, NM_UTILS_ERROR_UNKNOWN, "connect aborted due to %s", reason); + _device_connect_req_data_complete(c_req_data, self, NULL, error); + } +} + +gboolean +nm_bluez_manager_connect(NMBluezManager * self, + const char * object_path, + NMBluetoothCapabilities connection_bt_type, + int timeout_msec, + GCancellable * cancellable, + NMBluezManagerConnectCb callback, + gpointer callback_user_data, + GError ** error) +{ + gs_unref_object GCancellable *int_cancellable = NULL; + DeviceConnectReqData * c_req_data; + NMBluezManagerPrivate * priv; + BzDBusObj * bzobj; + char sbuf_cap[100]; + + g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), FALSE); + g_return_val_if_fail(NM_IN_SET(connection_bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP), + FALSE); + g_return_val_if_fail(callback, FALSE); + + nm_assert(timeout_msec > 0); + + priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + bzobj = _bzobjs_get(self, object_path); + + if (!bzobj) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s does not exist", object_path); + return FALSE; + } + + if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s is not usable", object_path); + return FALSE; + } + + if (!NM_FLAGS_ALL(bzobj->d_device_capabilities, connection_bt_type)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "device %s has not the required capabilities", + object_path); + return FALSE; + } + +#if !WITH_BLUEZ5_DUN + if (connection_bt_type == NM_BT_CAPABILITY_DUN) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "DUN is not supported"); + return FALSE; + } +#endif + + _connect_disconnect(self, bzobj, "new activation"); + + _LOGD("%s [%s]: connecting...", + nm_bluetooth_capability_to_string(connection_bt_type, sbuf_cap, sizeof(sbuf_cap)), + bzobj->object_path); + + int_cancellable = g_cancellable_new(); + +#if WITH_BLUEZ5_DUN + if (connection_bt_type == NM_BT_CAPABILITY_DUN) { + g_dbus_connection_call(priv->dbus_connection, + priv->name_owner, + bzobj->object_path, + NM_BLUEZ5_DEVICE_INTERFACE, + "Connect", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + timeout_msec, + int_cancellable, + _connect_dun_step1_cb, + bzobj); + } else +#endif + { + nm_assert(connection_bt_type == NM_BT_CAPABILITY_NAP); + g_dbus_connection_call(priv->dbus_connection, + priv->name_owner, + bzobj->object_path, + NM_BLUEZ5_NETWORK_INTERFACE, + "Connect", + g_variant_new("(s)", BLUETOOTH_CONNECT_NAP), + G_VARIANT_TYPE("(s)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + timeout_msec, + int_cancellable, + _connect_nap_cb, + bzobj); + } + + c_req_data = g_slice_new(DeviceConnectReqData); + *c_req_data = (DeviceConnectReqData){ + .int_cancellable = g_steal_pointer(&int_cancellable), + .ext_cancellable = g_object_ref(cancellable), + .callback = callback, + .callback_user_data = callback_user_data, + .ext_cancelled_id = + g_signal_connect(cancellable, "cancelled", G_CALLBACK(_connect_cancelled_cb), bzobj), + .timeout_id = g_timeout_add(timeout_msec, _connect_timeout_cb, bzobj), + }; + + bzobj->x_device_connect_bt_type = connection_bt_type; + bzobj->x_device.c_req_data = c_req_data; + + return TRUE; +} + +void +nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path) +{ + BzDBusObj *bzobj; + + g_return_if_fail(NM_IS_BLUEZ_MANAGER(self)); + g_return_if_fail(object_path); + + bzobj = _bzobjs_get(self, object_path); + if (!bzobj) + return; + + _connect_disconnect(self, bzobj, "disconnected by user"); +} + +/*****************************************************************************/ + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + *out_ignore = TRUE; + g_return_val_if_fail(plink->type == NM_LINK_TYPE_BNEP, NULL); + return NULL; +} + +static gboolean +match_connection(NMDeviceFactory *factory, NMConnection *connection) +{ + const char *type = nm_connection_get_connection_type(connection); + + nm_assert(nm_streq(type, NM_SETTING_BLUETOOTH_SETTING_NAME)); + + if (_nm_connection_get_setting_bluetooth_for_nap(connection)) + return FALSE; /* handled by the bridge factory */ + + return TRUE; +} + +/*****************************************************************************/ + +static void +nm_bluez_manager_init(NMBluezManager *self) +{ + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + priv->vtable_network_server = (NMBtVTableNetworkServer){ + .is_available = _network_server_vt_is_available, + .register_bridge = _network_server_vt_register_bridge, + .unregister_bridge = _network_server_vt_unregister_bridge, + }; + + c_list_init(&priv->network_server_lst_head); + c_list_init(&priv->process_change_lst_head); + + priv->conn_data_heads = + g_hash_table_new_full(_conn_data_head_hash, _conn_data_head_equal, g_free, NULL); + priv->conn_data_elems = g_hash_table_new_full(nm_pdirect_hash, + nm_pdirect_equal, + nm_g_slice_free_fcn(ConnDataElem), + NULL); + + priv->bzobjs = g_hash_table_new_full(nm_pstr_hash, + nm_pstr_equal, + (GDestroyNotify) _bz_dbus_obj_free, + NULL); + + priv->manager = g_object_ref(NM_MANAGER_GET); + priv->settings = g_object_ref(NM_SETTINGS_GET); + priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET); + + g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server, + NULL, + &priv->vtable_network_server); +} + +static void +dispose(GObject *object) +{ + NMBluezManager * self = NM_BLUEZ_MANAGER(object); + NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self); + + /* FIXME(shutdown): we need a nm_device_factory_stop() hook to first unregister all + * BzDBusObj instances and do necessary cleanup actions (like disconnecting devices + * or deleting panu_connection). */ + + nm_assert(c_list_is_empty(&priv->network_server_lst_head)); + nm_assert(c_list_is_empty(&priv->process_change_lst_head)); + nm_assert(priv->process_change_idle_id == 0); + + g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server, + &priv->vtable_network_server, + NULL); + + _cleanup_all(self); + + G_OBJECT_CLASS(nm_bluez_manager_parent_class)->dispose(object); + + g_clear_object(&priv->settings); + g_clear_object(&priv->manager); + g_clear_object(&priv->dbus_connection); + + nm_clear_pointer(&priv->bzobjs, g_hash_table_destroy); +} + +static void +nm_bluez_manager_class_init(NMBluezManagerClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + object_class->dispose = dispose; + + factory_class->get_supported_types = get_supported_types; + factory_class->create_device = create_device; + factory_class->match_connection = match_connection; + factory_class->start = start; +} diff --git a/src/core/devices/bluetooth/nm-bluez-manager.h b/src/core/devices/bluetooth/nm-bluez-manager.h new file mode 100644 index 00000000..04bfea7d --- /dev/null +++ b/src/core/devices/bluetooth/nm-bluez-manager.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2009 - 2019 Red Hat, Inc. + */ + +#ifndef __NM_BLUEZ_MANAGER_H__ +#define __NM_BLUEZ_MANAGER_H__ + +#define NM_TYPE_BLUEZ_MANAGER (nm_bluez_manager_get_type()) +#define NM_BLUEZ_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager)) +#define NM_BLUEZ_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass)) +#define NM_IS_BLUEZ_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_BLUEZ_MANAGER)) +#define NM_IS_BLUEZ_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_BLUEZ_MANAGER)) +#define NM_BLUEZ_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass)) + +typedef struct _NMBluezManager NMBluezManager; +typedef struct _NMBluezManagerClass NMBluezManagerClass; + +GType nm_bluez_manager_get_type(void); + +typedef void (*NMBluezManagerConnectCb)( + NMBluezManager *self, + gboolean is_completed /* or else is early notification with DUN path */, + const char * device_name, + GError * error, + gpointer user_data); + +gboolean nm_bluez_manager_connect(NMBluezManager * self, + const char * object_path, + NMBluetoothCapabilities connection_bt_type, + int timeout_msec, + GCancellable * cancellable, + NMBluezManagerConnectCb callback, + gpointer callback_user_data, + GError ** error); + +void nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path); + +#endif /* __NM_BLUEZ_MANAGER_H__ */ diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.c b/src/core/devices/bluetooth/nm-bluez5-dun.c new file mode 100644 index 00000000..e29884d8 --- /dev/null +++ b/src/core/devices/bluetooth/nm-bluez5-dun.c @@ -0,0 +1,857 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <sys/socket.h> +#include <bluetooth/sdp.h> +#include <bluetooth/sdp_lib.h> +#include <bluetooth/rfcomm.h> +#include <net/ethernet.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <fcntl.h> + +#include "nm-bluez5-dun.h" +#include "nm-bt-error.h" +#include "NetworkManagerUtils.h" + +#define RFCOMM_FMT "/dev/rfcomm%d" + +/*****************************************************************************/ + +typedef struct { + GCancellable * cancellable; + NMBluez5DunConnectCb callback; + gpointer callback_user_data; + + sdp_session_t *sdp_session; + + GError *rfcomm_sdp_search_error; + + GSource *source; + + gint64 connect_open_tty_started_at; + + gulong cancelled_id; + + guint8 sdp_session_try_count; +} ConnectData; + +struct _NMBluez5DunContext { + const char *dst_str; + + ConnectData *cdat; + + NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb; + gpointer notify_tty_hangup_user_data; + + char *rfcomm_tty_path; + + GSource *rfcomm_tty_poll_source; + + int rfcomm_sock_fd; + int rfcomm_tty_fd; + int rfcomm_tty_no; + int rfcomm_channel; + + bdaddr_t src; + bdaddr_t dst; + + char src_str[]; +}; + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_BT +#define _NMLOG_PREFIX_NAME "bluez" +#define _NMLOG(level, context, ...) \ + G_STMT_START \ + { \ + if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) { \ + const NMBluez5DunContext *const _context = (context); \ + \ + _nm_log((level), \ + (_NMLOG_DOMAIN), \ + 0, \ + NULL, \ + NULL, \ + "%s: DUN[%s] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + _context->src_str _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void _context_invoke_callback_success(NMBluez5DunContext *context); +static void _context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error); +static void _context_free(NMBluez5DunContext *context); +static int _connect_open_tty(NMBluez5DunContext *context); +static gboolean _connect_sdp_session_start(NMBluez5DunContext *context, GError **error); + +/*****************************************************************************/ + +NM_AUTO_DEFINE_FCN0(NMBluez5DunContext *, _nm_auto_free_context, _context_free); +#define nm_auto_free_context nm_auto(_nm_auto_free_context) + +/*****************************************************************************/ + +const char * +nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context) +{ + return context->src_str; +} + +const char * +nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context) +{ + return context->dst_str; +} + +const char * +nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context) +{ + return context->rfcomm_tty_path; +} + +/*****************************************************************************/ + +static gboolean +_rfcomm_tty_poll_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + + _LOGD(context, + "receive %s%s%s signal on rfcomm file descriptor", + NM_FLAGS_HAS(condition, G_IO_ERR) ? "ERR" : "", + NM_FLAGS_ALL(condition, G_IO_HUP | G_IO_ERR) ? "," : "", + NM_FLAGS_HAS(condition, G_IO_HUP) ? "HUP" : ""); + + nm_clear_g_source_inst(&context->rfcomm_tty_poll_source); + context->notify_tty_hangup_cb(context, context->notify_tty_hangup_user_data); + return G_SOURCE_REMOVE; +} + +static gboolean +_connect_open_tty_retry_cb(gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + int r; + + r = _connect_open_tty(context); + if (r >= 0) + return G_SOURCE_REMOVE; + + if (nm_utils_get_monotonic_timestamp_nsec() + > context->cdat->connect_open_tty_started_at + (30 * 100 * NM_UTILS_NSEC_PER_MSEC)) { + gs_free_error GError *error = NULL; + + nm_clear_g_source_inst(&context->cdat->source); + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "give up waiting to open %s device: %s (%d)", + context->rfcomm_tty_path, + nm_strerror_native(r), + -r); + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static int +_connect_open_tty(NMBluez5DunContext *context) +{ + int fd; + int errsv; + + fd = open(context->rfcomm_tty_path, O_RDONLY | O_NOCTTY | O_CLOEXEC); + if (fd < 0) { + errsv = NM_ERRNO_NATIVE(errno); + + if (!context->cdat->source) { + _LOGD(context, + "failed opening tty " RFCOMM_FMT ": %s (%d). Start polling...", + context->rfcomm_tty_no, + nm_strerror_native(errsv), + errsv); + context->cdat->connect_open_tty_started_at = nm_utils_get_monotonic_timestamp_nsec(); + context->cdat->source = nm_g_timeout_source_new(100, + G_PRIORITY_DEFAULT, + _connect_open_tty_retry_cb, + context, + NULL); + g_source_attach(context->cdat->source, NULL); + } + return -errsv; + } + + context->rfcomm_tty_fd = fd; + + context->rfcomm_tty_poll_source = nm_g_unix_fd_source_new(context->rfcomm_tty_fd, + G_IO_ERR | G_IO_HUP, + G_PRIORITY_DEFAULT, + _rfcomm_tty_poll_cb, + context, + NULL); + g_source_attach(context->rfcomm_tty_poll_source, NULL); + + _context_invoke_callback_success(context); + return 0; +} + +static void +_connect_create_rfcomm(NMBluez5DunContext *context) +{ + gs_free_error GError *error = NULL; + struct rfcomm_dev_req req; + int devid; + int errsv; + int r; + + _LOGD(context, "connected to %s on channel %d", context->dst_str, context->rfcomm_channel); + + /* Create an RFCOMM kernel device for the DUN channel */ + memset(&req, 0, sizeof(req)); + req.dev_id = -1; + req.flags = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP); + req.channel = context->rfcomm_channel; + memcpy(&req.src, &context->src, ETH_ALEN); + memcpy(&req.dst, &context->dst, ETH_ALEN); + devid = ioctl(context->rfcomm_sock_fd, RFCOMMCREATEDEV, &req); + if (devid < 0) { + errsv = NM_ERRNO_NATIVE(errno); + if (errsv == EBADFD) { + /* hm. We use a non-blocking socket to connect. Above getsockopt(SOL_SOCKET,SO_ERROR) indicated + * success, but still now we fail with EBADFD. I think that is a bug and we should get the + * failure during connect(). + * + * Anyway, craft a less confusing error message than + * "failed to create rfcomm device: File descriptor in bad state (77)". */ + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "unknown failure to connect to DUN device"); + } else { + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to create rfcomm device: %s (%d)", + nm_strerror_native(errsv), + errsv); + } + _context_invoke_callback_fail_and_free(context, error); + return; + } + + context->rfcomm_tty_no = devid; + context->rfcomm_tty_path = g_strdup_printf(RFCOMM_FMT, devid); + + r = _connect_open_tty(context); + if (r < 0) { + /* we created the rfcomm device, but cannot yet open it. That means, we are + * not yet fully connected. However, we notify the caller about "what we learned + * so far". Note that this happens synchronously. + * + * The purpose is that once we proceed synchronously, modem-manager races with + * the detection of the modem. We want to notify the caller first about the + * device name. */ + context->cdat->callback(NULL, + context->rfcomm_tty_path, + NULL, + context->cdat->callback_user_data); + } +} + +static gboolean +_connect_socket_connect_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + gs_free_error GError *error = NULL; + int errsv = 0; + socklen_t slen = sizeof(errsv); + int r; + + nm_clear_g_source_inst(&context->cdat->source); + + r = getsockopt(context->rfcomm_sock_fd, SOL_SOCKET, SO_ERROR, &errsv, &slen); + + if (r < 0) { + errsv = errno; + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to complete connecting RFCOMM socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; + } + + if (errsv != 0) { + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to connect RFCOMM socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; + } + + _connect_create_rfcomm(context); + return G_SOURCE_REMOVE; +} + +static void +_connect_socket_connect(NMBluez5DunContext *context) +{ + gs_free_error GError *error = NULL; + struct sockaddr_rc sa; + int errsv; + + context->rfcomm_sock_fd = + socket(AF_BLUETOOTH, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, BTPROTO_RFCOMM); + if (context->rfcomm_sock_fd < 0) { + errsv = errno; + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to create RFCOMM socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return; + } + + /* Connect to the remote device */ + memset(&sa, 0, sizeof(sa)); + sa.rc_family = AF_BLUETOOTH; + sa.rc_channel = 0; + memcpy(&sa.rc_bdaddr, &context->src, ETH_ALEN); + if (bind(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { + errsv = errno; + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to bind socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return; + } + + memset(&sa, 0, sizeof(sa)); + sa.rc_family = AF_BLUETOOTH; + sa.rc_channel = context->rfcomm_channel; + memcpy(&sa.rc_bdaddr, &context->dst, ETH_ALEN); + if (connect(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { + errsv = errno; + if (errsv != EINPROGRESS) { + g_set_error(&error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to connect to remote device: %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return; + } + + _LOGD(context, + "connecting to %s on channel %d...", + context->dst_str, + context->rfcomm_channel); + + context->cdat->source = nm_g_unix_fd_source_new(context->rfcomm_sock_fd, + G_IO_OUT, + G_PRIORITY_DEFAULT, + _connect_socket_connect_cb, + context, + NULL); + g_source_attach(context->cdat->source, NULL); + return; + } + + _connect_create_rfcomm(context); +} + +static void +_connect_sdp_search_cb(uint8_t type, uint16_t status, uint8_t *rsp, size_t size, void *user_data) +{ + NMBluez5DunContext *context = user_data; + int scanned; + int seqlen = 0; + int bytesleft = size; + uint8_t dataType; + int channel = -1; + + if (context->cdat->rfcomm_sdp_search_error || context->rfcomm_channel >= 0) + return; + + _LOGD(context, "SDP search finished with type=%d status=%d", status, type); + + /* SDP response received */ + if (status || type != SDP_SVC_SEARCH_ATTR_RSP) { + g_set_error(&context->cdat->rfcomm_sdp_search_error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "did not get a Service Discovery response"); + return; + } + + scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen); + + _LOGD(context, "SDP sequence type scanned=%d length=%d", scanned, seqlen); + + scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen); + if (!scanned || !seqlen) { + /* Short read or unknown sequence type */ + g_set_error(&context->cdat->rfcomm_sdp_search_error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "improper Service Discovery response"); + return; + } + + rsp += scanned; + bytesleft -= scanned; + do { + sdp_record_t *rec; + int recsize = 0; + sdp_list_t * protos; + + rec = sdp_extract_pdu(rsp, bytesleft, &recsize); + if (!rec) + break; + + if (!recsize) { + sdp_record_free(rec); + break; + } + + if (sdp_get_access_protos(rec, &protos) == 0) { + /* Extract the DUN channel number */ + channel = sdp_get_proto_port(protos, RFCOMM_UUID); + sdp_list_free(protos, NULL); + + _LOGD(context, "SDP channel=%d", channel); + } + sdp_record_free(rec); + + scanned += recsize; + rsp += recsize; + bytesleft -= recsize; + } while (scanned < (ssize_t) size && bytesleft > 0 && channel < 0); + + if (channel == -1) { + g_set_error(&context->cdat->rfcomm_sdp_search_error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "did not receive rfcomm-channel"); + return; + } + + context->rfcomm_channel = channel; +} + +static gboolean +_connect_sdp_search_io_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + gs_free_error GError *error = NULL; + int errsv; + + if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) { + _LOGD(context, "SDP search returned with invalid IO condition 0x%x", (guint) condition); + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "Service Discovery interrupted"); + nm_clear_g_source_inst(&context->cdat->source); + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; + } + + if (sdp_process(context->cdat->sdp_session) == 0) { + _LOGD(context, "SDP search still not finished"); + return G_SOURCE_CONTINUE; + } + + nm_clear_g_source_inst(&context->cdat->source); + + if (context->rfcomm_channel < 0 && !context->cdat->rfcomm_sdp_search_error) { + errsv = sdp_get_error(context->cdat->sdp_session); + _LOGD(context, "SDP search failed: %s (%d)", nm_strerror_native(errsv), errsv); + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "Service Discovery failed with %s (%d)", + nm_strerror_native(errsv), + errsv); + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; + } + + if (context->cdat->rfcomm_sdp_search_error) { + _LOGD(context, + "SDP search failed to complete: %s", + context->cdat->rfcomm_sdp_search_error->message); + _context_invoke_callback_fail_and_free(context, context->cdat->rfcomm_sdp_search_error); + return G_SOURCE_REMOVE; + } + + nm_clear_pointer(&context->cdat->sdp_session, sdp_close); + + _connect_socket_connect(context); + + return G_SOURCE_REMOVE; +} + +static gboolean +_connect_sdp_session_start_on_idle_cb(gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + gs_free_error GError *error = NULL; + + nm_clear_g_source_inst(&context->cdat->source); + + _LOGD(context, "retry starting sdp-session..."); + + if (!_connect_sdp_session_start(context, &error)) + _context_invoke_callback_fail_and_free(context, error); + + return G_SOURCE_REMOVE; +} + +static gboolean +_connect_sdp_io_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMBluez5DunContext *context = user_data; + sdp_list_t * search; + sdp_list_t * attrs; + uuid_t svclass; + uint16_t attr; + int errsv; + int fd_err = 0; + int r; + socklen_t len = sizeof(fd_err); + gs_free_error GError *error = NULL; + + nm_clear_g_source_inst(&context->cdat->source); + + _LOGD(context, "sdp-session ready to connect with fd=%d", fd); + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &fd_err, &len) < 0) { + errsv = NM_ERRNO_NATIVE(errno); + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "error for getsockopt on Service Discovery socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + goto done; + } + + if (fd_err != 0) { + errsv = nm_errno_native(fd_err); + + if (NM_IN_SET(errsv, ECONNREFUSED, EHOSTDOWN) + && --context->cdat->sdp_session_try_count > 0) { + /* *sigh* */ + _LOGD(context, + "sdp-session failed with %s (%d). Retry in a bit", + nm_strerror_native(errsv), + errsv); + nm_clear_g_source_inst(&context->cdat->source); + context->cdat->source = nm_g_timeout_source_new(1000, + G_PRIORITY_DEFAULT, + _connect_sdp_session_start_on_idle_cb, + context, + NULL); + g_source_attach(context->cdat->source, NULL); + return G_SOURCE_REMOVE; + } + + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "error on Service Discovery socket: %s (%d)", + nm_strerror_native(errsv), + errsv); + goto done; + } + + if (sdp_set_notify(context->cdat->sdp_session, _connect_sdp_search_cb, context) < 0) { + /* Should not be reached, only can fail if we passed bad sdp_session. */ + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "could not set Service Discovery notification"); + goto done; + } + + sdp_uuid16_create(&svclass, DIALUP_NET_SVCLASS_ID); + search = sdp_list_append(NULL, &svclass); + attr = SDP_ATTR_PROTO_DESC_LIST; + attrs = sdp_list_append(NULL, &attr); + + r = sdp_service_search_attr_async(context->cdat->sdp_session, + search, + SDP_ATTR_REQ_INDIVIDUAL, + attrs); + + sdp_list_free(attrs, NULL); + sdp_list_free(search, NULL); + + if (r < 0) { + errsv = nm_errno_native(sdp_get_error(context->cdat->sdp_session)); + error = g_error_new(NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "error starting Service Discovery: %s (%d)", + nm_strerror_native(errsv), + errsv); + goto done; + } + + /* Set callback responsible for update the internal SDP transaction */ + context->cdat->source = nm_g_unix_fd_source_new(fd, + G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + G_PRIORITY_DEFAULT, + _connect_sdp_search_io_cb, + context, + NULL); + g_source_attach(context->cdat->source, NULL); + +done: + if (error) + _context_invoke_callback_fail_and_free(context, error); + return G_SOURCE_REMOVE; +} + +/*****************************************************************************/ + +static void +_connect_cancelled_cb(GCancellable *cancellable, NMBluez5DunContext *context) +{ + gs_free_error GError *error = NULL; + + if (!g_cancellable_set_error_if_cancelled(cancellable, &error)) + g_return_if_reached(); + + _context_invoke_callback_fail_and_free(context, error); +} + +static gboolean +_connect_sdp_session_start(NMBluez5DunContext *context, GError **error) +{ + nm_assert(context->cdat); + + nm_clear_g_source_inst(&context->cdat->source); + nm_clear_pointer(&context->cdat->sdp_session, sdp_close); + + context->cdat->sdp_session = sdp_connect(&context->src, &context->dst, SDP_NON_BLOCKING); + if (!context->cdat->sdp_session) { + int errsv = nm_errno_native(errno); + + g_set_error(error, + NM_BT_ERROR, + NM_BT_ERROR_DUN_CONNECT_FAILED, + "failed to connect to the SDP server: %s (%d)", + nm_strerror_native(errsv), + errsv); + return FALSE; + } + + context->cdat->source = nm_g_unix_fd_source_new(sdp_get_socket(context->cdat->sdp_session), + G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + G_PRIORITY_DEFAULT, + _connect_sdp_io_cb, + context, + NULL); + g_source_attach(context->cdat->source, NULL); + return TRUE; +} + +/*****************************************************************************/ + +gboolean +nm_bluez5_dun_connect(const char * adapter, + const char * remote, + GCancellable * cancellable, + NMBluez5DunConnectCb callback, + gpointer callback_user_data, + NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb, + gpointer notify_tty_hangup_user_data, + GError ** error) +{ + nm_auto_free_context NMBluez5DunContext *context = NULL; + ConnectData * cdat; + gsize src_l; + gsize dst_l; + + g_return_val_if_fail(adapter, FALSE); + g_return_val_if_fail(remote, FALSE); + g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE); + g_return_val_if_fail(callback, FALSE); + g_return_val_if_fail(notify_tty_hangup_cb, FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + nm_assert(!g_cancellable_is_cancelled(cancellable)); + + src_l = strlen(adapter) + 1; + dst_l = strlen(remote) + 1; + + cdat = g_slice_new(ConnectData); + *cdat = (ConnectData){ + .callback = callback, + .callback_user_data = callback_user_data, + .cancellable = g_object_ref(cancellable), + .sdp_session_try_count = 5, + }; + + context = g_malloc(sizeof(NMBluez5DunContext) + src_l + dst_l); + *context = (NMBluez5DunContext){ + .cdat = cdat, + .notify_tty_hangup_cb = notify_tty_hangup_cb, + .notify_tty_hangup_user_data = notify_tty_hangup_user_data, + .rfcomm_tty_no = -1, + .rfcomm_sock_fd = -1, + .rfcomm_tty_fd = -1, + .rfcomm_channel = -1, + }; + memcpy(&context->src_str[0], adapter, src_l); + context->dst_str = &context->src_str[src_l]; + memcpy((char *) context->dst_str, remote, dst_l); + + if (str2ba(adapter, &context->src) < 0) { + g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid source"); + return FALSE; + } + + if (str2ba(remote, &context->dst) < 0) { + g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid remote"); + return FALSE; + } + + context->cdat->cancelled_id = g_signal_connect(context->cdat->cancellable, + "cancelled", + G_CALLBACK(_connect_cancelled_cb), + context); + + if (!_connect_sdp_session_start(context, error)) + return FALSE; + + _LOGD(context, "starting channel number discovery for device %s", context->dst_str); + + g_steal_pointer(&context); + return TRUE; +} + +/*****************************************************************************/ + +void +nm_bluez5_dun_disconnect(NMBluez5DunContext *context) +{ + nm_assert(context); + nm_assert(!context->cdat); + + _LOGD(context, "disconnecting DUN connection"); + + _context_free(context); +} + +/*****************************************************************************/ + +static void +_context_cleanup_connect_data(NMBluez5DunContext *context) +{ + ConnectData *cdat; + + cdat = g_steal_pointer(&context->cdat); + if (!cdat) + return; + + nm_clear_g_signal_handler(cdat->cancellable, &cdat->cancelled_id); + + nm_clear_g_source_inst(&cdat->source); + + nm_clear_pointer(&cdat->sdp_session, sdp_close); + + g_clear_object(&cdat->cancellable); + + g_clear_error(&cdat->rfcomm_sdp_search_error); + + nm_g_slice_free(cdat); +} + +static void +_context_invoke_callback(NMBluez5DunContext *context, GError *error) +{ + NMBluez5DunConnectCb callback; + gpointer callback_user_data; + + nm_assert(context); + nm_assert(context->cdat); + nm_assert(context->cdat->callback); + nm_assert(error || context->rfcomm_tty_path); + + if (!error) + _LOGD(context, "connected via \"%s\"", context->rfcomm_tty_path); + else if (nm_utils_error_is_cancelled(error)) + _LOGD(context, "cancelled"); + else + _LOGD(context, "failed to connect: %s", error->message); + + callback = context->cdat->callback; + callback_user_data = context->cdat->callback_user_data; + + _context_cleanup_connect_data(context); + + callback(error ? NULL : context, + error ? NULL : context->rfcomm_tty_path, + error, + callback_user_data); +} + +static void +_context_invoke_callback_success(NMBluez5DunContext *context) +{ + nm_assert(context->rfcomm_tty_path); + _context_invoke_callback(context, NULL); +} + +static void +_context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error) +{ + nm_assert(error); + _context_invoke_callback(context, error); + _context_free(context); +} + +static void +_context_free(NMBluez5DunContext *context) +{ + nm_assert(context); + + _context_cleanup_connect_data(context); + + nm_clear_g_source_inst(&context->rfcomm_tty_poll_source); + + if (context->rfcomm_sock_fd >= 0) { + if (context->rfcomm_tty_no >= 0) { + struct rfcomm_dev_req req; + + memset(&req, 0, sizeof(struct rfcomm_dev_req)); + req.dev_id = context->rfcomm_tty_no; + context->rfcomm_tty_no = -1; + (void) ioctl(context->rfcomm_sock_fd, RFCOMMRELEASEDEV, &req); + } + nm_close(nm_steal_fd(&context->rfcomm_sock_fd)); + } + + if (context->rfcomm_tty_fd >= 0) + nm_close(nm_steal_fd(&context->rfcomm_tty_fd)); + nm_clear_g_free(&context->rfcomm_tty_path); + g_free(context); +} diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.h b/src/core/devices/bluetooth/nm-bluez5-dun.h new file mode 100644 index 00000000..020d4119 --- /dev/null +++ b/src/core/devices/bluetooth/nm-bluez5-dun.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef __NM_BLUEZ5_DUN_H__ +#define __NM_BLUEZ5_DUN_H__ + +typedef struct _NMBluez5DunContext NMBluez5DunContext; + +#if WITH_BLUEZ5_DUN + +typedef void (*NMBluez5DunConnectCb)(NMBluez5DunContext *context, + const char * rfcomm_dev, + GError * error, + gpointer user_data); + +typedef void (*NMBluez5DunNotifyTtyHangupCb)(NMBluez5DunContext *context, gpointer user_data); + +gboolean nm_bluez5_dun_connect(const char * adapter, + const char * remote, + GCancellable * cancellable, + NMBluez5DunConnectCb callback, + gpointer callback_user_data, + NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb, + gpointer notify_tty_hangup_user_data, + GError ** error); + +void nm_bluez5_dun_disconnect(NMBluez5DunContext *context); + +const char *nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context); +const char *nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context); +const char *nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context); + +#endif /* WITH_BLUEZ5_DUN */ + +#endif /* __NM_BLUEZ5_DUN_H__ */ diff --git a/src/core/devices/bluetooth/nm-bt-error.c b/src/core/devices/bluetooth/nm-bt-error.c new file mode 100644 index 00000000..2e49eb7a --- /dev/null +++ b/src/core/devices/bluetooth/nm-bt-error.c @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-bt-error.h" + +NM_CACHED_QUARK_FCN("nm-bt-error", nm_bt_error_quark); diff --git a/src/core/devices/bluetooth/nm-bt-error.h b/src/core/devices/bluetooth/nm-bt-error.h new file mode 100644 index 00000000..432ae5b4 --- /dev/null +++ b/src/core/devices/bluetooth/nm-bt-error.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef _NM_BLUEZ5_ERROR_H_ +#define _NM_BLUEZ5_ERROR_H_ + +typedef enum { + NM_BT_ERROR_CONNECTION_NOT_BT = 0, /*< nick=ConnectionNotBt >*/ + NM_BT_ERROR_CONNECTION_INVALID, /*< nick=ConnectionInvalid >*/ + NM_BT_ERROR_CONNECTION_INCOMPATIBLE, /*< nick=ConnectionIncompatible >*/ + NM_BT_ERROR_DUN_CONNECT_FAILED, /*< nick=DunConnectFailed >*/ +} NMBtError; + +#define NM_BT_ERROR (nm_bt_error_quark()) +GQuark nm_bt_error_quark(void); + +#endif /* _NM_BT_ERROR_H_ */ diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c new file mode 100644 index 00000000..c07be2d3 --- /dev/null +++ b/src/core/devices/bluetooth/nm-device-bt.c @@ -0,0 +1,1412 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2011 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-bt.h" + +#include <stdio.h> +#include <linux/if_ether.h> + +#include "nm-core-internal.h" +#include "nm-bluez-common.h" +#include "nm-bluez-manager.h" +#include "devices/nm-device-private.h" +#include "ppp/nm-ppp-manager.h" +#include "nm-setting-connection.h" +#include "nm-setting-bluetooth.h" +#include "nm-setting-cdma.h" +#include "nm-setting-gsm.h" +#include "nm-setting-serial.h" +#include "nm-setting-ppp.h" +#include "NetworkManagerUtils.h" +#include "settings/nm-settings-connection.h" +#include "nm-utils.h" +#include "nm-bt-error.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" + +#include "devices/wwan/nm-modem-manager.h" +#include "devices/wwan/nm-modem.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceBt +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceBt, + PROP_BT_BDADDR, + PROP_BT_BZ_MGR, + PROP_BT_CAPABILITIES, + PROP_BT_DBUS_PATH, + PROP_BT_NAME, ); + +enum { + PPP_STATS, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct { + NMModemManager *modem_manager; + + NMBluezManager *bz_mgr; + + char *dbus_path; + + char *bdaddr; + char *name; + + char *connect_rfcomm_iface; + + GSList *connect_modem_candidates; + + NMModem *modem; + + GCancellable *connect_bz_cancellable; + + gulong connect_watch_link_id; + + guint connect_watch_link_idle_id; + + guint connect_wait_modem_id; + + NMBluetoothCapabilities capabilities : 6; + + NMBluetoothCapabilities connect_bt_type : 6; /* BT type of the current connection */ + + NMDeviceStageState stage1_bt_state : 3; + NMDeviceStageState stage1_modem_prepare_state : 3; + + bool is_connected : 1; + + bool mm_running : 1; + +} NMDeviceBtPrivate; + +struct _NMDeviceBt { + NMDevice parent; + NMDeviceBtPrivate _priv; +}; + +struct _NMDeviceBtClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceBt, nm_device_bt, NM_TYPE_DEVICE) + +#define NM_DEVICE_BT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDeviceBt, NM_IS_DEVICE_BT, NMDevice) + +/*****************************************************************************/ + +NMBluetoothCapabilities +nm_device_bt_get_capabilities(NMDeviceBt *self) +{ + g_return_val_if_fail(NM_IS_DEVICE_BT(self), NM_BT_CAPABILITY_NONE); + + return NM_DEVICE_BT_GET_PRIVATE(self)->capabilities; +} + +static NMBluetoothCapabilities +get_connection_bt_type(NMConnection *connection) +{ + NMSettingBluetooth *s_bt; + const char * bt_type; + + s_bt = nm_connection_get_setting_bluetooth(connection); + + if (s_bt) { + bt_type = nm_setting_bluetooth_get_connection_type(s_bt); + if (bt_type) { + if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_DUN)) + return NM_BT_CAPABILITY_DUN; + else if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_PANU)) + return NM_BT_CAPABILITY_NAP; + } + } + + return NM_BT_CAPABILITY_NONE; +} + +static gboolean +get_connection_bt_type_check(NMDeviceBt * self, + NMConnection * connection, + NMBluetoothCapabilities *out_bt_type, + GError ** error) +{ + NMBluetoothCapabilities bt_type; + + bt_type = get_connection_bt_type(connection); + + NM_SET_OUT(out_bt_type, bt_type); + + if (bt_type == NM_BT_CAPABILITY_NONE) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "profile is not a PANU/DUN bluetooth type"); + return FALSE; + } + + if (!NM_FLAGS_ALL(NM_DEVICE_BT_GET_PRIVATE(self)->capabilities, bt_type)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device does not support bluetooth type"); + return FALSE; + } + + return TRUE; +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_NON_KERNEL; +} + +static gboolean +can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object) +{ + NMDeviceBt * self = NM_DEVICE_BT(device); + NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self); + NMBluetoothCapabilities bt_type; + + nm_assert(!specific_object || !*specific_object); + + if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)->can_auto_connect(device, sett_conn, NULL)) + return FALSE; + + if (!get_connection_bt_type_check(self, + nm_settings_connection_get_connection(sett_conn), + &bt_type, + NULL)) + return FALSE; + + /* Can't auto-activate a DUN connection without ModemManager */ + if (bt_type == NM_BT_CAPABILITY_DUN && priv->mm_running == FALSE) + return FALSE; + + return TRUE; +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceBt * self = NM_DEVICE_BT(device); + NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self); + NMSettingBluetooth *s_bt; + const char * bdaddr; + + if (!NM_DEVICE_CLASS(nm_device_bt_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (!get_connection_bt_type_check(self, connection, NULL, error)) + return FALSE; + + s_bt = nm_connection_get_setting_bluetooth(connection); + + bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt); + if (!bdaddr) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "profile lacks bdaddr setting"); + return FALSE; + } + if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, bdaddr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "devices bdaddr setting mismatches"); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceBt * self = NM_DEVICE_BT(device); + NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self); + NMBluetoothCapabilities bt_type; + + if (!get_connection_bt_type_check(self, connection, &bt_type, error)) + return FALSE; + + if (bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "ModemManager missing for DUN profile"); + return FALSE; + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(device); + NMSettingBluetooth *s_bt; + const char * setting_bdaddr; + const char * ctype; + gboolean is_dun = FALSE; + gboolean is_pan = FALSE; + NMSettingGsm * s_gsm; + NMSettingCdma * s_cdma; + NMSettingSerial * s_serial; + NMSettingPpp * s_ppp; + const char * fallback_prefix = NULL, *preferred = NULL; + + s_gsm = nm_connection_get_setting_gsm(connection); + s_cdma = nm_connection_get_setting_cdma(connection); + s_serial = nm_connection_get_setting_serial(connection); + s_ppp = nm_connection_get_setting_ppp(connection); + + s_bt = nm_connection_get_setting_bluetooth(connection); + if (!s_bt) { + s_bt = (NMSettingBluetooth *) nm_setting_bluetooth_new(); + nm_connection_add_setting(connection, NM_SETTING(s_bt)); + } + + ctype = nm_setting_bluetooth_get_connection_type(s_bt); + if (ctype) { + if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_DUN)) + is_dun = TRUE; + else if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_PANU)) + is_pan = TRUE; + } else { + if (s_gsm || s_cdma) + is_dun = TRUE; + else if (priv->capabilities & NM_BT_CAPABILITY_NAP) + is_pan = TRUE; + } + + if (is_pan) { + /* Make sure the device supports PAN */ + if (!(priv->capabilities & NM_BT_CAPABILITY_NAP)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("PAN requested, but Bluetooth device does not support NAP")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_BLUETOOTH_SETTING_NAME, + NM_SETTING_BLUETOOTH_TYPE); + return FALSE; + } + + /* PAN can't use any DUN-related settings */ + if (s_gsm || s_cdma || s_serial || s_ppp) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("PAN connections cannot specify GSM, CDMA, or serial settings")); + g_prefix_error(error, + "%s: ", + s_gsm ? NM_SETTING_GSM_SETTING_NAME + : s_cdma ? NM_SETTING_CDMA_SETTING_NAME + : s_serial ? NM_SETTING_SERIAL_SETTING_NAME + : NM_SETTING_PPP_SETTING_NAME); + return FALSE; + } + + g_object_set(G_OBJECT(s_bt), + NM_SETTING_BLUETOOTH_TYPE, + NM_SETTING_BLUETOOTH_TYPE_PANU, + NULL); + + fallback_prefix = _("PAN connection"); + } else if (is_dun) { + /* Make sure the device supports PAN */ + if (!(priv->capabilities & NM_BT_CAPABILITY_DUN)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("DUN requested, but Bluetooth device does not support DUN")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_BLUETOOTH_SETTING_NAME, + NM_SETTING_BLUETOOTH_TYPE); + return FALSE; + } + + /* Need at least a GSM or a CDMA setting */ + if (!s_gsm && !s_cdma) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("DUN connection must include a GSM or CDMA setting")); + g_prefix_error(error, "%s: ", NM_SETTING_BLUETOOTH_SETTING_NAME); + return FALSE; + } + + g_object_set(G_OBJECT(s_bt), + NM_SETTING_BLUETOOTH_TYPE, + NM_SETTING_BLUETOOTH_TYPE_DUN, + NULL); + + if (s_gsm) { + fallback_prefix = _("GSM connection"); + } else { + fallback_prefix = _("CDMA connection"); + if (!nm_setting_cdma_get_number(s_cdma)) + g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL); + } + } else { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Unknown/unhandled Bluetooth connection type")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_BLUETOOTH_SETTING_NAME, + NM_SETTING_BLUETOOTH_TYPE); + return FALSE; + } + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_BLUETOOTH_SETTING_NAME, + existing_connections, + preferred, + fallback_prefix, + NULL, + NULL, + is_dun ? FALSE : TRUE); /* No IPv6 yet for DUN */ + + setting_bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt); + if (setting_bdaddr) { + /* Make sure the setting BT Address (if any) matches the device's */ + if (!nm_utils_hwaddr_matches(setting_bdaddr, -1, priv->bdaddr, -1)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("connection does not match device")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_BLUETOOTH_SETTING_NAME, + NM_SETTING_BLUETOOTH_BDADDR); + return FALSE; + } + } else { + /* Lock the connection to this device by default */ + if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, NULL, ETH_ALEN)) + g_object_set(G_OBJECT(s_bt), NM_SETTING_BLUETOOTH_BDADDR, priv->bdaddr, NULL); + } + + return TRUE; +} + +/*****************************************************************************/ +/* IP method PPP */ + +static void +ppp_stats(NMModem *modem, guint i_in_bytes, guint i_out_bytes, gpointer user_data) +{ + guint32 in_bytes = i_in_bytes; + guint32 out_bytes = i_out_bytes; + + g_signal_emit(NM_DEVICE_BT(user_data), + signals[PPP_STATS], + 0, + (guint) in_bytes, + (guint) out_bytes); +} + +static void +ppp_failed(NMModem *modem, guint i_reason, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceBt * self = NM_DEVICE_BT(user_data); + NMDeviceStateReason reason = i_reason; + + switch (nm_device_get_state(device)) { + case NM_DEVICE_STATE_PREPARE: + case NM_DEVICE_STATE_CONFIG: + case NM_DEVICE_STATE_NEED_AUTH: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason); + break; + case NM_DEVICE_STATE_IP_CONFIG: + case NM_DEVICE_STATE_IP_CHECK: + case NM_DEVICE_STATE_SECONDARIES: + case NM_DEVICE_STATE_ACTIVATED: + if (nm_device_activate_ip4_state_in_conf(device)) + nm_device_activate_schedule_ip_config_timeout(device, AF_INET); + else if (nm_device_activate_ip6_state_in_conf(device)) + nm_device_activate_schedule_ip_config_timeout(device, AF_INET6); + else if (nm_device_activate_ip4_state_done(device)) { + nm_device_ip_method_failed(device, + AF_INET, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else if (nm_device_activate_ip6_state_done(device)) { + nm_device_ip_method_failed(device, + AF_INET6, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else { + _LOGW(LOGD_MB, + "PPP failure in unexpected state %u", + (guint) nm_device_get_state(device)); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } + break; + default: + break; + } +} + +static void +modem_auth_requested(NMModem *modem, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled + * during activation. + */ + if (!nm_device_is_activating(device)) + return; + + nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE); +} + +static void +modem_auth_result(NMModem *modem, GError *error, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device); + + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + + if (error) { + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT; + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data) +{ + NMDeviceBt * self = user_data; + NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self); + NMDeviceStateReason reason = i_reason; + NMDeviceState state; + + state = nm_device_get_state(NM_DEVICE(self)); + + g_return_if_fail(NM_IN_SET(state, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_NEED_AUTH)); + + nm_assert(priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING); + + if (!success) { + if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT) { + /* If the connect failed because the SIM PIN was wrong don't allow + * the device to be auto-activated anymore, which would risk locking + * the SIM if the incorrect PIN continues to be used. + */ + nm_device_autoconnect_blocked_set(NM_DEVICE(self), + NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN); + } + + nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason); + return; + } + + priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_COMPLETED; + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device); + + if (priv->modem) + nm_modem_device_state_changed(priv->modem, new_state, old_state); + + /* Need to recheck available connections whenever MM appears or disappears, + * since the device could be both DUN and NAP capable and thus may not + * change state (which rechecks available connections) when MM comes and goes. + */ + if (priv->mm_running && NM_FLAGS_HAS(priv->capabilities, NM_BT_CAPABILITY_DUN)) + nm_device_recheck_available_connections(device); +} + +static void +modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data) +{ + NMDeviceBt *self = NM_DEVICE_BT(user_data); + NMDevice * device = NM_DEVICE(self); + + g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE); + + if (error) { + _LOGW(LOGD_MB | LOGD_IP4 | LOGD_BT, + "retrieving IP4 configuration failed: %s", + error->message); + nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return; + } + + nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config)); +} + +static void +ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + if (!nm_device_is_activating(device)) + return; + + if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } +} + +/*****************************************************************************/ + +static void +modem_cleanup(NMDeviceBt *self) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + if (priv->modem) { + g_signal_handlers_disconnect_matched(priv->modem, + G_SIGNAL_MATCH_DATA, + 0, + 0, + NULL, + NULL, + self); + nm_clear_pointer(&priv->modem, nm_modem_unclaim); + } +} + +static void +modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data) +{ + NMModemState new_state = new_state_i; + NMModemState old_state = old_state_i; + NMDevice * device = NM_DEVICE(user_data); + NMDeviceState dev_state = nm_device_get_state(device); + + if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING) { + /* Will be called whenever something external to NM disables the + * modem directly through ModemManager. + */ + if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) { + nm_device_state_changed(device, + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + return; + } + } + + if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING + && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) { + /* Fail the device if the modem disconnects unexpectedly while the + * device is activating/activated. */ + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER); + return; + } +} + +static void +modem_removed_cb(NMModem *modem, gpointer user_data) +{ + NMDeviceBt * self = NM_DEVICE_BT(user_data); + NMDeviceState state; + + state = nm_device_get_state(NM_DEVICE(self)); + if (nm_device_is_activating(NM_DEVICE(self)) || state == NM_DEVICE_STATE_ACTIVATED) { + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + return; + } + + modem_cleanup(self); +} + +static gboolean +modem_try_claim(NMDeviceBt *self, NMModem *modem) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + gs_free char * rfcomm_base_name = NULL; + NMDeviceState state; + + if (priv->modem) { + if (priv->modem == modem) + return TRUE; + return FALSE; + } + + if (nm_modem_is_claimed(modem)) + return FALSE; + + if (!priv->connect_rfcomm_iface) + return FALSE; + + rfcomm_base_name = g_path_get_basename(priv->connect_rfcomm_iface); + if (!nm_streq0(rfcomm_base_name, nm_modem_get_control_port(modem))) + return FALSE; + + /* Can only accept the modem in stage1, but since the interface matched + * what we were expecting, don't let anything else claim the modem either. + */ + state = nm_device_get_state(NM_DEVICE(self)); + if (state != NM_DEVICE_STATE_PREPARE) { + _LOGD(LOGD_BT | LOGD_MB, + "modem found but device not in correct state (%d)", + nm_device_get_state(NM_DEVICE(self))); + return FALSE; + } + + priv->modem = nm_modem_claim(modem); + priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT; + + g_signal_connect(modem, NM_MODEM_PPP_STATS, G_CALLBACK(ppp_stats), self); + g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self); + g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self); + g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self); + g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self); + g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self); + g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self); + g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self); + g_signal_connect(modem, + "notify::" NM_MODEM_IP_IFINDEX, + G_CALLBACK(ip_ifindex_changed_cb), + self); + + _LOGD(LOGD_BT | LOGD_MB, "modem found"); + + return TRUE; +} + +static void +mm_modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data) +{ + NMDeviceBt * self = user_data; + NMDeviceBtPrivate *priv; + + if (!modem_try_claim(user_data, modem)) + return; + + priv = NM_DEVICE_BT_GET_PRIVATE(self); + + if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED) + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); +} + +/*****************************************************************************/ + +void +_nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + connected = !!connected; + if (priv->is_connected == connected) + return; + + priv->is_connected = connected; + + if (connected || priv->stage1_bt_state != NM_DEVICE_STAGE_STATE_COMPLETED + || nm_device_get_state(NM_DEVICE(self)) > NM_DEVICE_STATE_ACTIVATED) { + _LOGT(LOGD_BT, "set-connected: %d", connected); + return; + } + + _LOGT(LOGD_BT, "set-connected: %d (disconnecting device...)", connected); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CARRIER); +} + +static gboolean +connect_watch_link_idle_cb(gpointer user_data) +{ + NMDeviceBt * self = user_data; + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + int ifindex; + + priv->connect_watch_link_idle_id = 0; + + if (nm_device_get_state(NM_DEVICE(self)) <= NM_DEVICE_STATE_ACTIVATED) { + ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self)); + if (ifindex > 0 + && !nm_platform_link_get(nm_device_get_platform(NM_DEVICE(self)), ifindex)) { + _LOGT(LOGD_BT, "device disappeared"); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + } + } + + return G_SOURCE_REMOVE; +} + +static void +connect_watch_link_cb(NMPlatform * platform, + int obj_type_i, + int ifindex, + NMPlatformLink *info, + int change_type_i, + NMDevice * self) +{ + const NMPlatformSignalChangeType change_type = change_type_i; + NMDeviceBtPrivate * priv; + + /* bluez doesn't notify us when the connection disconnects. + * Neither does NMManager (or NMDevice) tell us when the ip-ifindex goes away. + * This is horrible, and should be improved. For now, watch the link ourself... */ + + if (NM_IN_SET(change_type, NM_PLATFORM_SIGNAL_CHANGED, NM_PLATFORM_SIGNAL_REMOVED)) { + priv = NM_DEVICE_BT_GET_PRIVATE(self); + if (priv->connect_watch_link_idle_id == 0) + priv->connect_watch_link_idle_id = g_idle_add(connect_watch_link_idle_cb, self); + } +} + +static gboolean +connect_wait_modem_timeout(gpointer user_data) +{ + NMDeviceBt * self = NM_DEVICE_BT(user_data); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + /* since this timeout is longer than the connect timeout, we must have already + * hit the connect-timeout first or being connected. */ + nm_assert(priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED); + + priv->connect_wait_modem_id = 0; + nm_clear_g_cancellable(&priv->connect_bz_cancellable); + + if (priv->modem) + _LOGD(LOGD_BT, "timeout connecting modem for DUN connection"); + else + _LOGD(LOGD_BT, "timeout finding modem for DUN connection"); + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND); + return G_SOURCE_REMOVE; +} + +static void +connect_bz_cb(NMBluezManager *bz_mgr, + gboolean is_complete, + const char * device_name, + GError * error, + gpointer user_data) +{ + NMDeviceBt * self; + NMDeviceBtPrivate *priv; + char sbuf[100]; + + if (nm_utils_error_is_cancelled(error)) + return; + + self = user_data; + priv = NM_DEVICE_BT_GET_PRIVATE(self); + + nm_assert(nm_device_is_activating(NM_DEVICE(self))); + nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->connect_bt_type, + NM_BT_CAPABILITY_DUN, + NM_BT_CAPABILITY_NAP)); + + if (!is_complete) { + nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_DUN); + nm_assert(device_name); + nm_assert(!error); + + if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) { + nm_assert(!priv->connect_rfcomm_iface); + _LOGD(LOGD_BT, + "DUN is still connecting but got serial port \"%s\" to claim modem", + device_name); + g_free(priv->connect_rfcomm_iface); + priv->connect_rfcomm_iface = g_strdup(device_name); + } + return; + } + + g_clear_object(&priv->connect_bz_cancellable); + + if (!device_name) { + _LOGW(LOGD_BT, + "%s connect request failed: %s", + nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)), + error->message); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + return; + } + + _LOGD(LOGD_BT, + "%s connect request successful (%s)", + nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)), + device_name); + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) { + if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) { + nm_assert_not_reached(); + g_free(priv->connect_rfcomm_iface); + priv->connect_rfcomm_iface = g_strdup(device_name); + } + } else { + nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_NAP); + if (!nm_device_set_ip_iface(NM_DEVICE(self), device_name)) { + _LOGW(LOGD_BT, "Error connecting with bluez: cannot find device %s", device_name); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + return; + } + priv->connect_watch_link_id = g_signal_connect(nm_device_get_platform(NM_DEVICE(self)), + NM_PLATFORM_SIGNAL_LINK_CHANGED, + G_CALLBACK(connect_watch_link_cb), + self); + } + + if (!priv->is_connected) { + /* we got the callback from NMBluezManager with success. We actually should be + * connected and this line shouldn't be reached. */ + nm_assert_not_reached(); + _LOGE(LOGD_BT, "bluetooth is unexpectedly not in connected state"); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + return; + } + + priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_COMPLETED; + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceBt * self = NM_DEVICE_BT(device); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + gs_free_error GError *error = NULL; + NMConnection * connection; + + connection = nm_device_get_applied_connection(device); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + priv->connect_bt_type = get_connection_bt_type(connection); + if (priv->connect_bt_type == NM_BT_CAPABILITY_NONE) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_BT_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_PENDING) + return NM_ACT_STAGE_RETURN_POSTPONE; + else if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_INIT) { + gs_unref_object GCancellable *cancellable = NULL; + char sbuf[100]; + + _LOGD(LOGD_BT, + "connecting to %s bluetooth device", + nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf))); + + cancellable = g_cancellable_new(); + + if (!nm_bluez_manager_connect(priv->bz_mgr, + priv->dbus_path, + priv->connect_bt_type, + 30000, + cancellable, + connect_bz_cb, + self, + &error)) { + _LOGD(LOGD_BT, "cannot connect to bluetooth device: %s", error->message); + *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + priv->connect_bz_cancellable = g_steal_pointer(&cancellable); + priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_PENDING; + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) { + if (!priv->modem) { + gs_free NMModem **modems = NULL; + guint i, n; + + if (priv->connect_wait_modem_id == 0) + priv->connect_wait_modem_id = + g_timeout_add_seconds(30, connect_wait_modem_timeout, self); + + modems = nm_modem_manager_get_modems(priv->modem_manager, &n); + for (i = 0; i < n; i++) { + if (modem_try_claim(self, modems[i])) + break; + } + if (!priv->modem) + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING) + return NM_ACT_STAGE_RETURN_POSTPONE; + if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_INIT) { + priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_PENDING; + return nm_modem_act_stage1_prepare(priv->modem, + nm_device_get_act_request(NM_DEVICE(self)), + out_failure_reason); + } + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceBt * self = NM_DEVICE_BT(device); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) + nm_modem_act_stage2_config(priv->modem); + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device); + + nm_assert_addr_family(addr_family); + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) { + if (addr_family == AF_INET) { + return nm_modem_stage3_ip4_config_start(priv->modem, + device, + NM_DEVICE_CLASS(nm_device_bt_parent_class), + out_failure_reason); + } else { + return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason); + } + } + + return NM_DEVICE_CLASS(nm_device_bt_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device); + + nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->connect_watch_link_id); + nm_clear_g_source(&priv->connect_watch_link_idle_id); + priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT; + nm_clear_g_source(&priv->connect_wait_modem_id); + nm_clear_g_cancellable(&priv->connect_bz_cancellable); + + priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT; + + if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) { + if (priv->modem) { + nm_modem_deactivate(priv->modem, device); + + /* Since we're killing the Modem object before it'll get the + * state change signal, simulate the state change here. + */ + nm_modem_device_state_changed(priv->modem, + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_ACTIVATED); + modem_cleanup(NM_DEVICE_BT(device)); + } + } + + if (priv->connect_bt_type != NM_BT_CAPABILITY_NONE) { + priv->connect_bt_type = NM_BT_CAPABILITY_NONE; + nm_bluez_manager_disconnect(priv->bz_mgr, priv->dbus_path); + } + + nm_clear_g_free(&priv->connect_rfcomm_iface); + + if (NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate) + NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate(device); +} + +void +_nm_device_bt_notify_removed(NMDeviceBt *self) +{ + g_signal_emit_by_name(self, NM_DEVICE_REMOVED); +} + +/*****************************************************************************/ + +gboolean +_nm_device_bt_for_same_device(NMDeviceBt * self, + const char * dbus_path, + const char * bdaddr, + const char * name, + NMBluetoothCapabilities capabilities) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + return nm_streq(priv->dbus_path, dbus_path) && nm_streq(priv->bdaddr, bdaddr) + && capabilities == priv->capabilities && (!name || nm_streq(priv->name, name)); +} + +void +_nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + nm_assert(name); + + if (!nm_streq(priv->name, name)) { + _LOGT(LOGD_BT, "set-name: %s", name); + g_free(priv->name); + priv->name = g_strdup(name); + _notify(self, PROP_BT_NAME); + } +} + +/*****************************************************************************/ + +static gboolean +is_available(NMDevice *dev, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceBt * self = NM_DEVICE_BT(dev); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + /* PAN doesn't need ModemManager, so devices that support it are always available */ + if (priv->capabilities & NM_BT_CAPABILITY_NAP) + return TRUE; + + /* DUN requires ModemManager */ + return priv->mm_running; +} + +static void +set_mm_running(NMDeviceBt *self) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + gboolean running; + + running = (nm_modem_manager_name_owner_get(priv->modem_manager) != NULL); + + if (priv->mm_running != running) { + _LOGD(LOGD_BT, "ModemManager now %s", running ? "available" : "unavailable"); + + priv->mm_running = running; + nm_device_queue_recheck_available(NM_DEVICE(self), + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE); + } +} + +static void +mm_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data) +{ + set_mm_running(user_data); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_BT_NAME: + g_value_set_string(value, priv->name); + break; + case PROP_BT_CAPABILITIES: + g_value_set_uint(value, priv->capabilities); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_BT_BZ_MGR: + /* construct-only */ + priv->bz_mgr = g_object_ref(g_value_get_pointer(value)); + nm_assert(NM_IS_BLUEZ_MANAGER(priv->bz_mgr)); + break; + case PROP_BT_DBUS_PATH: + /* construct-only */ + priv->dbus_path = g_value_dup_string(value); + nm_assert(priv->dbus_path); + break; + case PROP_BT_BDADDR: + /* construct-only */ + priv->bdaddr = g_value_dup_string(value); + nm_assert(priv->bdaddr); + break; + case PROP_BT_NAME: + /* construct-only */ + priv->name = g_value_dup_string(value); + nm_assert(priv->name); + break; + case PROP_BT_CAPABILITIES: + /* construct-only */ + priv->capabilities = g_value_get_uint(value); + nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->capabilities, + NM_BT_CAPABILITY_DUN, + NM_BT_CAPABILITY_NAP, + NM_BT_CAPABILITY_DUN | NM_BT_CAPABILITY_NAP)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_bt_init(NMDeviceBt *self) +{} + +static void +constructed(GObject *object) +{ + NMDeviceBt * self = NM_DEVICE_BT(object); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + G_OBJECT_CLASS(nm_device_bt_parent_class)->constructed(object); + + priv->modem_manager = g_object_ref(nm_modem_manager_get()); + + nm_modem_manager_name_owner_ref(priv->modem_manager); + + g_signal_connect(priv->modem_manager, + NM_MODEM_MANAGER_MODEM_ADDED, + G_CALLBACK(mm_modem_added_cb), + self); + + g_signal_connect(priv->modem_manager, + "notify::" NM_MODEM_MANAGER_NAME_OWNER, + G_CALLBACK(mm_name_owner_changed_cb), + self); + + set_mm_running(self); +} + +NMDeviceBt * +nm_device_bt_new(NMBluezManager * bz_mgr, + const char * dbus_path, + const char * bdaddr, + const char * name, + NMBluetoothCapabilities capabilities) +{ + g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(bz_mgr), NULL); + g_return_val_if_fail(dbus_path, NULL); + g_return_val_if_fail(bdaddr, NULL); + g_return_val_if_fail(name, NULL); + g_return_val_if_fail(capabilities != NM_BT_CAPABILITY_NONE, NULL); + + return g_object_new(NM_TYPE_DEVICE_BT, + NM_DEVICE_UDI, + dbus_path, + NM_DEVICE_IFACE, + bdaddr, + NM_DEVICE_DRIVER, + "bluez", + NM_DEVICE_PERM_HW_ADDRESS, + bdaddr, + NM_DEVICE_BT_BDADDR, + bdaddr, + NM_DEVICE_BT_BZ_MGR, + bz_mgr, + NM_DEVICE_BT_CAPABILITIES, + (guint) capabilities, + NM_DEVICE_BT_DBUS_PATH, + dbus_path, + NM_DEVICE_BT_NAME, + name, + NM_DEVICE_TYPE_DESC, + "Bluetooth", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_BT, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceBt * self = NM_DEVICE_BT(object); + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self); + + nm_clear_g_signal_handler(nm_device_get_platform(NM_DEVICE(self)), + &priv->connect_watch_link_id); + nm_clear_g_source(&priv->connect_watch_link_idle_id); + + nm_clear_g_source(&priv->connect_wait_modem_id); + nm_clear_g_cancellable(&priv->connect_bz_cancellable); + + if (priv->modem_manager) { + g_signal_handlers_disconnect_by_func(priv->modem_manager, + G_CALLBACK(mm_name_owner_changed_cb), + self); + g_signal_handlers_disconnect_by_func(priv->modem_manager, + G_CALLBACK(mm_modem_added_cb), + self); + nm_modem_manager_name_owner_unref(priv->modem_manager); + g_clear_object(&priv->modem_manager); + } + + modem_cleanup(self); + + G_OBJECT_CLASS(nm_device_bt_parent_class)->dispose(object); + + g_clear_object(&priv->bz_mgr); +} + +static void +finalize(GObject *object) +{ + NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object); + + g_free(priv->connect_rfcomm_iface); + g_free(priv->dbus_path); + g_free(priv->name); + g_free(priv->bdaddr); + + G_OBJECT_CLASS(nm_device_bt_parent_class)->finalize(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_bluetooth = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_BLUETOOTH, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Name", "s", NM_DEVICE_BT_NAME), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("BtCapabilities", + "u", + NM_DEVICE_BT_CAPABILITIES), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_bt_class_init(NMDeviceBtClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->set_property = set_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bluetooth); + + device_class->connection_type_check_compatible = NM_SETTING_BLUETOOTH_SETTING_NAME; + + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->can_auto_connect = can_auto_connect; + device_class->deactivate = deactivate; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + device_class->is_available = is_available; + device_class->get_configured_mtu = nm_modem_get_configured_mtu; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_BT_BZ_MGR] = + g_param_spec_pointer(NM_DEVICE_BT_BZ_MGR, + "", + "", + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BT_BDADDR] = + g_param_spec_string(NM_DEVICE_BT_BDADDR, + "", + "", + NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BT_DBUS_PATH] = + g_param_spec_string(NM_DEVICE_BT_DBUS_PATH, + "", + "", + NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BT_NAME] = + g_param_spec_string(NM_DEVICE_BT_NAME, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BT_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_BT_CAPABILITIES, + "", + "", + NM_BT_CAPABILITY_NONE, + G_MAXUINT, + NM_BT_CAPABILITY_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[PPP_STATS] = g_signal_new(NM_DEVICE_BT_PPP_STATS, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_UINT /*guint32 in_bytes*/, + G_TYPE_UINT /*guint32 out_bytes*/); +} diff --git a/src/core/devices/bluetooth/nm-device-bt.h b/src/core/devices/bluetooth/nm-device-bt.h new file mode 100644 index 00000000..c2d3bc18 --- /dev/null +++ b/src/core/devices/bluetooth/nm-device-bt.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_BT_H__ +#define __NETWORKMANAGER_DEVICE_BT_H__ + +#include "devices/nm-device.h" + +#define NM_TYPE_DEVICE_BT (nm_device_bt_get_type()) +#define NM_DEVICE_BT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BT, NMDeviceBt)) +#define NM_DEVICE_BT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BT, NMDeviceBtClass)) +#define NM_IS_DEVICE_BT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BT)) +#define NM_IS_DEVICE_BT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BT)) +#define NM_DEVICE_BT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BT, NMDeviceBtClass)) + +#define NM_DEVICE_BT_BDADDR "bt-bdaddr" +#define NM_DEVICE_BT_BZ_MGR "bt-bz-mgr" +#define NM_DEVICE_BT_CAPABILITIES "bt-capabilities" +#define NM_DEVICE_BT_DBUS_PATH "bt-dbus-path" +#define NM_DEVICE_BT_NAME "bt-name" + +#define NM_DEVICE_BT_PPP_STATS "ppp-stats" + +typedef struct _NMDeviceBt NMDeviceBt; +typedef struct _NMDeviceBtClass NMDeviceBtClass; + +GType nm_device_bt_get_type(void); + +struct _NMBluezManager; + +NMDeviceBt *nm_device_bt_new(struct _NMBluezManager *bz_mgr, + const char * dbus_path, + const char * bdaddr, + const char * name, + NMBluetoothCapabilities capabilities); + +gboolean _nm_device_bt_for_same_device(NMDeviceBt * device, + const char * dbus_path, + const char * bdaddr, + const char * name, + NMBluetoothCapabilities capabilities); + +NMBluetoothCapabilities nm_device_bt_get_capabilities(NMDeviceBt *device); + +struct _NMModem; + +gboolean nm_device_bt_modem_added(NMDeviceBt *device, struct _NMModem *modem, const char *driver); + +void _nm_device_bt_notify_removed(NMDeviceBt *self); + +void _nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name); + +void _nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected); + +#endif /* __NETWORKMANAGER_DEVICE_BT_H__ */ diff --git a/src/core/devices/bluetooth/tests/nm-bt-test.c b/src/core/devices/bluetooth/tests/nm-bt-test.c new file mode 100644 index 00000000..0fc8aa87 --- /dev/null +++ b/src/core/devices/bluetooth/tests/nm-bt-test.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "src/core/nm-default-daemon.h" + +#include <glib-unix.h> + +#include "devices/bluetooth/nm-bluez5-dun.h" + +#include "nm-test-utils-core.h" + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_BT +#define _NMLOG(level, ...) \ + nm_log((level), \ + _NMLOG_DOMAIN, \ + NULL, \ + NULL, \ + "bt%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + NM_PRINT_FMT_QUOTED(gl.argv_cmd, "[", gl.argv_cmd, "]", "") \ + _NM_UTILS_MACRO_REST(__VA_ARGS__)) + +/*****************************************************************************/ + +struct { + int argc; + const char *const *argv; + const char * argv_cmd; + GMainLoop * loop; +} gl; + +typedef struct _MainCmdInfo { + const char *name; + int (*main_func)(const struct _MainCmdInfo *main_cmd_info); +} MainCmdInfo; + +/*****************************************************************************/ + +#if WITH_BLUEZ5_DUN + +typedef struct { + NMBluez5DunContext *dun_context; + GCancellable * cancellable; + guint timeout_id; + guint sig_term_id; + guint sig_int_id; +} DunConnectData; + +static void +_dun_connect_cb(NMBluez5DunContext *context, + const char * rfcomm_dev, + GError * error, + gpointer user_data) +{ + DunConnectData *dun_connect_data = user_data; + + g_assert(dun_connect_data); + g_assert(!dun_connect_data->dun_context); + g_assert((!!error) != (!!rfcomm_dev)); + + if (rfcomm_dev && !context) { + _LOGI("dun-connect notifies path \"%s\". Wait longer...", rfcomm_dev); + return; + } + + if (rfcomm_dev) { + g_assert(context); + _LOGI("dun-connect completed with path \"%s\"", rfcomm_dev); + } else { + g_assert(!context); + _LOGI("dun-connect failed with error: %s", error->message); + } + + dun_connect_data->dun_context = context; + + g_main_loop_quit(gl.loop); +} + +static void +_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data) +{ + _LOGI("dun-connect: notified TTY hangup"); +} + +static gboolean +_timeout_cb(gpointer user_data) +{ + DunConnectData *dun_connect_data = user_data; + + _LOGI("timeout"); + dun_connect_data->timeout_id = 0; + if (dun_connect_data->cancellable) + g_cancellable_cancel(dun_connect_data->cancellable); + return G_SOURCE_REMOVE; +} + +static gboolean +_sig_xxx_cb(DunConnectData *dun_connect_data, int sigid) +{ + _LOGI("signal %s received", sigid == SIGTERM ? "SIGTERM" : "SIGINT"); + g_main_loop_quit(gl.loop); + return G_SOURCE_CONTINUE; +} + +static gboolean +_sig_term_cb(gpointer user_data) +{ + return _sig_xxx_cb(user_data, SIGTERM); +} + +static gboolean +_sig_int_cb(gpointer user_data) +{ + return _sig_xxx_cb(user_data, SIGINT); +} +#endif + +static int +do_dun_connect(const MainCmdInfo *main_cmd_info) +{ +#if WITH_BLUEZ5_DUN + gs_unref_object GCancellable *cancellable = NULL; + gs_free_error GError *error = NULL; + const char * adapter; + const char * remote; + DunConnectData dun_connect_data = {}; + + if (gl.argc < 4) { + _LOGE("missing arguments \"adapter\" and \"remote\""); + return -1; + } + + adapter = gl.argv[2]; + remote = gl.argv[3]; + + cancellable = g_cancellable_new(); + dun_connect_data.cancellable = cancellable; + + if (!nm_bluez5_dun_connect(adapter, + remote, + cancellable, + _dun_connect_cb, + &dun_connect_data, + _dun_notify_tty_hangup_cb, + &dun_connect_data, + &error)) { + _LOGE("connect failed to start: %s", error->message); + return -1; + } + + dun_connect_data.timeout_id = g_timeout_add(60000, _timeout_cb, &dun_connect_data); + + g_main_loop_run(gl.loop); + + nm_clear_g_source(&dun_connect_data.timeout_id); + + if (dun_connect_data.dun_context) { + dun_connect_data.sig_term_id = g_unix_signal_add(SIGTERM, _sig_term_cb, &dun_connect_data); + dun_connect_data.sig_int_id = g_unix_signal_add(SIGINT, _sig_int_cb, &dun_connect_data); + + g_main_loop_run(gl.loop); + + nm_clear_g_source(&dun_connect_data.sig_term_id); + nm_clear_g_source(&dun_connect_data.sig_int_id); + + nm_bluez5_dun_disconnect(g_steal_pointer(&dun_connect_data.dun_context)); + } + + return 0; +#else + _LOGE("compiled without bluetooth DUN support"); + return 1; +#endif +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + static const MainCmdInfo main_cmd_infos[] = { + { + .name = "dun-connect", + .main_func = do_dun_connect, + }, + }; + int exit_code = 0; + guint i; + + if (!g_getenv("G_MESSAGES_DEBUG")) + g_setenv("G_MESSAGES_DEBUG", "all", TRUE); + + nmtst_init_with_logging(&argc, &argv, "DEBUG", "ALL"); + + nm_logging_init(NULL, TRUE); + + gl.argv = (const char *const *) argv; + gl.argc = argc; + gl.loop = g_main_loop_new(NULL, FALSE); + + _LOGI("bluetooth test util start"); + + gl.argv_cmd = argc >= 2 ? argv[1] : NULL; + + for (i = 0; i < G_N_ELEMENTS(main_cmd_infos); i++) { + if (nm_streq0(main_cmd_infos[i].name, gl.argv_cmd)) { + _LOGD("start \"%s\"", gl.argv_cmd); + exit_code = main_cmd_infos[i].main_func(&main_cmd_infos[i]); + _LOGD("completed with %d", exit_code); + break; + } + } + if (gl.argv_cmd && i >= G_N_ELEMENTS(main_cmd_infos)) { + nm_log_err(LOGD_BT, "invalid command \"%s\"", gl.argv_cmd); + exit_code = -1; + } + + nm_clear_pointer(&gl.loop, g_main_loop_unref); + + return exit_code; +} diff --git a/src/core/devices/meson.build b/src/core/devices/meson.build new file mode 100644 index 00000000..66f7aa92 --- /dev/null +++ b/src/core/devices/meson.build @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +subdir('adsl') + +if enable_modem_manager + subdir('wwan') + subdir('bluetooth') +endif + +if enable_wifi + subdir('wifi') +endif + +if enable_teamdctl + subdir('team') +endif + +if enable_ovs + subdir('ovs') +endif + +if enable_tests + subdir('tests') +endif diff --git a/src/core/devices/nm-acd-manager.c b/src/core/devices/nm-acd-manager.c new file mode 100644 index 00000000..b95f90fd --- /dev/null +++ b/src/core/devices/nm-acd-manager.c @@ -0,0 +1,496 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 - 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-acd-manager.h" + +#include <netinet/in.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <linux/if_ether.h> + +#include "platform/nm-platform.h" +#include "nm-utils.h" +#include "NetworkManagerUtils.h" +#include "n-acd/src/n-acd.h" + +/*****************************************************************************/ + +typedef enum { + STATE_INIT, + STATE_PROBING, + STATE_PROBE_DONE, + STATE_ANNOUNCING, +} State; + +typedef struct { + in_addr_t address; + gboolean duplicate; + NAcdProbe *probe; +} AddressInfo; + +struct _NMAcdManager { + int ifindex; + guint8 hwaddr[ETH_ALEN]; + State state; + GHashTable *addresses; + guint completed; + NAcd * acd; + GSource * event_source; + + NMAcdCallbacks callbacks; + gpointer user_data; +}; + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_IP4 +#define _NMLOG_PREFIX_NAME "acd" +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + char _sbuf[64]; \ + \ + nm_log((level), \ + _NMLOG_DOMAIN, \ + self && self->ifindex > 0 \ + ? nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex) \ + : NULL, \ + NULL, \ + "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + self ? nm_sprintf_buf(_sbuf, "[%p,%d]", self, self->ifindex) \ + : "" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static const char * +_acd_event_to_string(unsigned int event) +{ + switch (event) { + case N_ACD_EVENT_READY: + return "ready"; + case N_ACD_EVENT_USED: + return "used"; + case N_ACD_EVENT_DEFENDED: + return "defended"; + case N_ACD_EVENT_CONFLICT: + return "conflict"; + case N_ACD_EVENT_DOWN: + return "down"; + } + return NULL; +} + +#define ACD_EVENT_TO_STRING_BUF_SIZE 50 + +static const char * +_acd_event_to_string_buf(unsigned event, char buffer[static ACD_EVENT_TO_STRING_BUF_SIZE]) +{ + const char *s; + + s = _acd_event_to_string(event); + if (s) + return s; + + g_snprintf(buffer, ACD_EVENT_TO_STRING_BUF_SIZE, "(%u)", event); + return buffer; +} + +static const char * +acd_error_to_string(int error) +{ + if (error < 0) + return nm_strerror_native(-error); + + switch (error) { + case _N_ACD_E_SUCCESS: + return "success"; + case N_ACD_E_PREEMPTED: + return "preempted"; + case N_ACD_E_INVALID_ARGUMENT: + return "invalid argument"; + } + + g_return_val_if_reached(NULL); +} + +static int +acd_error_to_nmerr(int error, gboolean always_fail) +{ + if (error < 0) + return -nm_errno_native(error); + + if (always_fail) { + if (NM_IN_SET(error, N_ACD_E_PREEMPTED, N_ACD_E_INVALID_ARGUMENT)) + return -NME_UNSPEC; + g_return_val_if_reached(-NME_UNSPEC); + } + + /* so, @error is either zero (indicating success) or one + * of the special status codes like N_ACD_E_*. In both cases, + * return the positive value here. */ + if (NM_IN_SET(error, _N_ACD_E_SUCCESS, N_ACD_E_PREEMPTED, N_ACD_E_INVALID_ARGUMENT)) + return error; + + g_return_val_if_reached(error); +} + +/*****************************************************************************/ + +/** + * nm_acd_manager_add_address: + * @self: a #NMAcdManager + * @address: an IP address + * + * Add @address to the list of IP addresses to probe. + + * Returns: %TRUE on success, %FALSE if the address was already in the list + */ +gboolean +nm_acd_manager_add_address(NMAcdManager *self, in_addr_t address) +{ + AddressInfo *info; + + g_return_val_if_fail(self, FALSE); + g_return_val_if_fail(self->state == STATE_INIT, FALSE); + + if (g_hash_table_lookup(self->addresses, GUINT_TO_POINTER(address))) + return FALSE; + + info = g_slice_new0(AddressInfo); + info->address = address; + + g_hash_table_insert(self->addresses, GUINT_TO_POINTER(address), info); + + return TRUE; +} + +static gboolean +acd_event(int fd, GIOCondition condition, gpointer data) +{ + NMAcdManager *self = data; + NAcdEvent * event; + AddressInfo * info; + gboolean emit_probe_terminated = FALSE; + char address_str[INET_ADDRSTRLEN]; + int r; + + if (n_acd_dispatch(self->acd)) + return G_SOURCE_CONTINUE; + + while (!n_acd_pop_event(self->acd, &event) && event) { + char to_string_buffer[ACD_EVENT_TO_STRING_BUF_SIZE]; + gs_free char *hwaddr_str = NULL; + gboolean check_probing_done = FALSE; + + switch (event->event) { + case N_ACD_EVENT_READY: + n_acd_probe_get_userdata(event->ready.probe, (void **) &info); + info->duplicate = FALSE; + if (self->state == STATE_ANNOUNCING) { + /* fake probe ended, start announcing */ + r = n_acd_probe_announce(info->probe, N_ACD_DEFEND_ONCE); + if (r) { + _LOGW("couldn't announce address %s on interface '%s': %s", + _nm_utils_inet4_ntop(info->address, address_str), + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + } else { + _LOGD("announcing address %s", + _nm_utils_inet4_ntop(info->address, address_str)); + } + } + check_probing_done = TRUE; + break; + case N_ACD_EVENT_USED: + n_acd_probe_get_userdata(event->used.probe, (void **) &info); + info->duplicate = TRUE; + check_probing_done = TRUE; + break; + case N_ACD_EVENT_DEFENDED: + n_acd_probe_get_userdata(event->defended.probe, (void **) &info); + _LOGD("defended address %s from host %s", + _nm_utils_inet4_ntop(info->address, address_str), + (hwaddr_str = + nm_utils_hwaddr_ntoa(event->defended.sender, event->defended.n_sender))); + break; + case N_ACD_EVENT_CONFLICT: + n_acd_probe_get_userdata(event->conflict.probe, (void **) &info); + _LOGW("conflict for address %s detected with host %s on interface '%s'", + _nm_utils_inet4_ntop(info->address, address_str), + (hwaddr_str = + nm_utils_hwaddr_ntoa(event->defended.sender, event->defended.n_sender)), + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex)); + break; + default: + _LOGD("unhandled event '%s'", _acd_event_to_string_buf(event->event, to_string_buffer)); + break; + } + + if (check_probing_done && self->state == STATE_PROBING + && ++self->completed == g_hash_table_size(self->addresses)) { + self->state = STATE_PROBE_DONE; + emit_probe_terminated = TRUE; + } + } + + if (emit_probe_terminated) { + if (self->callbacks.probe_terminated_callback) { + self->callbacks.probe_terminated_callback(self, self->user_data); + } + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +acd_probe_add(NMAcdManager *self, AddressInfo *info, guint64 timeout) +{ + NAcdProbeConfig *probe_config; + int r; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + r = n_acd_probe_config_new(&probe_config); + if (r) { + _LOGW("could not create probe config for %s on interface '%s': %s", + _nm_utils_inet4_ntop(info->address, sbuf), + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + return FALSE; + } + + n_acd_probe_config_set_ip(probe_config, (struct in_addr){info->address}); + n_acd_probe_config_set_timeout(probe_config, timeout); + + r = n_acd_probe(self->acd, &info->probe, probe_config); + if (r) { + _LOGW("could not start probe for %s on interface '%s': %s", + _nm_utils_inet4_ntop(info->address, sbuf), + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + n_acd_probe_config_free(probe_config); + return FALSE; + } + + n_acd_probe_set_userdata(info->probe, info); + n_acd_probe_config_free(probe_config); + + return TRUE; +} + +static int +acd_init(NMAcdManager *self) +{ + NAcdConfig *config; + int r; + + if (self->acd) + return 0; + + r = n_acd_config_new(&config); + if (r) + return r; + + n_acd_config_set_ifindex(config, self->ifindex); + n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET); + n_acd_config_set_mac(config, self->hwaddr, ETH_ALEN); + + r = n_acd_new(&self->acd, config); + n_acd_config_free(config); + return r; +} + +/** + * nm_acd_manager_start_probe: + * @self: a #NMAcdManager + * @timeout: maximum probe duration in milliseconds + * @error: location to store error, or %NULL + * + * Start probing IP addresses for duplicates; when the probe terminates a + * PROBE_TERMINATED signal is emitted. + * + * Returns: 0 on success or a negative NetworkManager error code (NME_*). + */ +int +nm_acd_manager_start_probe(NMAcdManager *self, guint timeout) +{ + GHashTableIter iter; + AddressInfo * info; + gboolean success = FALSE; + int fd, r; + + g_return_val_if_fail(self, FALSE); + g_return_val_if_fail(self->state == STATE_INIT, FALSE); + + r = acd_init(self); + if (r) { + _LOGW("couldn't init ACD for probing on interface '%s': %s", + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + return acd_error_to_nmerr(r, TRUE); + } + + self->completed = 0; + + g_hash_table_iter_init(&iter, self->addresses); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info)) + success |= acd_probe_add(self, info, timeout); + + if (success) + self->state = STATE_PROBING; + + nm_assert(!self->event_source); + n_acd_get_fd(self->acd, &fd); + self->event_source = + nm_g_unix_fd_source_new(fd, G_IO_IN, G_PRIORITY_DEFAULT, acd_event, self, NULL); + g_source_attach(self->event_source, NULL); + + return success ? 0 : -NME_UNSPEC; +} + +/** + * nm_acd_manager_check_address: + * @self: a #NMAcdManager + * @address: an IP address + * + * Check if an IP address is duplicate. @address must have been added with + * nm_acd_manager_add_address(). + * + * Returns: %TRUE if the address is not duplicate, %FALSE otherwise + */ +gboolean +nm_acd_manager_check_address(NMAcdManager *self, in_addr_t address) +{ + AddressInfo *info; + + g_return_val_if_fail(self, FALSE); + g_return_val_if_fail(NM_IN_SET(self->state, STATE_INIT, STATE_PROBE_DONE), FALSE); + + info = g_hash_table_lookup(self->addresses, GUINT_TO_POINTER(address)); + g_return_val_if_fail(info, FALSE); + + return !info->duplicate; +} + +/** + * nm_acd_manager_announce_addresses: + * @self: a #NMAcdManager + * + * Start announcing addresses. + * + * Returns: a negative NetworkManager error number or zero on success. + */ +int +nm_acd_manager_announce_addresses(NMAcdManager *self) +{ + GHashTableIter iter; + AddressInfo * info; + int r; + int fd; + gboolean success = TRUE; + + r = acd_init(self); + if (r) { + _LOGW("couldn't init ACD for announcing addresses on interface '%s': %s", + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + return acd_error_to_nmerr(r, TRUE); + } + + if (self->state == STATE_INIT) { + /* n-acd can't announce without probing, therefore let's + * start a fake probe with zero timeout and then perform + * the announcement. */ + g_hash_table_iter_init(&iter, self->addresses); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info)) { + if (!acd_probe_add(self, info, 0)) + success = FALSE; + } + self->state = STATE_ANNOUNCING; + } else if (self->state == STATE_ANNOUNCING) { + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + g_hash_table_iter_init(&iter, self->addresses); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info)) { + if (info->duplicate) + continue; + r = n_acd_probe_announce(info->probe, N_ACD_DEFEND_ONCE); + if (r) { + _LOGW("couldn't announce address %s on interface '%s': %s", + _nm_utils_inet4_ntop(info->address, sbuf), + nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex), + acd_error_to_string(r)); + success = FALSE; + } else + _LOGD("announcing address %s", _nm_utils_inet4_ntop(info->address, sbuf)); + } + } + + if (!self->event_source) { + n_acd_get_fd(self->acd, &fd); + self->event_source = + nm_g_unix_fd_source_new(fd, G_IO_IN, G_PRIORITY_DEFAULT, acd_event, self, NULL); + g_source_attach(self->event_source, NULL); + } + + return success ? 0 : -NME_UNSPEC; +} + +static void +destroy_address_info(gpointer data) +{ + AddressInfo *info = (AddressInfo *) data; + + n_acd_probe_free(info->probe); + + g_slice_free(AddressInfo, info); +} + +/*****************************************************************************/ + +NMAcdManager * +nm_acd_manager_new(int ifindex, + const guint8 * hwaddr, + guint hwaddr_len, + const NMAcdCallbacks *callbacks, + gpointer user_data) +{ + NMAcdManager *self; + + g_return_val_if_fail(ifindex > 0, NULL); + g_return_val_if_fail(hwaddr, NULL); + g_return_val_if_fail(hwaddr_len == ETH_ALEN, NULL); + + self = g_slice_new0(NMAcdManager); + + if (callbacks) + self->callbacks = *callbacks; + self->user_data = user_data; + + self->addresses = g_hash_table_new_full(nm_direct_hash, NULL, NULL, destroy_address_info); + self->state = STATE_INIT; + self->ifindex = ifindex; + memcpy(self->hwaddr, hwaddr, ETH_ALEN); + return self; +} + +void +nm_acd_manager_free(NMAcdManager *self) +{ + g_return_if_fail(self); + + if (self->callbacks.user_data_destroy) + self->callbacks.user_data_destroy(self->user_data); + + nm_clear_pointer(&self->addresses, g_hash_table_destroy); + nm_clear_g_source_inst(&self->event_source); + nm_clear_pointer(&self->acd, n_acd_unref); + + g_slice_free(NMAcdManager, self); +} diff --git a/src/core/devices/nm-acd-manager.h b/src/core/devices/nm-acd-manager.h new file mode 100644 index 00000000..e8ef6f2b --- /dev/null +++ b/src/core/devices/nm-acd-manager.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 - 2018 Red Hat, Inc. + */ + +#ifndef __NM_ACD_MANAGER__ +#define __NM_ACD_MANAGER__ + +#include <netinet/in.h> + +typedef struct _NMAcdManager NMAcdManager; + +typedef struct { + void (*probe_terminated_callback)(NMAcdManager *self, gpointer user_data); + GDestroyNotify user_data_destroy; +} NMAcdCallbacks; + +NMAcdManager *nm_acd_manager_new(int ifindex, + const guint8 * hwaddr, + guint hwaddr_len, + const NMAcdCallbacks *callbacks, + gpointer user_data); + +void nm_acd_manager_free(NMAcdManager *self); + +gboolean nm_acd_manager_add_address(NMAcdManager *self, in_addr_t address); +int nm_acd_manager_start_probe(NMAcdManager *self, guint timeout); +gboolean nm_acd_manager_check_address(NMAcdManager *self, in_addr_t address); +int nm_acd_manager_announce_addresses(NMAcdManager *self); + +NM_AUTO_DEFINE_FCN0(NMAcdManager *, _nm_auto_free_acdmgr, nm_acd_manager_free); +#define nm_auto_free_acdmgr nm_auto(_nm_auto_free_acdmgr) + +#endif /* __NM_ACD_MANAGER__ */ diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c new file mode 100644 index 00000000..fe116dd4 --- /dev/null +++ b/src/core/devices/nm-device-6lowpan.c @@ -0,0 +1,339 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-6lowpan.h" + +#include "nm-device-private.h" +#include "settings/nm-settings.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-6lowpan.h" +#include "nm-utils.h" + +#define _NMLOG_DEVICE_TYPE NMDevice6Lowpan +#include "nm-device-logging.h" + +/*****************************************************************************/ + +typedef struct { + gulong parent_state_id; +} NMDevice6LowpanPrivate; + +struct _NMDevice6Lowpan { + NMDevice parent; + NMDevice6LowpanPrivate _priv; +}; + +struct _NMDevice6LowpanClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDevice6Lowpan, nm_device_6lowpan, NM_TYPE_DEVICE) + +#define NM_DEVICE_6LOWPAN_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDevice6Lowpan, NM_IS_DEVICE_6LOWPAN, NMDevice) + +/*****************************************************************************/ + +static void +parent_state_changed(NMDevice * parent, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason, + gpointer user_data) +{ + NMDevice6Lowpan *self = NM_DEVICE_6LOWPAN(user_data); + + nm_device_set_unmanaged_by_flags(NM_DEVICE(self), + NM_UNMANAGED_PARENT, + !nm_device_get_managed(parent, FALSE), + reason); +} + +static void +parent_changed_notify(NMDevice *device, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + NMDevice6Lowpan * self = NM_DEVICE_6LOWPAN(device); + NMDevice6LowpanPrivate *priv = NM_DEVICE_6LOWPAN_GET_PRIVATE(self); + + NM_DEVICE_CLASS(nm_device_6lowpan_parent_class) + ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent); + + /* note that @self doesn't have to clear @parent_state_id on dispose, + * because NMDevice's dispose() will unset the parent, which in turn calls + * parent_changed_notify(). */ + nm_clear_g_signal_handler(old_parent, &priv->parent_state_id); + + if (new_parent) { + priv->parent_state_id = g_signal_connect(new_parent, + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(parent_state_changed), + device); + + /* Set parent-dependent unmanaged flag */ + nm_device_set_unmanaged_by_flags(device, + NM_UNMANAGED_PARENT, + !nm_device_get_managed(new_parent, FALSE), + NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED); + } + + if (new_ifindex > 0) { + /* Recheck availability now that the parent has changed */ + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_PARENT_CHANGED, + NM_DEVICE_STATE_REASON_PARENT_CHANGED); + } +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSetting6Lowpan *s_6lowpan; + int parent_ifindex; + int r; + + s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN)); + g_return_val_if_fail(s_6lowpan, FALSE); + + parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0; + + if (parent_ifindex <= 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "6LoWPAN devices can not be created without a parent interface"); + g_return_val_if_fail(!parent, FALSE); + return FALSE; + } + + r = nm_platform_link_6lowpan_add(nm_device_get_platform(device), + iface, + parent_ifindex, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create 6lowpan interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + nm_device_parent_set_ifindex(device, parent_ifindex); + + return TRUE; +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NMDevice6Lowpan *self = NM_DEVICE_6LOWPAN(device); + int parent = 0; + int ifindex; + + NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)->link_changed(device, pllink); + + ifindex = nm_device_get_ifindex(device); + if (!nm_platform_link_6lowpan_get_properties(nm_device_get_platform(device), + ifindex, + &parent)) { + _LOGW(LOGD_DEVICE, "could not get 6lowpan properties"); + return; + } + + nm_device_parent_set_ifindex(device, parent); +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + if (!nm_device_parent_get_device(device)) + return FALSE; + return NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)->is_available(device, flags); +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSetting6Lowpan *s_6lowpan; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_6LOWPAN_SETTING_NAME, + existing_connections, + NULL, + _("6LOWPAN connection"), + NULL, + NULL, + TRUE); + + s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN)); + if (!s_6lowpan) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A '6lowpan' setting is required."); + return FALSE; + } + + /* If there's no 6LoWPAN interface, no parent, and no hardware address in the + * settings, then there's not enough information to complete the setting. + */ + if (!nm_setting_6lowpan_get_parent(s_6lowpan) + && !nm_device_match_parent_hwaddr(device, connection, TRUE)) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "The '6lowpan' setting had no interface name, parent, or hardware address."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMSetting6Lowpan *s_6lowpan = + NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN)); + + if (!s_6lowpan) { + s_6lowpan = (NMSetting6Lowpan *) nm_setting_6lowpan_new(); + nm_connection_add_setting(connection, (NMSetting *) s_6lowpan); + } + + g_object_set( + s_6lowpan, + NM_SETTING_6LOWPAN_PARENT, + nm_device_parent_find_for_connection(device, nm_setting_6lowpan_get_parent(s_6lowpan)), + NULL); +} + +/*****************************************************************************/ + +static void +nm_device_6lowpan_init(NMDevice6Lowpan *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_6lowpan = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_6LOWPAN, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Parent", "o", NM_DEVICE_PARENT), ), ), +}; + +static void +nm_device_6lowpan_class_init(NMDevice6LowpanClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_6lowpan); + + device_class->connection_type_supported = NM_SETTING_6LOWPAN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_6LOWPAN_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_6LOWPAN); + + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->complete_connection = complete_connection; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + device_class->link_changed = link_changed; + device_class->is_available = is_available; + device_class->parent_changed_notify = parent_changed_notify; + device_class->update_connection = update_connection; +} + +/*****************************************************************************/ + +#define NM_TYPE_6LOWPAN_DEVICE_FACTORY (nm_6lowpan_device_factory_get_type()) +#define NM_6LOWPAN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_6LOWPAN_DEVICE_FACTORY, NM6LowpanDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_6LOWPAN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "6LoWPAN", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_6LOWPAN, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_6LOWPAN, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSetting6Lowpan *s_6lowpan; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_6LOWPAN_SETTING_NAME), NULL); + + s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN)); + g_assert(s_6lowpan); + + return nm_setting_6lowpan_get_parent(s_6lowpan); +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + NMSetting6Lowpan *s_6lowpan; + const char * ifname; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_6LOWPAN_SETTING_NAME), NULL); + + s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN)); + g_assert(s_6lowpan); + + if (!parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + return g_strdup(ifname); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + 6LOWPAN, + 6Lowpan, + 6lowpan, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_6LOWPAN) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_6LOWPAN_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-6lowpan.h b/src/core/devices/nm-device-6lowpan.h new file mode 100644 index 00000000..34ea3c8d --- /dev/null +++ b/src/core/devices/nm-device-6lowpan.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_6LOWPAN_H__ +#define __NETWORKMANAGER_DEVICE_6LOWPAN_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_6LOWPAN (nm_device_6lowpan_get_type()) +#define NM_DEVICE_6LOWPAN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6Lowpan)) +#define NM_DEVICE_6LOWPAN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_6LOWPAN, NMDevice6LowpanClass)) +#define NM_IS_DEVICE_6LOWPAN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_6LOWPAN)) +#define NM_IS_DEVICE_6LOWPAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_6LOWPAN)) +#define NM_DEVICE_6LOWPAN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6LowpanClass)) + +typedef struct _NMDevice6Lowpan NMDevice6Lowpan; +typedef struct _NMDevice6LowpanClass NMDevice6LowpanClass; + +GType nm_device_6lowpan_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_6LOWPAN_H__ */ diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c new file mode 100644 index 00000000..f68c080b --- /dev/null +++ b/src/core/devices/nm-device-bond.c @@ -0,0 +1,651 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-bond.h" + +#include <stdlib.h> +#include <net/if.h> + +#include "NetworkManagerUtils.h" +#include "nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-core-internal.h" +#include "nm-ip4-config.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceBond +#include "nm-device-logging.h" + +/*****************************************************************************/ + +#define OPTIONS_APPLY_SUBSET \ + NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY, \ + NM_SETTING_BOND_OPTION_DOWNDELAY, NM_SETTING_BOND_OPTION_ARP_INTERVAL, \ + NM_SETTING_BOND_OPTION_ARP_VALIDATE, NM_SETTING_BOND_OPTION_PRIMARY, \ + NM_SETTING_BOND_OPTION_AD_ACTOR_SYSTEM, NM_SETTING_BOND_OPTION_AD_ACTOR_SYS_PRIO, \ + NM_SETTING_BOND_OPTION_AD_SELECT, NM_SETTING_BOND_OPTION_AD_USER_PORT_KEY, \ + NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE, NM_SETTING_BOND_OPTION_ARP_ALL_TARGETS, \ + NM_SETTING_BOND_OPTION_FAIL_OVER_MAC, NM_SETTING_BOND_OPTION_LACP_RATE, \ + NM_SETTING_BOND_OPTION_LP_INTERVAL, NM_SETTING_BOND_OPTION_MIN_LINKS, \ + NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \ + NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_TLB_DYNAMIC_LB, \ + NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY, \ + NM_SETTING_BOND_OPTION_NUM_GRAT_ARP + +#define OPTIONS_REAPPLY_SUBSET \ + NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY, \ + NM_SETTING_BOND_OPTION_DOWNDELAY, NM_SETTING_BOND_OPTION_ARP_INTERVAL, \ + NM_SETTING_BOND_OPTION_ARP_VALIDATE, NM_SETTING_BOND_OPTION_PRIMARY, \ + NM_SETTING_BOND_OPTION_AD_ACTOR_SYSTEM, NM_SETTING_BOND_OPTION_AD_ACTOR_SYS_PRIO, \ + NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE, NM_SETTING_BOND_OPTION_ARP_ALL_TARGETS, \ + NM_SETTING_BOND_OPTION_FAIL_OVER_MAC, NM_SETTING_BOND_OPTION_LP_INTERVAL, \ + NM_SETTING_BOND_OPTION_MIN_LINKS, NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, \ + NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, NM_SETTING_BOND_OPTION_RESEND_IGMP, \ + NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY, \ + NM_SETTING_BOND_OPTION_NUM_GRAT_ARP + +#define OPTIONS_REAPPLY_FULL \ + OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \ + NM_SETTING_BOND_OPTION_ARP_IP_TARGET + +/*****************************************************************************/ + +struct _NMDeviceBond { + NMDevice parent; +}; + +struct _NMDeviceBondClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceBond, nm_device_bond, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingBond *s_bond; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_BOND_SETTING_NAME, + existing_connections, + NULL, + _("Bond connection"), + "bond", + NULL, + TRUE); + + s_bond = nm_connection_get_setting_bond(connection); + if (!s_bond) { + s_bond = (NMSettingBond *) nm_setting_bond_new(); + nm_connection_add_setting(connection, NM_SETTING(s_bond)); + } + + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +_set_bond_attr(NMDevice *device, const char *attr, const char *value) +{ + NMDeviceBond *self = NM_DEVICE_BOND(device); + int ifindex = nm_device_get_ifindex(device); + gboolean ret; + + ret = + nm_platform_sysctl_master_set_option(nm_device_get_platform(device), ifindex, attr, value); + if (!ret) + _LOGW(LOGD_PLATFORM, "failed to set bonding attribute '%s' to '%s'", attr, value); + return ret; +} + +#define _set_bond_attr_take(device, attr, value) \ + G_STMT_START \ + { \ + gs_free char *_tmp = (value); \ + \ + _set_bond_attr(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, _tmp); \ + } \ + G_STMT_END + +#define _set_bond_attr_printf(device, attr, fmt, ...) \ + _set_bond_attr_take((device), (attr), g_strdup_printf(fmt, __VA_ARGS__)) + +static gboolean +ignore_option(NMSettingBond *s_bond, const char *option, const char *value) +{ + const char *defvalue; + + if (nm_streq0(option, NM_SETTING_BOND_OPTION_MIIMON)) { + /* The default value for miimon, when missing in the setting, is + * 0 if arp_interval is != 0, and 100 otherwise. So, let's ignore + * miimon=0 (which means that miimon is disabled) and accept any + * other value. Adding miimon=100 does not cause any harm. + */ + defvalue = "0"; + } else + defvalue = nm_setting_bond_get_option_default(s_bond, option); + + return nm_streq0(value, defvalue); +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceBond * self = NM_DEVICE_BOND(device); + NMSettingBond *s_bond = nm_connection_get_setting_bond(connection); + int ifindex = nm_device_get_ifindex(device); + NMBondMode mode = NM_BOND_MODE_UNKNOWN; + const char ** options; + + if (!s_bond) { + s_bond = (NMSettingBond *) nm_setting_bond_new(); + nm_connection_add_setting(connection, (NMSetting *) s_bond); + } + + /* Read bond options from sysfs and update the Bond setting to match */ + options = nm_setting_bond_get_valid_options(NULL); + for (; options[0]; options++) { + const char * option = options[0]; + gs_free char *value = NULL; + char * p; + + if (NM_IN_STRSET(option, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE)) + continue; + + value = + nm_platform_sysctl_master_get_option(nm_device_get_platform(device), ifindex, option); + + if (value && _nm_setting_bond_get_option_type(s_bond, option) == NM_BOND_OPTION_TYPE_BOTH) { + p = strchr(value, ' '); + if (p) + *p = '\0'; + } + + if (mode == NM_BOND_MODE_UNKNOWN) { + if (value && nm_streq(option, NM_SETTING_BOND_OPTION_MODE)) + mode = _nm_setting_bond_mode_from_string(value); + if (mode == NM_BOND_MODE_UNKNOWN) + continue; + } + + if (!_nm_setting_bond_option_supported(option, mode)) + continue; + + if (value && value[0] && !ignore_option(s_bond, option, value)) { + /* Replace " " with "," for arp_ip_targets from the kernel */ + if (nm_streq(option, NM_SETTING_BOND_OPTION_ARP_IP_TARGET)) { + for (p = value; *p; p++) { + if (*p == ' ') + *p = ','; + } + } + + if (!_nm_setting_bond_validate_option(option, value, NULL)) + _LOGT(LOGD_BOND, "cannot set invalid bond option '%s' = '%s'", option, value); + else + nm_setting_bond_add_option(s_bond, option, value); + } + } +} + +static gboolean +master_update_slave_connection(NMDevice * self, + NMDevice * slave, + NMConnection *connection, + GError ** error) +{ + g_object_set(nm_connection_get_setting_connection(connection), + NM_SETTING_CONNECTION_MASTER, + nm_device_get_iface(self), + NM_SETTING_CONNECTION_SLAVE_TYPE, + NM_SETTING_BOND_SETTING_NAME, + NULL); + return TRUE; +} + +static void +set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new_arp_ip_target) +{ + gs_unref_ptrarray GPtrArray *free_list = NULL; + gs_free const char ** cur_strv = NULL; + gs_free const char ** new_strv = NULL; + gsize cur_len; + gsize new_len; + gsize i; + gsize j; + + cur_strv = nm_utils_strsplit_set_full(cur_arp_ip_target, + NM_ASCII_SPACES, + NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP); + new_strv = nm_utils_bond_option_arp_ip_targets_split(new_arp_ip_target); + + cur_len = NM_PTRARRAY_LEN(cur_strv); + new_len = NM_PTRARRAY_LEN(new_strv); + + if (new_len > 0) { + for (j = 0, i = 0; i < new_len; i++) { + const char *s; + in_addr_t a4; + + s = new_strv[i]; + if (nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &a4)) { + char sbuf[INET_ADDRSTRLEN]; + + _nm_utils_inet4_ntop(a4, sbuf); + if (!nm_streq(s, sbuf)) { + if (!free_list) + free_list = g_ptr_array_new_with_free_func(g_free); + s = g_strdup(sbuf); + g_ptr_array_add(free_list, (gpointer) s); + } + } + + if (nm_utils_strv_find_first((char **) new_strv, i, s) < 0) + new_strv[j++] = s; + } + new_strv[j] = NULL; + new_len = j; + } + + if (cur_len == 0 && new_len == 0) + return; + + if (nm_utils_strv_equal(cur_strv, new_strv)) + return; + + for (i = 0; i < cur_len; i++) + _set_bond_attr_printf(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, "-%s", cur_strv[i]); + for (i = 0; i < new_len; i++) + _set_bond_attr_printf(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, "+%s", new_strv[i]); +} + +/* + * Sets bond attribute stored in the option hashtable or + * the default value if no value was set. + */ +static void +set_bond_attr_or_default(NMDevice *device, NMSettingBond *s_bond, const char *opt) +{ + NMDeviceBond *self = NM_DEVICE_BOND(device); + const char * value; + + value = nm_setting_bond_get_option_or_default(s_bond, opt); + if (!value) { + if (_LOGT_ENABLED(LOGD_BOND) && nm_setting_bond_get_option_by_name(s_bond, opt)) + _LOGT(LOGD_BOND, "bond option '%s' not set as it conflicts with other options", opt); + return; + } + + _set_bond_attr(device, opt, value); +} + +static void +set_bond_attrs_or_default(NMDevice *device, NMSettingBond *s_bond, const char *const *attr_v) +{ + nm_assert(NM_IS_DEVICE(device)); + nm_assert(s_bond); + nm_assert(attr_v); + + for (; *attr_v; ++attr_v) + set_bond_attr_or_default(device, s_bond, *attr_v); +} + +static void +set_bond_arp_ip_targets(NMDevice *device, NMSettingBond *s_bond) +{ + int ifindex = nm_device_get_ifindex(device); + gs_free char *cur_arp_ip_target = NULL; + + /* ARP targets: clear and initialize the list */ + cur_arp_ip_target = nm_platform_sysctl_master_get_option(nm_device_get_platform(device), + ifindex, + NM_SETTING_BOND_OPTION_ARP_IP_TARGET); + set_arp_targets( + device, + cur_arp_ip_target, + nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_ARP_IP_TARGET)); +} + +static gboolean +apply_bonding_config(NMDeviceBond *self) +{ + NMDevice * device = NM_DEVICE(self); + NMSettingBond *s_bond; + NMBondMode mode; + const char * mode_str; + gs_free char * device_bond_mode = NULL; + + s_bond = nm_device_get_applied_setting(device, NM_TYPE_SETTING_BOND); + g_return_val_if_fail(s_bond, FALSE); + + mode_str = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE); + mode = _nm_setting_bond_mode_from_string(mode_str); + g_return_val_if_fail(mode != NM_BOND_MODE_UNKNOWN, FALSE); + + /* Set mode first, as some other options (e.g. arp_interval) are valid + * only for certain modes. + */ + device_bond_mode = nm_platform_sysctl_master_get_option(nm_device_get_platform(device), + nm_device_get_ifindex(device), + NM_SETTING_BOND_OPTION_MODE); + /* Need to release all slaves before we can change bond mode */ + if (!nm_streq0(device_bond_mode, mode_str)) + nm_device_master_release_slaves(device); + + set_bond_attr_or_default(device, s_bond, NM_SETTING_BOND_OPTION_MODE); + + set_bond_arp_ip_targets(device, s_bond); + + set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_APPLY_SUBSET)); + return TRUE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceBond * self = NM_DEVICE_BOND(device); + NMActStageReturn ret = NM_ACT_STAGE_RETURN_SUCCESS; + + /* Interface must be down to set bond options */ + nm_device_take_down(device, TRUE); + if (!apply_bonding_config(self)) + ret = NM_ACT_STAGE_RETURN_FAILURE; + else { + if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) + ret = NM_ACT_STAGE_RETURN_FAILURE; + } + nm_device_bring_up(device, TRUE, NULL); + + return ret; +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + NMDeviceBond *self = NM_DEVICE_BOND(device); + + nm_device_master_check_slave_physical_port(device, slave, LOGD_BOND); + + if (configure) { + gboolean success; + + nm_device_take_down(slave, TRUE); + success = nm_platform_link_enslave(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + nm_device_get_ip_ifindex(slave)); + nm_device_bring_up(slave, TRUE, NULL); + + if (!success) { + _LOGI(LOGD_BOND, "enslaved bond slave %s: failed", nm_device_get_ip_iface(slave)); + return FALSE; + } + + _LOGI(LOGD_BOND, "enslaved bond slave %s", nm_device_get_ip_iface(slave)); + } else + _LOGI(LOGD_BOND, "bond slave %s was enslaved", nm_device_get_ip_iface(slave)); + + return TRUE; +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{ + NMDeviceBond *self = NM_DEVICE_BOND(device); + gboolean success; + gs_free char *address = NULL; + int ifindex_slave; + int ifindex; + + if (configure) { + ifindex = nm_device_get_ifindex(device); + if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex)) + configure = FALSE; + } + + ifindex_slave = nm_device_get_ip_ifindex(slave); + + if (ifindex_slave <= 0) + _LOGD(LOGD_BOND, "bond slave %s is already released", nm_device_get_ip_iface(slave)); + + if (configure) { + /* When the last slave is released the bond MAC will be set to a random + * value by kernel; remember the current one and restore it afterwards. + */ + address = g_strdup(nm_device_get_hw_address(device)); + + if (ifindex_slave > 0) { + success = nm_platform_link_release(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + ifindex_slave); + + if (success) { + _LOGI(LOGD_BOND, "released bond slave %s", nm_device_get_ip_iface(slave)); + } else { + _LOGW(LOGD_BOND, "failed to release bond slave %s", nm_device_get_ip_iface(slave)); + } + } + + nm_platform_process_events(nm_device_get_platform(device)); + if (nm_device_update_hw_address(device)) + nm_device_hw_addr_set(device, address, "restore", FALSE); + + /* Kernel bonding code "closes" the slave when releasing it, (which clears + * IFF_UP), so we must bring it back up here to ensure carrier changes and + * other state is noticed by the now-released slave. + */ + if (ifindex_slave > 0) { + if (!nm_device_bring_up(slave, TRUE, NULL)) + _LOGW(LOGD_BOND, "released bond slave could not be brought up."); + } + } else { + if (ifindex_slave > 0) { + _LOGI(LOGD_BOND, "bond slave %s was released", nm_device_get_ip_iface(slave)); + } + } +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char *iface = nm_device_get_iface(device); + int r; + + g_assert(iface); + + r = nm_platform_link_bond_add(nm_device_get_platform(device), iface, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create bond interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + return TRUE; +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + /* Only handle bond setting here, delegate other settings to parent class */ + if (nm_streq(setting_name, NM_SETTING_BOND_SETTING_NAME)) { + NMSettingBond *s_a = NM_SETTING_BOND(s_old); + NMSettingBond *s_b = NM_SETTING_BOND(s_new); + const char ** option_list; + + if (!nm_device_hash_check_invalid_keys(diffs, + NM_SETTING_BOND_SETTING_NAME, + error, + NM_SETTING_BOND_OPTIONS)) + return FALSE; + + option_list = nm_setting_bond_get_valid_options(NULL); + + for (; *option_list; ++option_list) { + const char *name = *option_list; + + /* We support changes to these */ + if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL)) + continue; + + /* Reject any other changes */ + if (!nm_streq0(nm_setting_bond_get_option_normalized(s_a, name), + nm_setting_bond_get_option_normalized(s_b, name))) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Can't reapply '%s' bond option", + name); + return FALSE; + } + } + + return TRUE; + } + + device_class = NM_DEVICE_CLASS(nm_device_bond_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static void +reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new) +{ + NMDeviceBond * self = NM_DEVICE_BOND(device); + NMSettingBond *s_bond; + const char * value; + NMBondMode mode; + + NM_DEVICE_CLASS(nm_device_bond_parent_class)->reapply_connection(device, con_old, con_new); + + _LOGD(LOGD_BOND, "reapplying bond settings"); + s_bond = nm_connection_get_setting_bond(con_new); + g_return_if_fail(s_bond); + + value = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE); + mode = _nm_setting_bond_mode_from_string(value); + g_return_if_fail(mode != NM_BOND_MODE_UNKNOWN); + + /* Below we set only the bond options that kernel allows to modify + * while keeping the bond interface up */ + + set_bond_arp_ip_targets(device, s_bond); + + set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_REAPPLY_SUBSET)); +} + +/*****************************************************************************/ + +static void +nm_device_bond_init(NMDeviceBond *self) +{ + nm_assert(nm_device_is_master(NM_DEVICE(self))); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_bond = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_BOND, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves", + "ao", + NM_DEVICE_SLAVES), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_bond_class_init(NMDeviceBondClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bond); + + device_class->connection_type_supported = NM_SETTING_BOND_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_BOND_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BOND); + + device_class->is_master = TRUE; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->complete_connection = complete_connection; + + device_class->update_connection = update_connection; + device_class->master_update_slave_connection = master_update_slave_connection; + + device_class->create_and_realize = create_and_realize; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + device_class->can_reapply_change = can_reapply_change; + device_class->reapply_connection = reapply_connection; +} + +/*****************************************************************************/ + +#define NM_TYPE_BOND_DEVICE_FACTORY (nm_bond_device_factory_get_type()) +#define NM_BOND_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BOND_DEVICE_FACTORY, NMBondDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_BOND, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_DRIVER, + "bonding", + NM_DEVICE_TYPE_DESC, + "Bond", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_BOND, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_BOND, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + BOND, + Bond, + bond, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_BOND) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BOND_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-bond.h b/src/core/devices/nm-device-bond.h new file mode 100644 index 00000000..19301793 --- /dev/null +++ b/src/core/devices/nm-device-bond.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2012 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_BOND_H__ +#define __NETWORKMANAGER_DEVICE_BOND_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_BOND (nm_device_bond_get_type()) +#define NM_DEVICE_BOND(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BOND, NMDeviceBond)) +#define NM_DEVICE_BOND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BOND, NMDeviceBondClass)) +#define NM_IS_DEVICE_BOND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BOND)) +#define NM_IS_DEVICE_BOND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BOND)) +#define NM_DEVICE_BOND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BOND, NMDeviceBondClass)) + +typedef struct _NMDeviceBond NMDeviceBond; +typedef struct _NMDeviceBondClass NMDeviceBondClass; + +GType nm_device_bond_get_type(void); + +#endif /* NM_DEVICE_BOND_H */ diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c new file mode 100644 index 00000000..c919d85d --- /dev/null +++ b/src/core/devices/nm-device-bridge.c @@ -0,0 +1,1246 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-bridge.h" + +#include <stdlib.h> +#include <linux/if_ether.h> + +#include "NetworkManagerUtils.h" +#include "nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceBridge +#include "nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceBridge { + NMDevice parent; + GCancellable *bt_cancellable; + bool vlan_configured : 1; + bool bt_registered : 1; +}; + +struct _NMDeviceBridgeClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceBridge, nm_device_bridge, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +const NMBtVTableNetworkServer *nm_bt_vtable_network_server = NULL; + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + NMSettingBluetooth *s_bt; + + if (!NM_DEVICE_CLASS(nm_device_bridge_parent_class) + ->check_connection_available(device, connection, flags, specific_object, error)) + return FALSE; + + s_bt = _nm_connection_get_setting_bluetooth_for_nap(connection); + if (s_bt) { + const char *bdaddr; + + if (!nm_bt_vtable_network_server) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "bluetooth plugin not available to activate NAP profile"); + return FALSE; + } + + bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt); + if (!nm_bt_vtable_network_server->is_available( + nm_bt_vtable_network_server, + bdaddr, + (self->bt_cancellable || self->bt_registered) ? device : NULL)) { + if (bdaddr) + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "no suitable NAP device \"%s\" available", + bdaddr); + else + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "no suitable NAP device available"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingBridge *s_bridge; + const char * mac_address; + + if (!NM_DEVICE_CLASS(nm_device_bridge_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME) + && _nm_connection_get_setting_bluetooth_for_nap(connection)) { + s_bridge = nm_connection_get_setting_bridge(connection); + if (!s_bridge) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "missing bridge setting for bluetooth NAP profile"); + return FALSE; + } + + /* a bluetooth NAP connection is handled by the bridge. + * + * Proceed... */ + } else { + s_bridge = + _nm_connection_check_main_setting(connection, NM_SETTING_BRIDGE_SETTING_NAME, error); + if (!s_bridge) + return FALSE; + } + + mac_address = nm_setting_bridge_get_mac_address(s_bridge); + if (mac_address && nm_device_is_real(device)) { + const char *hw_addr; + + hw_addr = nm_device_get_hw_address(device); + if (!hw_addr || !nm_utils_hwaddr_matches(hw_addr, -1, mac_address, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "mac address mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingBridge *s_bridge; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_BRIDGE_SETTING_NAME, + existing_connections, + NULL, + _("Bridge connection"), + "bridge", + NULL, + TRUE); + + s_bridge = nm_connection_get_setting_bridge(connection); + if (!s_bridge) { + s_bridge = (NMSettingBridge *) nm_setting_bridge_new(); + nm_connection_add_setting(connection, NM_SETTING(s_bridge)); + } + + return TRUE; +} + +static void +to_sysfs_group_address_sys(const char *group_address, NMEtherAddr *out_addr) +{ + if (group_address == NULL) { + *out_addr = NM_ETHER_ADDR_INIT(NM_BRIDGE_GROUP_ADDRESS_DEF_BIN); + return; + } + if (!nm_utils_hwaddr_aton(group_address, out_addr, ETH_ALEN)) + nm_assert_not_reached(); +} + +static void +from_sysfs_group_address(const char *value, GValue *out) +{ + if (!nm_utils_hwaddr_matches(value, -1, NM_BRIDGE_GROUP_ADDRESS_DEF_STR, -1)) + g_value_set_string(out, value); +} + +static const char * +to_sysfs_group_address(GValue *value) +{ + return g_value_get_string(value) ?: NM_BRIDGE_GROUP_ADDRESS_DEF_STR; +} + +static int +to_sysfs_vlan_protocol_sys(const char *value) +{ + if (nm_streq0(value, "802.1ad")) + return ETH_P_8021AD; + + return ETH_P_8021Q; +} + +static void +from_sysfs_vlan_protocol(const char *value, GValue *out) +{ + switch (_nm_utils_ascii_str_to_uint64(value, 16, 0, G_MAXUINT, -1)) { + case ETH_P_8021Q: + /* default value */ + break; + case ETH_P_8021AD: + g_value_set_string(out, "802.1ad"); + break; + } +} + +static const char * +to_sysfs_vlan_protocol(GValue *value) +{ + const char *str = g_value_get_string(value); + + if (nm_streq0(str, "802.1ad")) { + G_STATIC_ASSERT_EXPR(ETH_P_8021AD == 0x88A8); + return "0x88A8"; + } + + G_STATIC_ASSERT_EXPR(ETH_P_8021Q == 0x8100); + return "0x8100"; +} + +static int +to_sysfs_multicast_router_sys(const char *value) +{ + if (nm_streq0(value, "disabled")) + return 0; + if (nm_streq0(value, "auto")) + return 1; + if (nm_streq0(value, "enabled")) + return 2; + + return 1; +} + +static const char * +to_sysfs_multicast_router(GValue *value) +{ + const char *str = g_value_get_string(value); + + if (nm_streq0(str, "disabled")) + return "0"; + if (nm_streq0(str, "auto")) + return "1"; + if (nm_streq0(str, "enabled")) + return "2"; + + return "1"; +} + +static void +from_sysfs_multicast_router(const char *value, GValue *out) +{ + switch (_nm_utils_ascii_str_to_uint64(value, 10, 0, G_MAXUINT, -1)) { + case 0: + g_value_set_string(out, "disabled"); + break; + case 2: + g_value_set_string(out, "enabled"); + break; + case 1: + default: + /* default value */ + break; + } +} + +/*****************************************************************************/ +#define _DEFAULT_IF_ZERO(val, def_val) \ + ({ \ + typeof(val) _val = (val); \ + typeof(val) _def_val = (def_val); \ + \ + (_val == 0) ? _def_val : _val; \ + }) + +typedef struct { + const char *name; + const char *sysname; + const char *(*to_sysfs)(GValue *value); + void (*from_sysfs)(const char *value, GValue *out); + guint64 nm_min; + guint64 nm_max; + guint64 nm_default; + bool default_if_zero; + bool user_hz_compensate; + bool only_with_stp; +} Option; + +#define OPTION(_name, _sysname, ...) \ + { \ + .name = ""_name \ + "", \ + .sysname = ""_sysname \ + "", \ + __VA_ARGS__ \ + } + +#define OPTION_TYPE_INT(min, max, def) .nm_min = (min), .nm_max = (max), .nm_default = (def) + +#define OPTION_TYPE_BOOL(def) OPTION_TYPE_INT(FALSE, TRUE, def) + +#define OPTION_TYPE_TOFROM(to, fro) .to_sysfs = (to), .from_sysfs = (fro) + +static const Option master_options[] = { + OPTION(NM_SETTING_BRIDGE_STP, /* this must stay as the first item */ + "stp_state", + OPTION_TYPE_BOOL(NM_BRIDGE_STP_DEF), ), + OPTION(NM_SETTING_BRIDGE_PRIORITY, + "priority", + OPTION_TYPE_INT(NM_BRIDGE_PRIORITY_MIN, NM_BRIDGE_PRIORITY_MAX, NM_BRIDGE_PRIORITY_DEF), + .default_if_zero = TRUE, + .only_with_stp = TRUE, ), + OPTION(NM_SETTING_BRIDGE_FORWARD_DELAY, + "forward_delay", + OPTION_TYPE_INT(NM_BRIDGE_FORWARD_DELAY_MIN, + NM_BRIDGE_FORWARD_DELAY_MAX, + NM_BRIDGE_FORWARD_DELAY_DEF), + .default_if_zero = TRUE, + .user_hz_compensate = TRUE, + .only_with_stp = TRUE, ), + OPTION(NM_SETTING_BRIDGE_HELLO_TIME, + "hello_time", + OPTION_TYPE_INT(NM_BRIDGE_HELLO_TIME_MIN, + NM_BRIDGE_HELLO_TIME_MAX, + NM_BRIDGE_HELLO_TIME_DEF), + .default_if_zero = TRUE, + .user_hz_compensate = TRUE, + .only_with_stp = TRUE, ), + OPTION(NM_SETTING_BRIDGE_MAX_AGE, + "max_age", + OPTION_TYPE_INT(NM_BRIDGE_MAX_AGE_MIN, NM_BRIDGE_MAX_AGE_MAX, NM_BRIDGE_MAX_AGE_DEF), + .default_if_zero = TRUE, + .user_hz_compensate = TRUE, + .only_with_stp = TRUE, ), + OPTION(NM_SETTING_BRIDGE_AGEING_TIME, + "ageing_time", + OPTION_TYPE_INT(NM_BRIDGE_AGEING_TIME_MIN, + NM_BRIDGE_AGEING_TIME_MAX, + NM_BRIDGE_AGEING_TIME_DEF), + .default_if_zero = TRUE, + .user_hz_compensate = TRUE, ), + OPTION(NM_SETTING_BRIDGE_GROUP_FORWARD_MASK, "group_fwd_mask", OPTION_TYPE_INT(0, 0xFFFF, 0), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_HASH_MAX, + "hash_max", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_HASH_MAX_MIN, + NM_BRIDGE_MULTICAST_HASH_MAX_MAX, + NM_BRIDGE_MULTICAST_HASH_MAX_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT, + "multicast_last_member_count", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MIN, + NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MAX, + NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL, + "multicast_last_member_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL, + "multicast_membership_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERIER, + "multicast_querier", + OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_QUERIER_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL, + "multicast_querier_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL, + "multicast_query_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_QUERY_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL, + "multicast_query_response_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR, + "multicast_query_use_ifaddr", + OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_SNOOPING, + "multicast_snooping", + OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_SNOOPING_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_ROUTER, + "multicast_router", + OPTION_TYPE_TOFROM(to_sysfs_multicast_router, from_sysfs_multicast_router), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT, + "multicast_startup_query_count", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MIN, + NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MAX, + NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_DEF), ), + OPTION(NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL, + "multicast_startup_query_interval", + OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MIN, + NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MAX, + NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_DEF), ), + OPTION(NM_SETTING_BRIDGE_GROUP_ADDRESS, + "group_addr", + OPTION_TYPE_TOFROM(to_sysfs_group_address, from_sysfs_group_address), ), + OPTION(NM_SETTING_BRIDGE_VLAN_PROTOCOL, + "vlan_protocol", + OPTION_TYPE_TOFROM(to_sysfs_vlan_protocol, from_sysfs_vlan_protocol), ), + OPTION(NM_SETTING_BRIDGE_VLAN_STATS_ENABLED, + "vlan_stats_enabled", + OPTION_TYPE_BOOL(NM_BRIDGE_VLAN_STATS_ENABLED_DEF)), + { + 0, + }}; + +static const Option slave_options[] = { + OPTION(NM_SETTING_BRIDGE_PORT_PRIORITY, + "priority", + OPTION_TYPE_INT(NM_BRIDGE_PORT_PRIORITY_MIN, + NM_BRIDGE_PORT_PRIORITY_MAX, + NM_BRIDGE_PORT_PRIORITY_DEF), + .default_if_zero = TRUE, ), + OPTION(NM_SETTING_BRIDGE_PORT_PATH_COST, + "path_cost", + OPTION_TYPE_INT(NM_BRIDGE_PORT_PATH_COST_MIN, + NM_BRIDGE_PORT_PATH_COST_MAX, + NM_BRIDGE_PORT_PATH_COST_DEF), + .default_if_zero = TRUE, ), + OPTION(NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE, "hairpin_mode", OPTION_TYPE_BOOL(FALSE), ), + {0}}; + +static void +commit_option(NMDevice *device, NMSetting *setting, const Option *option, gboolean slave) +{ + int ifindex = nm_device_get_ifindex(device); + nm_auto_unset_gvalue GValue val = G_VALUE_INIT; + GParamSpec * pspec; + const char * value; + char value_buf[100]; + + if (slave) + nm_assert(NM_IS_SETTING_BRIDGE_PORT(setting)); + else + nm_assert(NM_IS_SETTING_BRIDGE(setting)); + + pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), option->name); + nm_assert(pspec); + + g_value_init(&val, G_PARAM_SPEC_VALUE_TYPE(pspec)); + g_object_get_property((GObject *) setting, option->name, &val); + + if (option->to_sysfs) { + value = option->to_sysfs(&val); + goto out; + } + + switch (pspec->value_type) { + case G_TYPE_BOOLEAN: + value = g_value_get_boolean(&val) ? "1" : "0"; + break; + case G_TYPE_UINT64: + case G_TYPE_UINT: + { + guint64 uval; + + if (pspec->value_type == G_TYPE_UINT64) + uval = g_value_get_uint64(&val); + else + uval = (guint) g_value_get_uint(&val); + + /* zero means "unspecified" for some NM properties but isn't in the + * allowed kernel range, so reset the property to the default value. + */ + if (option->default_if_zero && uval == 0) { + if (pspec->value_type == G_TYPE_UINT64) + uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT64(pspec); + else + uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT(pspec); + } + + /* Linux kernel bridge interfaces use 'centiseconds' for time-based values. + * In reality it's not centiseconds, but depends on HZ and USER_HZ, which + * is almost always works out to be a multiplier of 100, so we can assume + * centiseconds. See clock_t_to_jiffies(). + */ + if (option->user_hz_compensate) + uval *= 100; + + if (pspec->value_type == G_TYPE_UINT64) + nm_sprintf_buf(value_buf, "%" G_GUINT64_FORMAT, uval); + else + nm_sprintf_buf(value_buf, "%u", (guint) uval); + + value = value_buf; + } break; + case G_TYPE_STRING: + value = g_value_get_string(&val); + break; + default: + nm_assert_not_reached(); + value = NULL; + break; + } + +out: + if (!value) + return; + + if (slave) { + nm_platform_sysctl_slave_set_option(nm_device_get_platform(device), + ifindex, + option->sysname, + value); + } else { + nm_platform_sysctl_master_set_option(nm_device_get_platform(device), + ifindex, + option->sysname, + value); + } +} + +static const NMPlatformBridgeVlan ** +setting_vlans_to_platform(GPtrArray *array) +{ + NMPlatformBridgeVlan **arr; + NMPlatformBridgeVlan * p_data; + guint i; + + if (!array || !array->len) + return NULL; + + G_STATIC_ASSERT_EXPR(_nm_alignof(NMPlatformBridgeVlan *) >= _nm_alignof(NMPlatformBridgeVlan)); + arr = g_malloc((sizeof(NMPlatformBridgeVlan *) * (array->len + 1)) + + (sizeof(NMPlatformBridgeVlan) * (array->len))); + p_data = (NMPlatformBridgeVlan *) &arr[array->len + 1]; + + for (i = 0; i < array->len; i++) { + NMBridgeVlan *vlan = array->pdata[i]; + guint16 vid_start, vid_end; + + nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end); + + p_data[i] = (NMPlatformBridgeVlan){ + .vid_start = vid_start, + .vid_end = vid_end, + .pvid = nm_bridge_vlan_is_pvid(vlan), + .untagged = nm_bridge_vlan_is_untagged(vlan), + }; + arr[i] = &p_data[i]; + } + arr[i] = NULL; + return (const NMPlatformBridgeVlan **) arr; +} + +static void +commit_slave_options(NMDevice *device, NMSettingBridgePort *setting) +{ + const Option * option; + NMSetting * s; + gs_unref_object NMSetting *s_clear = NULL; + + if (setting) + s = NM_SETTING(setting); + else + s = s_clear = nm_setting_bridge_port_new(); + + for (option = slave_options; option->name; option++) + commit_option(device, s, option, TRUE); +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + NMSettingBridge *s_bridge = nm_connection_get_setting_bridge(connection); + int ifindex = nm_device_get_ifindex(device); + const Option * option; + gs_free char * stp = NULL; + int stp_value; + + if (!s_bridge) { + s_bridge = (NMSettingBridge *) nm_setting_bridge_new(); + nm_connection_add_setting(connection, (NMSetting *) s_bridge); + } + + option = master_options; + nm_assert(nm_streq(option->sysname, "stp_state")); + + stp = nm_platform_sysctl_master_get_option(nm_device_get_platform(device), + ifindex, + option->sysname); + stp_value = + _nm_utils_ascii_str_to_int64(stp, 10, option->nm_min, option->nm_max, option->nm_default); + g_object_set(s_bridge, option->name, stp_value, NULL); + option++; + + for (; option->name; option++) { + nm_auto_unset_gvalue GValue value = G_VALUE_INIT; + gs_free char * str = NULL; + GParamSpec * pspec; + + str = nm_platform_sysctl_master_get_option(nm_device_get_platform(device), + ifindex, + option->sysname); + pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(s_bridge), option->name); + + if (!stp_value && option->only_with_stp) + continue; + + if (!str) { + _LOGW(LOGD_BRIDGE, "failed to read bridge setting '%s'", option->sysname); + continue; + } + + g_value_init(&value, G_PARAM_SPEC_VALUE_TYPE(pspec)); + + if (option->from_sysfs) { + option->from_sysfs(str, &value); + goto out; + } + + switch (pspec->value_type) { + case G_TYPE_UINT64: + case G_TYPE_UINT: + { + guint64 uvalue; + + /* See comments in set_sysfs_uint() about centiseconds. */ + if (option->user_hz_compensate) { + uvalue = _nm_utils_ascii_str_to_int64(str, + 10, + option->nm_min * 100, + option->nm_max * 100, + option->nm_default * 100); + uvalue /= 100; + } else { + uvalue = _nm_utils_ascii_str_to_uint64(str, + 10, + option->nm_min, + option->nm_max, + option->nm_default); + } + + if (pspec->value_type == G_TYPE_UINT64) + g_value_set_uint64(&value, uvalue); + else + g_value_set_uint(&value, (guint) uvalue); + } break; + case G_TYPE_BOOLEAN: + { + gboolean bvalue; + + bvalue = _nm_utils_ascii_str_to_int64(str, + 10, + option->nm_min, + option->nm_max, + option->nm_default); + g_value_set_boolean(&value, bvalue); + } break; + case G_TYPE_STRING: + g_value_set_string(&value, str); + break; + default: + nm_assert_not_reached(); + break; + } + +out: + g_object_set_property(G_OBJECT(s_bridge), option->name, &value); + } +} + +static gboolean +master_update_slave_connection(NMDevice * device, + NMDevice * slave, + NMConnection *connection, + GError ** error) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + NMSettingConnection *s_con; + NMSettingBridgePort *s_port; + int ifindex_slave = nm_device_get_ifindex(slave); + const char * iface = nm_device_get_iface(device); + const Option * option; + + g_return_val_if_fail(ifindex_slave > 0, FALSE); + + s_con = nm_connection_get_setting_connection(connection); + s_port = nm_connection_get_setting_bridge_port(connection); + if (!s_port) { + s_port = (NMSettingBridgePort *) nm_setting_bridge_port_new(); + nm_connection_add_setting(connection, NM_SETTING(s_port)); + } + + for (option = slave_options; option->name; option++) { + gs_free char *str = nm_platform_sysctl_slave_get_option(nm_device_get_platform(device), + ifindex_slave, + option->sysname); + uint value; + + if (str) { + /* See comments in set_sysfs_uint() about centiseconds. */ + if (option->user_hz_compensate) { + value = _nm_utils_ascii_str_to_int64(str, + 10, + option->nm_min * 100, + option->nm_max * 100, + option->nm_default * 100); + value /= 100; + } else { + value = _nm_utils_ascii_str_to_int64(str, + 10, + option->nm_min, + option->nm_max, + option->nm_default); + } + g_object_set(s_port, option->name, value, NULL); + } else + _LOGW(LOGD_BRIDGE, "failed to read bridge port setting '%s'", option->sysname); + } + + g_object_set(s_con, + NM_SETTING_CONNECTION_MASTER, + iface, + NM_SETTING_CONNECTION_SLAVE_TYPE, + NM_SETTING_BRIDGE_SETTING_NAME, + NULL); + return TRUE; +} + +static gboolean +bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + gconstpointer hwaddr; + size_t length; + gboolean enabled; + guint16 pvid; + NMPlatform * plat; + int ifindex; + gs_unref_ptrarray GPtrArray *vlans = NULL; + gs_free const NMPlatformBridgeVlan **plat_vlans = NULL; + + if (self->vlan_configured) + return TRUE; + + plat = nm_device_get_platform(device); + ifindex = nm_device_get_ifindex(device); + enabled = nm_setting_bridge_get_vlan_filtering(s_bridge); + + if (!enabled) { + nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0"); + nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "1"); + nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL); + return TRUE; + } + + hwaddr = nm_platform_link_get_address(plat, ifindex, &length); + g_return_val_if_fail(length == ETH_ALEN, FALSE); + if (nm_utils_hwaddr_matches(hwaddr, length, &nm_ether_addr_zero, ETH_ALEN)) { + /* We need a non-zero MAC address to set the default pvid. + * Retry later. */ + return TRUE; + } + + self->vlan_configured = TRUE; + + /* Filtering must be disabled to change the default PVID */ + if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0")) + return FALSE; + + /* Clear the default PVID so that we later can force the re-creation of + * default PVID VLANs by writing the option again. */ + if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "0")) + return FALSE; + + /* Clear all existing VLANs */ + if (!nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL)) + return FALSE; + + /* Now set the default PVID. After this point the kernel creates + * a PVID VLAN on each port, including the bridge itself. */ + pvid = nm_setting_bridge_get_vlan_default_pvid(s_bridge); + if (pvid) { + char value[32]; + + nm_sprintf_buf(value, "%u", pvid); + if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", value)) + return FALSE; + } + + /* Create VLANs only after setting the default PVID, so that + * any PVID VLAN overrides the bridge's default PVID. */ + g_object_get(s_bridge, NM_SETTING_BRIDGE_VLANS, &vlans, NULL); + plat_vlans = setting_vlans_to_platform(vlans); + if (plat_vlans && !nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, plat_vlans)) + return FALSE; + + if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "1")) + return FALSE; + + return TRUE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMConnection *connection; + NMSetting * s_bridge; + const Option *option; + + connection = nm_device_get_applied_connection(device); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + s_bridge = (NMSetting *) nm_connection_get_setting_bridge(connection); + g_return_val_if_fail(s_bridge, NM_ACT_STAGE_RETURN_FAILURE); + + for (option = master_options; option->name; option++) + commit_option(device, s_bridge, option, FALSE); + + if (!bridge_set_vlan_options(device, (NMSettingBridge *) s_bridge)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +_bt_register_bridge_cb(GError *error, gpointer user_data) +{ + NMDeviceBridge *self; + + if (nm_utils_error_is_cancelled(error)) + return; + + self = user_data; + + g_clear_object(&self->bt_cancellable); + + if (error) { + _LOGD(LOGD_DEVICE, "bluetooth NAP server failed to register bridge: %s", error->message); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_BT_FAILED); + return; + } + + nm_device_activate_schedule_stage2_device_config(NM_DEVICE(self), FALSE); +} + +void +_nm_device_bridge_notify_unregister_bt_nap(NMDevice *device, const char *reason) +{ + NMDeviceBridge *self = NM_DEVICE_BRIDGE(device); + + _LOGD(LOGD_DEVICE, + "bluetooth NAP server unregistered from bridge: %s%s", + reason, + self->bt_registered ? "" : " (was no longer registered)"); + + nm_clear_g_cancellable(&self->bt_cancellable); + + if (self->bt_registered) { + self->bt_registered = FALSE; + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_BT_FAILED); + } +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + NMConnection * connection; + NMSettingBluetooth *s_bt; + gs_free_error GError *error = NULL; + + connection = nm_device_get_applied_connection(device); + + s_bt = _nm_connection_get_setting_bluetooth_for_nap(connection); + if (!s_bt) + return NM_ACT_STAGE_RETURN_SUCCESS; + + if (!nm_bt_vtable_network_server) { + _LOGD(LOGD_DEVICE, "bluetooth NAP server failed because bluetooth plugin not available"); + *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (self->bt_cancellable) + return NM_ACT_STAGE_RETURN_POSTPONE; + + if (self->bt_registered) + return NM_ACT_STAGE_RETURN_POSTPONE; + + self->bt_cancellable = g_cancellable_new(); + if (!nm_bt_vtable_network_server->register_bridge(nm_bt_vtable_network_server, + nm_setting_bluetooth_get_bdaddr(s_bt), + device, + self->bt_cancellable, + _bt_register_bridge_cb, + device, + &error)) { + _LOGD(LOGD_DEVICE, "bluetooth NAP server failed to register bridge: %s", error->message); + *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + self->bt_registered = TRUE; + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceBridge *self = NM_DEVICE_BRIDGE(device); + + _LOGD(LOGD_DEVICE, + "deactivate bridge%s", + self->bt_registered ? " (registered as NAP bluetooth device)" : ""); + + self->vlan_configured = FALSE; + + nm_clear_g_cancellable(&self->bt_cancellable); + + if (self->bt_registered) { + self->bt_registered = FALSE; + nm_bt_vtable_network_server->unregister_bridge(nm_bt_vtable_network_server, device); + } +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + NMDeviceBridge * self = NM_DEVICE_BRIDGE(device); + NMConnection * master_connection; + NMSettingBridge * s_bridge; + NMSettingBridgePort *s_port; + + if (configure) { + if (!nm_platform_link_enslave(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + nm_device_get_ip_ifindex(slave))) + return FALSE; + + master_connection = nm_device_get_applied_connection(device); + nm_assert(master_connection); + s_bridge = nm_connection_get_setting_bridge(master_connection); + nm_assert(s_bridge); + s_port = nm_connection_get_setting_bridge_port(connection); + + bridge_set_vlan_options(device, s_bridge); + + if (nm_setting_bridge_get_vlan_filtering(s_bridge)) { + gs_free const NMPlatformBridgeVlan **plat_vlans = NULL; + gs_unref_ptrarray GPtrArray *vlans = NULL; + + if (s_port) + g_object_get(s_port, NM_SETTING_BRIDGE_PORT_VLANS, &vlans, NULL); + + plat_vlans = setting_vlans_to_platform(vlans); + + /* Since the link was just enslaved, there are no existing VLANs + * (except for the default one) and so there's no need to flush. */ + + if (plat_vlans + && !nm_platform_link_set_bridge_vlans(nm_device_get_platform(slave), + nm_device_get_ifindex(slave), + TRUE, + plat_vlans)) + return FALSE; + } + + commit_slave_options(slave, s_port); + + _LOGI(LOGD_BRIDGE, "attached bridge port %s", nm_device_get_ip_iface(slave)); + } else { + _LOGI(LOGD_BRIDGE, "bridge port %s was attached", nm_device_get_ip_iface(slave)); + } + + return TRUE; +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{ + NMDeviceBridge *self = NM_DEVICE_BRIDGE(device); + gboolean success; + int ifindex_slave; + int ifindex; + + if (configure) { + ifindex = nm_device_get_ifindex(device); + if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex)) + configure = FALSE; + } + + ifindex_slave = nm_device_get_ip_ifindex(slave); + + if (ifindex_slave <= 0) { + _LOGD(LOGD_TEAM, "bond slave %s is already released", nm_device_get_ip_iface(slave)); + return; + } + + if (configure) { + success = nm_platform_link_release(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + ifindex_slave); + + if (success) { + _LOGI(LOGD_BRIDGE, "detached bridge port %s", nm_device_get_ip_iface(slave)); + } else { + _LOGW(LOGD_BRIDGE, "failed to detach bridge port %s", nm_device_get_ip_iface(slave)); + } + } else { + _LOGI(LOGD_BRIDGE, "bridge port %s was detached", nm_device_get_ip_iface(slave)); + } +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + NMSettingWired * s_wired; + NMSettingBridge * s_bridge; + const char * iface = nm_device_get_iface(device); + const char * hwaddr; + gs_free char * hwaddr_cloned = NULL; + guint8 mac_address[NM_UTILS_HWADDR_LEN_MAX]; + NMPlatformLnkBridge props; + int r; + guint32 mtu = 0; + + nm_assert(iface); + + s_bridge = nm_connection_get_setting_bridge(connection); + nm_assert(s_bridge); + + s_wired = nm_connection_get_setting_wired(connection); + if (s_wired) + mtu = nm_setting_wired_get_mtu(s_wired); + + hwaddr = nm_setting_bridge_get_mac_address(s_bridge); + if (!hwaddr + && nm_device_hw_addr_get_cloned(device, connection, FALSE, &hwaddr_cloned, NULL, NULL)) { + /* FIXME: we set the MAC address when creating the interface, while the + * NMDevice is still unrealized. As we afterwards realize the device, it + * forgets the parameters for the cloned MAC address, and in stage 1 + * it might create a different MAC address. That should be fixed by + * better handling device realization. */ + hwaddr = hwaddr_cloned; + } + + if (hwaddr) { + if (!nm_utils_hwaddr_aton(hwaddr, mac_address, ETH_ALEN)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Invalid hardware address '%s'", + hwaddr); + g_return_val_if_reached(FALSE); + } + } + + props = (NMPlatformLnkBridge){ + .forward_delay = _DEFAULT_IF_ZERO(nm_setting_bridge_get_forward_delay(s_bridge) * 100u, + NM_BRIDGE_FORWARD_DELAY_DEF_SYS), + .hello_time = _DEFAULT_IF_ZERO(nm_setting_bridge_get_hello_time(s_bridge) * 100u, + NM_BRIDGE_HELLO_TIME_DEF_SYS), + .max_age = _DEFAULT_IF_ZERO(nm_setting_bridge_get_max_age(s_bridge) * 100u, + NM_BRIDGE_MAX_AGE_DEF_SYS), + .ageing_time = _DEFAULT_IF_ZERO(nm_setting_bridge_get_ageing_time(s_bridge) * 100u, + NM_BRIDGE_AGEING_TIME_DEF_SYS), + .stp_state = nm_setting_bridge_get_stp(s_bridge), + .priority = nm_setting_bridge_get_priority(s_bridge), + .vlan_protocol = to_sysfs_vlan_protocol_sys(nm_setting_bridge_get_vlan_protocol(s_bridge)), + .vlan_stats_enabled = nm_setting_bridge_get_vlan_stats_enabled(s_bridge), + .group_fwd_mask = nm_setting_bridge_get_group_forward_mask(s_bridge), + .mcast_snooping = nm_setting_bridge_get_multicast_snooping(s_bridge), + .mcast_router = + to_sysfs_multicast_router_sys(nm_setting_bridge_get_multicast_router(s_bridge)), + .mcast_query_use_ifaddr = nm_setting_bridge_get_multicast_query_use_ifaddr(s_bridge), + .mcast_querier = nm_setting_bridge_get_multicast_querier(s_bridge), + .mcast_hash_max = nm_setting_bridge_get_multicast_hash_max(s_bridge), + .mcast_last_member_count = nm_setting_bridge_get_multicast_last_member_count(s_bridge), + .mcast_startup_query_count = nm_setting_bridge_get_multicast_startup_query_count(s_bridge), + .mcast_last_member_interval = + nm_setting_bridge_get_multicast_last_member_interval(s_bridge), + .mcast_membership_interval = nm_setting_bridge_get_multicast_membership_interval(s_bridge), + .mcast_querier_interval = nm_setting_bridge_get_multicast_querier_interval(s_bridge), + .mcast_query_interval = nm_setting_bridge_get_multicast_query_interval(s_bridge), + .mcast_query_response_interval = + nm_setting_bridge_get_multicast_query_response_interval(s_bridge), + .mcast_startup_query_interval = + nm_setting_bridge_get_multicast_startup_query_interval(s_bridge), + }; + + to_sysfs_group_address_sys(nm_setting_bridge_get_group_address(s_bridge), &props.group_addr); + + /* If mtu != 0, we set the MTU of the new bridge at creation time. However, kernel will still + * automatically adjust the MTU of the bridge based on the minimum of the slave's MTU. + * We don't want this automatism as the user asked for a fixed MTU. + * + * To workaround this behavior of kernel, we will later toggle the MTU twice. See + * NMDeviceClass.mtu_force_set. */ + r = nm_platform_link_bridge_add(nm_device_get_platform(device), + iface, + hwaddr ? mac_address : NULL, + hwaddr ? ETH_ALEN : 0, + mtu, + &props, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create bridge interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +nm_device_bridge_init(NMDeviceBridge *self) +{ + nm_assert(nm_device_is_master(NM_DEVICE(self))); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_bridge = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_BRIDGE, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves", + "ao", + NM_DEVICE_SLAVES), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_bridge_class_init(NMDeviceBridgeClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bridge); + + device_class->connection_type_supported = NM_SETTING_BRIDGE_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BRIDGE); + + device_class->is_master = TRUE; + device_class->mtu_force_set = TRUE; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + + device_class->update_connection = update_connection; + device_class->master_update_slave_connection = master_update_slave_connection; + + device_class->create_and_realize = create_and_realize; + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->deactivate = deactivate; + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; +} + +/*****************************************************************************/ + +#define NM_TYPE_BRIDGE_DEVICE_FACTORY (nm_bridge_device_factory_get_type()) +#define NM_BRIDGE_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BRIDGE_DEVICE_FACTORY, NMBridgeDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_BRIDGE, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_DRIVER, + "bridge", + NM_DEVICE_TYPE_DESC, + "Bridge", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_BRIDGE, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_BRIDGE, + NULL); +} + +static gboolean +match_connection(NMDeviceFactory *factory, NMConnection *connection) +{ + const char *type = nm_connection_get_connection_type(connection); + + if (nm_streq(type, NM_SETTING_BRIDGE_SETTING_NAME)) + return TRUE; + + nm_assert(nm_streq(type, NM_SETTING_BLUETOOTH_SETTING_NAME)); + + if (!_nm_connection_get_setting_bluetooth_for_nap(connection)) + return FALSE; + + if (!g_type_from_name("NMBluezManager")) { + /* bluetooth NAP connections are handled by bridge factory. However, + * it needs help from the bluetooth plugin, so if the plugin is not loaded, + * we claim not to support it. */ + return FALSE; + } + + return TRUE; +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + BRIDGE, + Bridge, + bridge, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_BRIDGE) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BRIDGE_SETTING_NAME, + NM_SETTING_BLUETOOTH_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->match_connection = match_connection;); diff --git a/src/core/devices/nm-device-bridge.h b/src/core/devices/nm-device-bridge.h new file mode 100644 index 00000000..32437947 --- /dev/null +++ b/src/core/devices/nm-device-bridge.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2012 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_BRIDGE_H__ +#define __NETWORKMANAGER_DEVICE_BRIDGE_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_BRIDGE (nm_device_bridge_get_type()) +#define NM_DEVICE_BRIDGE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridge)) +#define NM_DEVICE_BRIDGE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridgeClass)) +#define NM_IS_DEVICE_BRIDGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BRIDGE)) +#define NM_IS_DEVICE_BRIDGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BRIDGE)) +#define NM_DEVICE_BRIDGE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridgeClass)) + +typedef struct _NMDeviceBridge NMDeviceBridge; +typedef struct _NMDeviceBridgeClass NMDeviceBridgeClass; + +GType nm_device_bridge_get_type(void); + +extern const NMBtVTableNetworkServer *nm_bt_vtable_network_server; + +void _nm_device_bridge_notify_unregister_bt_nap(NMDevice *device, const char *reason); + +#endif /* __NETWORKMANAGER_DEVICE_BRIDGE_H__ */ diff --git a/src/core/devices/nm-device-dummy.c b/src/core/devices/nm-device-dummy.c new file mode 100644 index 00000000..13cfd3b0 --- /dev/null +++ b/src/core/devices/nm-device-dummy.c @@ -0,0 +1,183 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-dummy.h" + +#include <stdlib.h> +#include <sys/types.h> + +#include "nm-act-request.h" +#include "nm-device-private.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-dummy.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceDummy +#include "nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceDummy { + NMDevice parent; +}; + +struct _NMDeviceDummyClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceDummy, nm_device_dummy, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingDummy *s_dummy; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_DUMMY_SETTING_NAME, + existing_connections, + NULL, + _("Dummy connection"), + NULL, + NULL, + TRUE); + + s_dummy = nm_connection_get_setting_dummy(connection); + if (!s_dummy) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'dummy' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMSettingDummy *s_dummy = nm_connection_get_setting_dummy(connection); + + if (!s_dummy) { + s_dummy = (NMSettingDummy *) nm_setting_dummy_new(); + nm_connection_add_setting(connection, (NMSetting *) s_dummy); + } +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSettingDummy *s_dummy; + int r; + + s_dummy = nm_connection_get_setting_dummy(connection); + g_assert(s_dummy); + + r = nm_platform_link_dummy_add(nm_device_get_platform(device), iface, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create dummy interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +nm_device_dummy_init(NMDeviceDummy *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_dummy = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_DUMMY, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_dummy_class_init(NMDeviceDummyClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_dummy); + + device_class->connection_type_supported = NM_SETTING_DUMMY_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_DUMMY_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_DUMMY); + + device_class->complete_connection = complete_connection; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->update_connection = update_connection; + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; +} + +/*****************************************************************************/ + +#define NM_TYPE_DUMMY_DEVICE_FACTORY (nm_dummy_device_factory_get_type()) +#define NM_DUMMY_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DUMMY_DEVICE_FACTORY, NMDummyDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_DUMMY, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Dummy", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_DUMMY, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_DUMMY, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + DUMMY, + Dummy, + dummy, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_DUMMY) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_DUMMY_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-dummy.h b/src/core/devices/nm-device-dummy.h new file mode 100644 index 00000000..2845b4eb --- /dev/null +++ b/src/core/devices/nm-device-dummy.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_DUMMY_H__ +#define __NETWORKMANAGER_DEVICE_DUMMY_H__ + +#include "nm-device-generic.h" + +#define NM_TYPE_DEVICE_DUMMY (nm_device_dummy_get_type()) +#define NM_DEVICE_DUMMY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummy)) +#define NM_DEVICE_DUMMY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_DUMMY, NMDeviceDummyClass)) +#define NM_IS_DEVICE_DUMMY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_DUMMY)) +#define NM_IS_DEVICE_DUMMY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_DUMMY)) +#define NM_DEVICE_DUMMY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummyClass)) + +typedef struct _NMDeviceDummy NMDeviceDummy; +typedef struct _NMDeviceDummyClass NMDeviceDummyClass; + +GType nm_device_dummy_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_DUMMY_H__ */ diff --git a/src/core/devices/nm-device-ethernet-utils.c b/src/core/devices/nm-device-ethernet-utils.c new file mode 100644 index 00000000..a36d12b7 --- /dev/null +++ b/src/core/devices/nm-device-ethernet-utils.c @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ethernet-utils.h" + +#include "settings/nm-settings-connection.h" + +char * +nm_device_ethernet_utils_get_default_wired_name(GHashTable *existing_ids) +{ + char *temp; + int i; + + /* Find the next available unique connection name */ + for (i = 1; i < G_MAXINT; i++) { + temp = g_strdup_printf(_("Wired connection %d"), i); + if (!existing_ids || !g_hash_table_contains(existing_ids, temp)) + return temp; + g_free(temp); + } + return NULL; +} diff --git a/src/core/devices/nm-device-ethernet-utils.h b/src/core/devices/nm-device-ethernet-utils.h new file mode 100644 index 00000000..133340fc --- /dev/null +++ b/src/core/devices/nm-device-ethernet-utils.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H__ +#define __NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H__ + +char *nm_device_ethernet_utils_get_default_wired_name(GHashTable *existing_ids); + +#endif /* NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H */ diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c new file mode 100644 index 00000000..44428869 --- /dev/null +++ b/src/core/devices/nm-device-ethernet.c @@ -0,0 +1,2128 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2014 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ethernet.h" + +#include <netinet/in.h> +#include <stdlib.h> +#include <unistd.h> +#include <libudev.h> +#include <linux/if_ether.h> + +#include "nm-device-private.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" +#include "NetworkManagerUtils.h" +#include "supplicant/nm-supplicant-manager.h" +#include "supplicant/nm-supplicant-interface.h" +#include "supplicant/nm-supplicant-config.h" +#include "ppp/nm-ppp-manager.h" +#include "ppp/nm-ppp-manager-call.h" +#include "ppp/nm-ppp-status.h" +#include "platform/nm-platform.h" +#include "nm-platform/nm-platform-utils.h" +#include "nm-dcb.h" +#include "settings/nm-settings-connection.h" +#include "nm-config.h" +#include "nm-device-ethernet-utils.h" +#include "settings/nm-settings.h" +#include "nm-device-factory.h" +#include "nm-core-internal.h" +#include "NetworkManagerUtils.h" +#include "nm-udev-aux/nm-udev-utils.h" +#include "nm-device-veth.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceEthernet +#include "nm-device-logging.h" + +/*****************************************************************************/ + +#define PPPOE_RECONNECT_DELAY 7 +#define PPPOE_ENCAP_OVERHEAD 8 /* 2 bytes for PPP, 6 for PPPoE */ + +#define SUPPLICANT_LNK_TIMEOUT_SEC 15 + +/*****************************************************************************/ + +typedef enum { + DCB_WAIT_UNKNOWN = 0, + /* Ensure carrier is up before enabling DCB */ + DCB_WAIT_CARRIER_PREENABLE_UP, + /* Wait for carrier down when device starts enabling */ + DCB_WAIT_CARRIER_PRECONFIG_DOWN, + /* Wait for carrier up when device has finished enabling */ + DCB_WAIT_CARRIER_PRECONFIG_UP, + /* Wait carrier down when device starts configuring */ + DCB_WAIT_CARRIER_POSTCONFIG_DOWN, + /* Wait carrier up when device has finished configuring */ + DCB_WAIT_CARRIER_POSTCONFIG_UP, +} DcbWait; + +typedef struct _NMDeviceEthernetPrivate { + /* s390 */ + char * subchan1; + char * subchan2; + char * subchan3; + char * subchannels; /* Composite used for checking unmanaged specs */ + char ** subchannels_dbus; /* Array exported on D-Bus */ + char * s390_nettype; + GHashTable *s390_options; + + guint32 speed; + gulong carrier_id; + + struct { + NMSupplicantManager * mgr; + NMSupplMgrCreateIfaceHandle *create_handle; + NMSupplicantInterface * iface; + + gulong iface_state_id; + gulong auth_state_id; + + guint con_timeout_id; + + guint lnk_timeout_id; + + bool is_associated : 1; + } supplicant; + + NMActRequestGetSecretsCallId *wired_secrets_id; + + /* PPPoE */ + NMPPPManager *ppp_manager; + gint32 last_pppoe_time; + guint pppoe_wait_id; + + /* DCB */ + DcbWait dcb_wait; + guint dcb_timeout_id; + + guint32 ethtool_prev_speed; + + NMPlatformLinkDuplexType ethtool_prev_duplex : 3; + + bool dcb_handle_carrier_changes : 1; + + bool ethtool_prev_set : 1; + bool ethtool_prev_autoneg : 1; + +} NMDeviceEthernetPrivate; + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceEthernet, PROP_SPEED, PROP_S390_SUBCHANNELS, ); + +/*****************************************************************************/ + +G_DEFINE_TYPE(NMDeviceEthernet, nm_device_ethernet, NM_TYPE_DEVICE) + +#define NM_DEVICE_ETHERNET_GET_PRIVATE(self) \ + _NM_GET_PRIVATE_PTR(self, NMDeviceEthernet, NM_IS_DEVICE_ETHERNET, NMDevice) + +/*****************************************************************************/ + +static void wired_secrets_cancel(NMDeviceEthernet *self); + +/*****************************************************************************/ + +static char * +get_link_basename(const char *parent_path, const char *name, GError **error) +{ + char *link_dest, *path; + char *result = NULL; + + path = g_strdup_printf("%s/%s", parent_path, name); + link_dest = g_file_read_link(path, error); + if (link_dest) { + result = g_path_get_basename(link_dest); + g_free(link_dest); + } + g_free(path); + return result; +} + +static void +_update_s390_subchannels(NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + struct udev_device * dev = NULL; + struct udev_device * parent = NULL; + const char * parent_path, *item; + int ifindex; + GDir * dir; + GError * error = NULL; + + if (priv->subchannels) { + /* only read the subchannels once. For one, we don't expect them to change + * on multiple invocations. Second, we didn't implement proper reloading. + * Proper reloading might also be complicated, because the subchannels are + * used to match on devices based on a device-spec. Thus, it's not clear + * what it means to change afterwards. */ + return; + } + + ifindex = nm_device_get_ifindex((NMDevice *) self); + dev = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)), ifindex); + if (!dev) + return; + + /* Try for the "ccwgroup" parent */ + parent = udev_device_get_parent_with_subsystem_devtype(dev, "ccwgroup", NULL); + if (!parent) { + /* FIXME: whatever 'lcs' devices' subsystem is here... */ + + /* Not an s390 device */ + return; + } + + parent_path = udev_device_get_syspath(parent); + dir = g_dir_open(parent_path, 0, &error); + if (!dir) { + _LOGW(LOGD_DEVICE | LOGD_PLATFORM, + "update-s390: failed to open directory '%s': %s", + parent_path, + error->message); + g_clear_error(&error); + return; + } + + while ((item = g_dir_read_name(dir))) { + if (!strcmp(item, "cdev0")) { + priv->subchan1 = get_link_basename(parent_path, "cdev0", &error); + } else if (!strcmp(item, "cdev1")) { + priv->subchan2 = get_link_basename(parent_path, "cdev1", &error); + } else if (!strcmp(item, "cdev2")) { + priv->subchan3 = get_link_basename(parent_path, "cdev2", &error); + } else if (!strcmp(item, "driver")) { + priv->s390_nettype = get_link_basename(parent_path, "driver", &error); + } else if (!strcmp(item, "layer2") || !strcmp(item, "portname") + || !strcmp(item, "portno")) { + gs_free char *path = NULL, *value = NULL; + + path = g_strdup_printf("%s/%s", parent_path, item); + value = nm_platform_sysctl_get(nm_device_get_platform(NM_DEVICE(self)), + NMP_SYSCTL_PATHID_ABSOLUTE(path)); + + if (!strcmp(item, "portname") && !g_strcmp0(value, "no portname required")) { + /* Do nothing */ + } else if (value && *value) { + g_hash_table_insert(priv->s390_options, g_strdup(item), value); + value = NULL; + } else + _LOGW(LOGD_DEVICE | LOGD_PLATFORM, "update-s390: error reading %s", path); + } + + if (error) { + _LOGW(LOGD_DEVICE | LOGD_PLATFORM, + "update-s390: failed reading sysfs for %s (%s)", + item, + error->message); + g_clear_error(&error); + } + } + + g_dir_close(dir); + + if (priv->subchan3) { + priv->subchannels = + g_strdup_printf("%s,%s,%s", priv->subchan1, priv->subchan2, priv->subchan3); + } else if (priv->subchan2) { + priv->subchannels = g_strdup_printf("%s,%s", priv->subchan1, priv->subchan2); + } else + priv->subchannels = g_strdup(priv->subchan1); + + priv->subchannels_dbus = g_new(char *, 3 + 1); + priv->subchannels_dbus[0] = g_strdup(priv->subchan1); + priv->subchannels_dbus[1] = g_strdup(priv->subchan2); + priv->subchannels_dbus[2] = g_strdup(priv->subchan3); + priv->subchannels_dbus[3] = NULL; + + _LOGI(LOGD_DEVICE | LOGD_PLATFORM, + "update-s390: found s390 '%s' subchannels [%s]", + nm_device_get_driver((NMDevice *) self) ?: "(unknown driver)", + priv->subchannels); + + _notify(self, PROP_S390_SUBCHANNELS); +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + if (new_state > NM_DEVICE_STATE_ACTIVATED) + wired_secrets_cancel(NM_DEVICE_ETHERNET(device)); +} + +static void +nm_device_ethernet_init(NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetPrivate); + self->_priv = priv; + + priv->s390_options = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + NMDeviceEthernet *self = NM_DEVICE_ETHERNET(device); + int ifindex = nm_device_get_ifindex(device); + + if (ifindex > 0) { + if (nm_platform_link_supports_carrier_detect(nm_device_get_platform(device), ifindex)) + return NM_DEVICE_CAP_CARRIER_DETECT; + else { + _LOGI(LOGD_PLATFORM, + "driver '%s' does not support carrier detection.", + nm_device_get_driver(device)); + } + } + + return NM_DEVICE_CAP_NONE; +} + +static guint32 +_subchannels_count_num(const char *const *array) +{ + int i; + + if (!array) + return 0; + for (i = 0; array[i]; i++) + /* NOP */; + return i; +} + +static gboolean +match_subchans(NMDeviceEthernet *self, NMSettingWired *s_wired, gboolean *try_mac) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + const char *const * subchans; + guint32 num1, num2; + int i; + + *try_mac = TRUE; + + subchans = nm_setting_wired_get_s390_subchannels(s_wired); + num1 = _subchannels_count_num(subchans); + num2 = _subchannels_count_num((const char *const *) priv->subchannels_dbus); + /* connection has no subchannels */ + if (num1 == 0) + return TRUE; + /* connection requires subchannels but the device has none */ + if (num2 == 0) + return FALSE; + /* number of subchannels differ */ + if (num1 != num2) + return FALSE; + + /* Make sure each subchannel in the connection is a subchannel of this device */ + for (i = 0; subchans[i]; i++) { + const char *candidate = subchans[i]; + + if ((priv->subchan1 && !strcmp(priv->subchan1, candidate)) + || (priv->subchan2 && !strcmp(priv->subchan2, candidate)) + || (priv->subchan3 && !strcmp(priv->subchan3, candidate))) + continue; + + return FALSE; /* a subchannel was not found */ + } + + *try_mac = FALSE; + return TRUE; +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceEthernet *self = NM_DEVICE_ETHERNET(device); + NMSettingWired * s_wired; + + if (!NM_DEVICE_CLASS(nm_device_ethernet_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME) + || (nm_connection_is_type(connection, NM_SETTING_VETH_SETTING_NAME) + && NM_IS_DEVICE_VETH(device))) { + s_wired = nm_connection_get_setting_wired(connection); + } else { + s_wired = + _nm_connection_check_main_setting(connection, NM_SETTING_WIRED_SETTING_NAME, error); + if (!s_wired) + return FALSE; + } + + if (s_wired) { + const char * mac, *perm_hw_addr; + gboolean try_mac = TRUE; + const char *const *mac_blacklist; + int i; + + if (!match_subchans(self, s_wired, &try_mac)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "s390 subchannels don't match"); + return FALSE; + } + + perm_hw_addr = nm_device_get_permanent_hw_address(device); + mac = nm_setting_wired_get_mac_address(s_wired); + if (perm_hw_addr) { + if (try_mac && mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "permanent MAC address doesn't match"); + return FALSE; + } + + /* Check for MAC address blacklist */ + mac_blacklist = nm_setting_wired_get_mac_address_blacklist(s_wired); + for (i = 0; mac_blacklist[i]; i++) { + if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "invalid MAC in blacklist"); + return FALSE; + } + + if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "permanent MAC address of device blacklisted"); + return FALSE; + } + } + } else if (mac) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has no permanent MAC address to match"); + return FALSE; + } + } + + return TRUE; +} + +/*****************************************************************************/ +/* 802.1X */ + +static void +supplicant_interface_release(NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + nm_clear_pointer(&priv->supplicant.create_handle, + nm_supplicant_manager_create_interface_cancel); + + nm_clear_g_source(&priv->supplicant.lnk_timeout_id); + nm_clear_g_source(&priv->supplicant.con_timeout_id); + nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id); + nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.auth_state_id); + + if (priv->supplicant.iface) { + nm_supplicant_interface_disconnect(priv->supplicant.iface); + g_clear_object(&priv->supplicant.iface); + } +} + +static void +supplicant_auth_state_changed(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceEthernet * self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSupplicantAuthState state; + + state = nm_supplicant_interface_get_auth_state(priv->supplicant.iface); + _LOGD(LOGD_CORE, "supplicant auth state changed to %u", (unsigned) state); + + if (state == NM_SUPPLICANT_AUTH_STATE_SUCCESS) { + nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id); + nm_device_update_dynamic_ip_setup(NM_DEVICE(self)); + } +} + +static gboolean +wired_auth_is_optional(NMDeviceEthernet *self) +{ + NMSetting8021x *s_8021x; + + s_8021x = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_802_1X); + g_return_val_if_fail(s_8021x, FALSE); + return nm_setting_802_1x_get_optional(s_8021x); +} + +static void +wired_auth_cond_fail(NMDeviceEthernet *self, NMDeviceStateReason reason) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + + if (wired_auth_is_optional(self)) { + _LOGI( + LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) 802.1X authentication is optional, continuing after a failure"); + if (NM_IN_SET(nm_device_get_state(device), + NM_DEVICE_STATE_CONFIG, + NM_DEVICE_STATE_NEED_AUTH)) + nm_device_activate_schedule_stage3_ip_config_start(device); + + if (!priv->supplicant.auth_state_id) { + priv->supplicant.auth_state_id = + g_signal_connect(priv->supplicant.iface, + "notify::" NM_SUPPLICANT_INTERFACE_AUTH_STATE, + G_CALLBACK(supplicant_auth_state_changed), + self); + } + return; + } + + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason); +} + +static void +wired_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * connection, + GError * error, + gpointer user_data) +{ + NMDeviceEthernet * self = user_data; + NMDevice * device = user_data; + NMDeviceEthernetPrivate *priv; + + g_return_if_fail(NM_IS_DEVICE_ETHERNET(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + g_return_if_fail(priv->wired_secrets_id == call_id); + + priv->wired_secrets_id = NULL; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + g_return_if_fail(nm_act_request_get_settings_connection(req) == connection); + + if (error) { + _LOGW(LOGD_ETHER, "%s", error->message); + wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + supplicant_interface_release(self); + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +wired_secrets_cancel(NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + if (priv->wired_secrets_id) + nm_act_request_cancel_secrets(NULL, priv->wired_secrets_id); + nm_assert(!priv->wired_secrets_id); +} + +static void +wired_secrets_get_secrets(NMDeviceEthernet * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMActRequest * req; + + wired_secrets_cancel(self); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv->wired_secrets_id = + nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, wired_secrets_cb, self); + g_return_if_fail(priv->wired_secrets_id); +} + +static gboolean +supplicant_lnk_timeout_cb(gpointer user_data) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(user_data); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMActRequest * req; + NMConnection * applied_connection; + const char * setting_name; + + priv->supplicant.lnk_timeout_id = 0; + + req = nm_device_get_act_request(device); + + if (nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) { + wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT); + return G_SOURCE_REMOVE; + } + + /* Disconnect event during initial authentication and credentials + * ARE checked - we are likely to have wrong key. Ask the user for + * another one. + */ + if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG) + goto time_out; + + nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req)); + + applied_connection = nm_act_request_get_applied_connection(req); + setting_name = nm_connection_need_secrets(applied_connection, NULL); + if (!setting_name) + goto time_out; + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) disconnected during authentication, asking for new key."); + if (!wired_auth_is_optional(self)) + supplicant_interface_release(self); + + nm_device_state_changed(device, + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + wired_secrets_get_secrets(self, setting_name, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW); + + return G_SOURCE_REMOVE; + +time_out: + _LOGW(LOGD_DEVICE | LOGD_ETHER, "link timed out."); + wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + + return G_SOURCE_REMOVE; +} + +static NMSupplicantConfig * +build_supplicant_config(NMDeviceEthernet *self, GError **error) +{ + const char * con_uuid; + NMSupplicantConfig *config = NULL; + NMSetting8021x * security; + NMConnection * connection; + guint32 mtu; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + g_return_val_if_fail(connection, NULL); + + con_uuid = nm_connection_get_uuid(connection); + mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), + nm_device_get_ifindex(NM_DEVICE(self))); + + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + + security = nm_connection_get_setting_802_1x(connection); + if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) { + g_prefix_error(error, "802-1x-setting: "); + g_clear_object(&config); + } + + return config; +} + +static void +supplicant_iface_state_is_completed(NMDeviceEthernet *self, NMSupplicantInterfaceState state) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + if (state == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) { + nm_clear_g_source(&priv->supplicant.lnk_timeout_id); + nm_clear_g_source(&priv->supplicant.con_timeout_id); + + /* If this is the initial association during device activation, + * schedule the next activation stage. + */ + if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) { + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) Stage 2 of 5 (Device Configure) successful."); + nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self)); + } + return; + } + + if (!priv->supplicant.lnk_timeout_id && !priv->supplicant.con_timeout_id) + priv->supplicant.lnk_timeout_id = + g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self); +} + +static void +supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data) +{ + NMDeviceEthernet * self; + NMDeviceEthernetPrivate *priv; + + if (nm_utils_error_is_cancelled_or_disposing(error)) + return; + + self = NM_DEVICE_ETHERNET(user_data); + priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + if (error) { + supplicant_interface_release(self); + nm_device_queue_state(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED); + return; + } + + nm_assert(!priv->supplicant.lnk_timeout_id); + nm_assert(!priv->supplicant.is_associated); + + priv->supplicant.is_associated = TRUE; + supplicant_iface_state_is_completed(self, + nm_supplicant_interface_get_state(priv->supplicant.iface)); +} + +static gboolean +supplicant_iface_start(NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + gs_unref_object NMSupplicantConfig *config = NULL; + gs_free_error GError *error = NULL; + + config = build_supplicant_config(self, &error); + if (!config) { + _LOGE(LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) couldn't build security configuration: %s", + error->message); + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED); + return FALSE; + } + + nm_supplicant_interface_disconnect(priv->supplicant.iface); + nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self); + return TRUE; +} + +static void +supplicant_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(user_data); + NMDeviceEthernetPrivate * priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSupplicantInterfaceState new_state = new_state_i; + NMSupplicantInterfaceState old_state = old_state_i; + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "supplicant interface state: %s -> %s", + nm_supplicant_interface_state_to_string(old_state), + nm_supplicant_interface_state_to_string(new_state)); + + if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) { + supplicant_interface_release(self); + wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + if (!supplicant_iface_start(self)) + return; + } + + if (priv->supplicant.is_associated) + supplicant_iface_state_is_completed(self, new_state); +} + +static gboolean +handle_auth_or_fail(NMDeviceEthernet *self, NMActRequest *req, gboolean new_secrets) +{ + const char * setting_name; + NMConnection *applied_connection; + + if (!nm_device_auth_retries_try_next(NM_DEVICE(self))) + return FALSE; + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_NONE); + + nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req)); + + applied_connection = nm_act_request_get_applied_connection(req); + setting_name = nm_connection_need_secrets(applied_connection, NULL); + if (!setting_name) { + _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets."); + return FALSE; + } + + _LOGI(LOGD_DEVICE | LOGD_ETHER, "Activation: (ethernet) asking for new secrets"); + + /* Don't tear down supplicant if the authentication is optional + * because in case of a failure in getting new secrets we want to + * keep the supplicant alive. + */ + if (!wired_auth_is_optional(self)) + supplicant_interface_release(self); + + wired_secrets_get_secrets( + self, + setting_name, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0)); + return TRUE; +} + +static gboolean +supplicant_connection_timeout_cb(gpointer user_data) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(user_data); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMActRequest * req; + NMSettingsConnection * connection; + guint64 timestamp = 0; + gboolean new_secrets = TRUE; + + priv->supplicant.con_timeout_id = 0; + + /* Authentication failed; either driver problems, the encryption key is + * wrong, the passwords or certificates were wrong or the Ethernet switch's + * port is not configured for 802.1x. */ + _LOGW(LOGD_DEVICE | LOGD_ETHER, "Activation: (ethernet) association took too long."); + + req = nm_device_get_act_request(device); + connection = nm_act_request_get_settings_connection(req); + + /* Ask for new secrets only if we've never activated this connection + * before. If we've connected before, don't bother the user with dialogs, + * just retry or fail, and if we never connect the user can fix the + * password somewhere else. */ + if (nm_settings_connection_get_timestamp(connection, ×tamp)) + new_secrets = !timestamp; + + if (!handle_auth_or_fail(self, req, new_secrets)) { + wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_NO_SECRETS); + return G_SOURCE_REMOVE; + } + + if (!priv->supplicant.lnk_timeout_id && priv->supplicant.iface) { + NMSupplicantInterfaceState state; + + state = nm_supplicant_interface_get_state(priv->supplicant.iface); + if (state != NM_SUPPLICANT_INTERFACE_STATE_COMPLETED + && nm_supplicant_interface_state_is_operational(state)) + priv->supplicant.lnk_timeout_id = + g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self); + } + + return G_SOURCE_REMOVE; +} + +static void +supplicant_interface_create_cb(NMSupplicantManager * supplicant_manager, + NMSupplMgrCreateIfaceHandle *handle, + NMSupplicantInterface * iface, + GError * error, + gpointer user_data) +{ + NMDeviceEthernet * self; + NMDeviceEthernetPrivate *priv; + guint timeout; + + if (nm_utils_error_is_cancelled(error)) + return; + + self = user_data; + priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + nm_assert(priv->supplicant.create_handle == handle); + priv->supplicant.create_handle = NULL; + + if (error) { + _LOGE(LOGD_DEVICE | LOGD_ETHER, + "Couldn't initialize supplicant interface: %s", + error->message); + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + priv->supplicant.iface = g_object_ref(iface); + priv->supplicant.is_associated = FALSE; + + priv->supplicant.iface_state_id = g_signal_connect(priv->supplicant.iface, + NM_SUPPLICANT_INTERFACE_STATE, + G_CALLBACK(supplicant_iface_state_cb), + self); + + timeout = nm_device_get_supplicant_timeout(NM_DEVICE(self)); + priv->supplicant.con_timeout_id = + g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self); + + if (nm_supplicant_interface_state_is_operational(nm_supplicant_interface_get_state(iface))) + supplicant_iface_start(self); +} + +static NMPlatformLinkDuplexType +link_duplex_to_platform(const char *duplex) +{ + if (!duplex) + return NM_PLATFORM_LINK_DUPLEX_UNKNOWN; + if (nm_streq(duplex, "full")) + return NM_PLATFORM_LINK_DUPLEX_FULL; + if (nm_streq(duplex, "half")) + return NM_PLATFORM_LINK_DUPLEX_HALF; + g_return_val_if_reached(NM_PLATFORM_LINK_DUPLEX_UNKNOWN); +} + +static void +link_negotiation_set(NMDevice *device) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSettingWired * s_wired; + gboolean autoneg = TRUE; + gboolean link_autoneg; + NMPlatformLinkDuplexType duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN; + NMPlatformLinkDuplexType link_duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN; + guint32 speed = 0; + guint32 link_speed; + + s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED); + if (s_wired) { + autoneg = nm_setting_wired_get_auto_negotiate(s_wired); + speed = nm_setting_wired_get_speed(s_wired); + duplex = link_duplex_to_platform(nm_setting_wired_get_duplex(s_wired)); + if (!autoneg && !speed && !duplex) { + _LOGD(LOGD_DEVICE, "set-link: ignore link negotiation"); + return; + } + } + + if (!nm_platform_ethtool_get_link_settings(nm_device_get_platform(device), + nm_device_get_ifindex(device), + &link_autoneg, + &link_speed, + &link_duplex)) { + _LOGW(LOGD_DEVICE, "set-link: unable to retrieve link negotiation"); + return; + } + + /* If link negotiation setting are already in place do nothing and return with success */ + if (!!autoneg == !!link_autoneg && speed == link_speed && duplex == link_duplex) { + _LOGD(LOGD_DEVICE, "set-link: link negotiation is already configured"); + return; + } + + if (autoneg && !speed && !duplex) + _LOGD(LOGD_DEVICE, "set-link: configure auto-negotiation"); + else { + _LOGD(LOGD_DEVICE, + "set-link: configure %snegotiation (%u Mbit%s, %s duplex%s)", + autoneg ? "auto-" : "static ", + speed ?: link_speed, + speed ? "" : "*", + duplex ? nm_platform_link_duplex_type_to_string(duplex) + : nm_platform_link_duplex_type_to_string(link_duplex), + duplex ? "" : "*"); + } + + if (!priv->ethtool_prev_set) { + /* remember the values we had before setting it. */ + priv->ethtool_prev_autoneg = link_autoneg; + priv->ethtool_prev_speed = link_speed; + priv->ethtool_prev_duplex = link_duplex; + priv->ethtool_prev_set = TRUE; + } + + if (!nm_platform_ethtool_set_link_settings(nm_device_get_platform(device), + nm_device_get_ifindex(device), + autoneg, + speed, + duplex)) { + _LOGW(LOGD_DEVICE, "set-link: failure to set link negotiation"); + return; + } +} + +static gboolean +pppoe_reconnect_delay(gpointer user_data) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(user_data); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + priv->pppoe_wait_id = 0; + priv->last_pppoe_time = 0; + _LOGI(LOGD_DEVICE, "PPPoE reconnect delay complete, resuming connection..."); + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); + return G_SOURCE_REMOVE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + if (nm_device_sys_iface_state_is_external_or_assume(device)) { + if (!priv->ethtool_prev_set && !nm_device_sys_iface_state_is_external(device)) { + NMSettingWired *s_wired; + + /* During restart of NetworkManager service we forget the original auto + * negotiation settings. When taking over a device, remember to reset + * the "default" during deactivate. */ + s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED); + if (s_wired + && (nm_setting_wired_get_auto_negotiate(s_wired) + || nm_setting_wired_get_speed(s_wired) + || nm_setting_wired_get_duplex(s_wired))) { + priv->ethtool_prev_set = TRUE; + priv->ethtool_prev_autoneg = TRUE; + priv->ethtool_prev_speed = 0; + priv->ethtool_prev_duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN; + } + } + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + link_negotiation_set(device); + + /* If we're re-activating a PPPoE connection a short while after + * a previous PPPoE connection was torn down, wait a bit to allow the + * remote side to handle the disconnection. Otherwise, the peer may + * get confused and fail to negotiate the new connection. (rh #1023503) + * + * FIXME(shutdown): when exiting, we also need to wait before quitting, + * at least for additional NM_SHUTDOWN_TIMEOUT_MS seconds because + * otherwise after restart the device won't work for the first seconds. + */ + if (priv->last_pppoe_time != 0) { + gint32 delay = nm_utils_get_monotonic_timestamp_sec() - priv->last_pppoe_time; + + if (delay < PPPOE_RECONNECT_DELAY + && nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE)) { + if (priv->pppoe_wait_id == 0) { + _LOGI(LOGD_DEVICE, + "delaying PPPoE reconnect for %d seconds to ensure peer is ready...", + delay); + priv->pppoe_wait_id = g_timeout_add_seconds(delay, pppoe_reconnect_delay, self); + } + return NM_ACT_STAGE_RETURN_POSTPONE; + } + nm_clear_g_source(&priv->pppoe_wait_id); + priv->last_pppoe_time = 0; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +supplicant_check_secrets_needed(NMDeviceEthernet *self, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMConnection * connection; + NMSetting8021x * security; + const char * setting_name; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + security = nm_connection_get_setting_802_1x(connection); + if (!security) { + _LOGE(LOGD_DEVICE, "Invalid or missing 802.1X security"); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (!priv->supplicant.mgr) + priv->supplicant.mgr = g_object_ref(nm_supplicant_manager_get()); + + /* If we need secrets, get them */ + setting_name = nm_connection_need_secrets(connection, NULL); + if (setting_name) { + NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self)); + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) connection '%s' has security, but secrets are required.", + nm_connection_get_id(connection)); + + if (!handle_auth_or_fail(self, req, FALSE)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + return NM_ACT_STAGE_RETURN_FAILURE; + } + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: (ethernet) connection '%s' requires no security. No secrets needed.", + nm_connection_get_id(connection)); + + supplicant_interface_release(self); + + priv->supplicant.create_handle = + nm_supplicant_manager_create_interface(priv->supplicant.mgr, + nm_device_get_ifindex(NM_DEVICE(self)), + NM_SUPPLICANT_DRIVER_WIRED, + supplicant_interface_create_cb, + self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +carrier_changed(NMSupplicantInterface *iface, GParamSpec *pspec, NMDeviceEthernet *self) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMDeviceStateReason reason; + NMActStageReturn ret; + + if (!nm_device_has_carrier(NM_DEVICE(self))) + return; + + _LOGD(LOGD_DEVICE | LOGD_ETHER, "got carrier, initializing supplicant"); + nm_clear_g_signal_handler(self, &priv->carrier_id); + ret = supplicant_check_secrets_needed(self, &reason); + if (ret == NM_ACT_STAGE_RETURN_FAILURE) { + nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason); + } +} + +/*****************************************************************************/ +/* PPPoE */ + +static void +ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + switch (status) { + case NM_PPP_STATUS_DISCONNECT: + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_PPP_DISCONNECT); + break; + case NM_PPP_STATUS_DEAD: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED); + break; + default: + break; + } +} + +static void +ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + if (!nm_device_set_ip_ifindex(device, ifindex)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } +} + +static void +ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + /* Ignore PPP IP4 events that come in after initial configuration */ + if (nm_device_activate_ip4_state_in_conf(device)) + nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config)); +} + +static NMActStageReturn +pppoe_stage3_ip4_config_start(NMDeviceEthernet *self, NMDeviceStateReason *out_failure_reason) +{ + NMDevice * device = NM_DEVICE(self); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSettingPppoe * s_pppoe; + NMActRequest * req; + GError * err = NULL; + + req = nm_device_get_act_request(device); + + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + s_pppoe = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE); + + g_return_val_if_fail(s_pppoe, NM_ACT_STAGE_RETURN_FAILURE); + + priv->ppp_manager = nm_ppp_manager_create(nm_device_get_iface(device), &err); + + if (priv->ppp_manager) { + nm_ppp_manager_set_route_parameters(priv->ppp_manager, + nm_device_get_route_table(device, AF_INET), + nm_device_get_route_metric(device, AF_INET), + nm_device_get_route_table(device, AF_INET6), + nm_device_get_route_metric(device, AF_INET6)); + } + + if (!priv->ppp_manager + || !nm_ppp_manager_start(priv->ppp_manager, + req, + nm_setting_pppoe_get_username(s_pppoe), + 30, + 0, + &err)) { + _LOGW(LOGD_DEVICE, "PPPoE failed to start: %s", err->message); + g_error_free(err); + + g_clear_object(&priv->ppp_manager); + + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_STATE_CHANGED, + G_CALLBACK(ppp_state_changed), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IFINDEX_SET, + G_CALLBACK(ppp_ifindex_set), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IP4_CONFIG, + G_CALLBACK(ppp_ip4_config), + self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static void dcb_state(NMDevice *device, gboolean timeout); + +static gboolean +dcb_carrier_timeout(gpointer user_data) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(user_data); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(user_data); + + g_return_val_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG, G_SOURCE_REMOVE); + + priv->dcb_timeout_id = 0; + if (priv->dcb_wait != DCB_WAIT_CARRIER_POSTCONFIG_DOWN) { + _LOGW(LOGD_DCB, "DCB: timed out waiting for carrier (step %d)", priv->dcb_wait); + } + dcb_state(device, TRUE); + return G_SOURCE_REMOVE; +} + +static gboolean +dcb_configure(NMDevice *device) +{ + NMDeviceEthernet * self = (NMDeviceEthernet *) device; + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSettingDcb * s_dcb; + GError * error = NULL; + + nm_clear_g_source(&priv->dcb_timeout_id); + + s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB); + + g_return_val_if_fail(s_dcb, FALSE); + + if (!nm_dcb_setup(nm_device_get_iface(device), s_dcb, &error)) { + _LOGW(LOGD_DCB, "Activation: (ethernet) failed to enable DCB/FCoE: %s", error->message); + g_clear_error(&error); + return FALSE; + } + + /* Pause again just in case the device takes the carrier down when + * setting specific DCB attributes. + */ + _LOGD(LOGD_DCB, "waiting for carrier (postconfig down)"); + priv->dcb_wait = DCB_WAIT_CARRIER_POSTCONFIG_DOWN; + priv->dcb_timeout_id = g_timeout_add_seconds(3, dcb_carrier_timeout, device); + return TRUE; +} + +static gboolean +dcb_enable(NMDevice *device) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + GError * error = NULL; + + nm_clear_g_source(&priv->dcb_timeout_id); + if (!nm_dcb_enable(nm_device_get_iface(device), TRUE, &error)) { + _LOGW(LOGD_DCB, "Activation: (ethernet) failed to enable DCB/FCoE: %s", error->message); + g_clear_error(&error); + return FALSE; + } + + /* Pause for 3 seconds after enabling DCB to let the card reconfigure + * itself. Drivers will often re-initialize internal settings which + * takes the carrier down for 2 or more seconds. During this time, + * lldpad will refuse to do anything else with the card since the carrier + * is down. But NM might get the carrier-down signal long after calling + * "dcbtool dcb on", so we have to first wait for the carrier to go down. + */ + _LOGD(LOGD_DCB, "waiting for carrier (preconfig down)"); + priv->dcb_wait = DCB_WAIT_CARRIER_PRECONFIG_DOWN; + priv->dcb_timeout_id = g_timeout_add_seconds(3, dcb_carrier_timeout, device); + return TRUE; +} + +static void +dcb_state(NMDevice *device, gboolean timeout) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + gboolean carrier; + + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG); + + carrier = nm_platform_link_is_connected(nm_device_get_platform(device), + nm_device_get_ifindex(device)); + _LOGD(LOGD_DCB, "dcb_state() wait %d carrier %d timeout %d", priv->dcb_wait, carrier, timeout); + + switch (priv->dcb_wait) { + case DCB_WAIT_CARRIER_PREENABLE_UP: + if (timeout || carrier) { + _LOGD(LOGD_DCB, "dcb_state() enabling DCB"); + nm_clear_g_source(&priv->dcb_timeout_id); + if (!dcb_enable(device)) { + priv->dcb_handle_carrier_changes = FALSE; + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED); + } + } + break; + case DCB_WAIT_CARRIER_PRECONFIG_DOWN: + nm_clear_g_source(&priv->dcb_timeout_id); + priv->dcb_wait = DCB_WAIT_CARRIER_PRECONFIG_UP; + + if (!carrier) { + /* Wait for the carrier to come back up */ + _LOGD(LOGD_DCB, "waiting for carrier (preconfig up)"); + priv->dcb_timeout_id = g_timeout_add_seconds(5, dcb_carrier_timeout, device); + break; + } + _LOGD(LOGD_DCB, "dcb_state() preconfig down falling through"); + /* fall-through */ + case DCB_WAIT_CARRIER_PRECONFIG_UP: + if (timeout || carrier) { + _LOGD(LOGD_DCB, "dcb_state() preconfig up configuring DCB"); + nm_clear_g_source(&priv->dcb_timeout_id); + if (!dcb_configure(device)) { + priv->dcb_handle_carrier_changes = FALSE; + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED); + } + } + break; + case DCB_WAIT_CARRIER_POSTCONFIG_DOWN: + nm_clear_g_source(&priv->dcb_timeout_id); + priv->dcb_wait = DCB_WAIT_CARRIER_POSTCONFIG_UP; + + if (!carrier) { + /* Wait for the carrier to come back up */ + _LOGD(LOGD_DCB, "waiting for carrier (postconfig up)"); + priv->dcb_timeout_id = g_timeout_add_seconds(5, dcb_carrier_timeout, device); + break; + } + _LOGD(LOGD_DCB, "dcb_state() postconfig down falling through"); + /* fall-through */ + case DCB_WAIT_CARRIER_POSTCONFIG_UP: + if (timeout || carrier) { + _LOGD(LOGD_DCB, "dcb_state() postconfig up starting IP"); + nm_clear_g_source(&priv->dcb_timeout_id); + priv->dcb_handle_carrier_changes = FALSE; + priv->dcb_wait = DCB_WAIT_UNKNOWN; + nm_device_activate_schedule_stage3_ip_config_start(device); + } + break; + default: + g_assert_not_reached(); + } +} + +/*****************************************************************************/ + +static gboolean +wake_on_lan_enable(NMDevice *device) +{ + NMSettingWiredWakeOnLan wol; + NMSettingWired * s_wired; + const char * password = NULL; + + s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED); + + if (NM_IS_DEVICE_VETH(device)) + return FALSE; + + if (s_wired) { + wol = nm_setting_wired_get_wake_on_lan(s_wired); + password = nm_setting_wired_get_wake_on_lan_password(s_wired); + if (wol != NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT) + goto found; + } + + wol = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ethernet.wake-on-lan"), + device, + NM_SETTING_WIRED_WAKE_ON_LAN_NONE, + G_MAXINT32, + NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT); + + if (NM_FLAGS_ANY(wol, NM_SETTING_WIRED_WAKE_ON_LAN_EXCLUSIVE_FLAGS) + && !nm_utils_is_power_of_two(wol)) { + nm_log_dbg(LOGD_ETHER, "invalid default value %u for wake-on-lan", (guint) wol); + wol = NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT; + } + if (wol != NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT) + goto found; + wol = NM_SETTING_WIRED_WAKE_ON_LAN_IGNORE; +found: + return nm_platform_ethtool_set_wake_on_lan(nm_device_get_platform(device), + nm_device_get_ifindex(device), + _NM_SETTING_WIRED_WAKE_ON_LAN_CAST(wol), + password); +} + +/*****************************************************************************/ + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceEthernet * self = (NMDeviceEthernet *) device; + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSettingConnection * s_con; + const char * connection_type; + gboolean do_postpone = FALSE; + NMSettingDcb * s_dcb; + + s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION); + + g_return_val_if_fail(s_con, NM_ACT_STAGE_RETURN_FAILURE); + + nm_clear_g_source(&priv->dcb_timeout_id); + priv->dcb_handle_carrier_changes = FALSE; + + /* 802.1x has to run before any IP configuration since the 802.1x auth + * process opens the port up for normal traffic. + */ + connection_type = nm_setting_connection_get_connection_type(s_con); + if (nm_streq(connection_type, NM_SETTING_WIRED_SETTING_NAME)) { + NMSetting8021x *security; + + security = nm_device_get_applied_setting(device, NM_TYPE_SETTING_802_1X); + + if (security) { + /* FIXME: for now 802.1x is mutually exclusive with DCB */ + if (!nm_device_has_carrier(NM_DEVICE(self))) { + _LOGD(LOGD_DEVICE | LOGD_ETHER, + "delay supplicant initialization until carrier goes up"); + priv->carrier_id = g_signal_connect(self, + "notify::" NM_DEVICE_CARRIER, + G_CALLBACK(carrier_changed), + self); + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + return supplicant_check_secrets_needed(self, out_failure_reason); + } + } + + wake_on_lan_enable(device); + + /* DCB and FCoE setup */ + s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB); + if (s_dcb) { + /* lldpad really really wants the carrier to be up */ + if (nm_platform_link_is_connected(nm_device_get_platform(device), + nm_device_get_ifindex(device))) { + if (!dcb_enable(device)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } else { + _LOGD(LOGD_DCB, "waiting for carrier (preenable up)"); + priv->dcb_wait = DCB_WAIT_CARRIER_PREENABLE_UP; + priv->dcb_timeout_id = g_timeout_add_seconds(4, dcb_carrier_timeout, device); + } + + priv->dcb_handle_carrier_changes = TRUE; + do_postpone = TRUE; + } + + /* PPPoE setup */ + if (nm_connection_is_type(nm_device_get_applied_connection(device), + NM_SETTING_PPPOE_SETTING_NAME)) { + NMSettingPpp *s_ppp; + + s_ppp = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPP); + if (s_ppp) { + guint32 mtu; + guint32 mru; + guint32 mxu; + + mtu = nm_setting_ppp_get_mtu(s_ppp); + mru = nm_setting_ppp_get_mru(s_ppp); + mxu = MAX(mru, mtu); + if (mxu) { + _LOGD(LOGD_PPP, + "set MTU to %u (PPP interface MRU %u, MTU %u)", + mxu + PPPOE_ENCAP_OVERHEAD, + mru, + mtu); + nm_platform_link_set_mtu(nm_device_get_platform(device), + nm_device_get_ifindex(device), + mxu + PPPOE_ENCAP_OVERHEAD); + } + } + } + + return do_postpone ? NM_ACT_STAGE_RETURN_POSTPONE : NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + NMSettingConnection *s_con; + const char * connection_type; + int ifindex; + + ifindex = nm_device_get_ifindex(device); + + if (ifindex <= 0) + return NM_ACT_STAGE_RETURN_FAILURE; + + if (addr_family == AF_INET) { + s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION); + + g_return_val_if_fail(s_con, NM_ACT_STAGE_RETURN_FAILURE); + + connection_type = nm_setting_connection_get_connection_type(s_con); + if (!strcmp(connection_type, NM_SETTING_PPPOE_SETTING_NAME)) + return pppoe_stage3_ip4_config_start(NM_DEVICE_ETHERNET(device), out_failure_reason); + } + + return NM_DEVICE_CLASS(nm_device_ethernet_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + /* MTU only set for plain ethernet */ + if (NM_DEVICE_ETHERNET_GET_PRIVATE(device)->ppp_manager) + return 0; + + return nm_device_get_configured_mtu_for_wired(device, out_source, out_force); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + NMSettingDcb * s_dcb; + GError * error = NULL; + int ifindex; + + nm_clear_g_source(&priv->pppoe_wait_id); + nm_clear_g_signal_handler(self, &priv->carrier_id); + + if (priv->ppp_manager) { + nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL); + g_clear_object(&priv->ppp_manager); + } + + supplicant_interface_release(self); + + priv->dcb_wait = DCB_WAIT_UNKNOWN; + nm_clear_g_source(&priv->dcb_timeout_id); + priv->dcb_handle_carrier_changes = FALSE; + + /* Tear down DCB/FCoE if it was enabled */ + s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB); + if (s_dcb) { + if (!nm_dcb_cleanup(nm_device_get_iface(device), &error)) { + _LOGW(LOGD_DEVICE | LOGD_PLATFORM, "failed to disable DCB/FCoE: %s", error->message); + g_clear_error(&error); + } + } + + /* Set last PPPoE connection time */ + if (nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE)) + priv->last_pppoe_time = nm_utils_get_monotonic_timestamp_sec(); + + ifindex = nm_device_get_ifindex(device); + if (ifindex > 0 && priv->ethtool_prev_set) { + priv->ethtool_prev_set = FALSE; + + _LOGD(LOGD_DEVICE, + "set-link: reset %snegotiation (%u Mbit, %s duplex)", + priv->ethtool_prev_autoneg ? "auto-" : "static ", + priv->ethtool_prev_speed, + nm_platform_link_duplex_type_to_string(priv->ethtool_prev_duplex)); + if (!nm_platform_ethtool_set_link_settings(nm_device_get_platform(device), + ifindex, + priv->ethtool_prev_autoneg, + priv->ethtool_prev_speed, + priv->ethtool_prev_duplex)) { + _LOGW(LOGD_DEVICE, "set-link: failure to reset link negotiation"); + return; + } + } +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingWired *s_wired; + NMSettingPppoe *s_pppoe; + + if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_VETH_SETTING_NAME)) { + NMSettingVeth *s_veth; + const char * peer_name = NULL; + const char * con_peer_name = NULL; + int ifindex; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_VETH_SETTING_NAME, + existing_connections, + NULL, + _("Veth connection"), + "veth", + NULL, + TRUE); + + s_veth = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VETH); + if (!s_veth) { + s_veth = (NMSettingVeth *) nm_setting_veth_new(); + nm_connection_add_setting(connection, NM_SETTING(s_veth)); + } + + ifindex = nm_device_get_ip_ifindex(device); + if (ifindex > 0) { + const NMPlatformLink *pllink; + + pllink = nm_platform_link_get(nm_device_get_platform(device), ifindex); + if (pllink && pllink->type == NM_LINK_TYPE_VETH && pllink->parent > 0) { + pllink = nm_platform_link_get(nm_device_get_platform(device), pllink->parent); + + if (pllink && pllink->type == NM_LINK_TYPE_VETH) { + peer_name = pllink->name; + } + } + } + + if (!peer_name) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "cannot find peer for veth device"); + return FALSE; + } + + con_peer_name = nm_setting_veth_get_peer(s_veth); + if (con_peer_name) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "mismatching veth peer \"%s\"", + con_peer_name); + return FALSE; + } else + g_object_set(s_veth, NM_SETTING_VETH_PEER, peer_name, NULL); + + return TRUE; + } + + s_pppoe = nm_connection_get_setting_pppoe(connection); + + /* We can't telepathically figure out the service name or username, so if + * those weren't given, we can't complete the connection. + */ + if (s_pppoe && !nm_setting_verify(NM_SETTING(s_pppoe), NULL, error)) + return FALSE; + + s_wired = nm_connection_get_setting_wired(connection); + if (!s_wired) { + s_wired = (NMSettingWired *) nm_setting_wired_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wired)); + } + + /* Default to an ethernet-only connection, but if a PPPoE setting was given + * then PPPoE should be our connection type. + */ + nm_utils_complete_generic( + nm_device_get_platform(device), + connection, + s_pppoe ? NM_SETTING_PPPOE_SETTING_NAME : NM_SETTING_WIRED_SETTING_NAME, + existing_connections, + NULL, + s_pppoe ? _("PPPoE connection") : _("Wired connection"), + NULL, + nm_setting_wired_get_mac_address(s_wired) ? NULL : nm_device_get_iface(device), + s_pppoe ? FALSE : TRUE); /* No IPv6 by default yet for PPPoE */ + + return TRUE; +} + +static NMConnection * +new_default_connection(NMDevice *self) +{ + NMConnection * connection; + NMSettingsConnection *const *connections; + NMSetting * setting; + gs_unref_hashtable GHashTable *existing_ids = NULL; + struct udev_device * dev; + const char * perm_hw_addr; + const char * iface; + const char * uprop = "0"; + gs_free char * defname = NULL; + gs_free char * uuid = NULL; + guint i, n_connections; + + perm_hw_addr = nm_device_get_permanent_hw_address(self); + iface = nm_device_get_iface(self); + + connection = nm_simple_connection_new(); + setting = nm_setting_connection_new(); + nm_connection_add_setting(connection, setting); + + connections = nm_settings_get_connections(nm_device_get_settings(self), &n_connections); + if (n_connections > 0) { + existing_ids = g_hash_table_new(nm_str_hash, g_str_equal); + for (i = 0; i < n_connections; i++) + g_hash_table_add(existing_ids, (char *) nm_settings_connection_get_id(connections[i])); + } + defname = nm_device_ethernet_utils_get_default_wired_name(existing_ids); + if (!defname) + return NULL; + + /* Create a stable UUID. The UUID is also the Network_ID for stable-privacy addr-gen-mode, + * thus when it changes we will also generate different IPv6 addresses. */ + uuid = _nm_utils_uuid_generate_from_strings("default-wired", + nm_utils_machine_id_str(), + defname, + perm_hw_addr ?: iface, + NULL); + + g_object_set(setting, + NM_SETTING_CONNECTION_ID, + defname, + NM_SETTING_CONNECTION_TYPE, + NM_SETTING_WIRED_SETTING_NAME, + NM_SETTING_CONNECTION_AUTOCONNECT, + TRUE, + NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY, + NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY_MIN, + NM_SETTING_CONNECTION_UUID, + uuid, + NM_SETTING_CONNECTION_TIMESTAMP, + (guint64) time(NULL), + NM_SETTING_CONNECTION_INTERFACE_NAME, + iface, + NULL); + + /* Check if we should create a Link-Local only connection */ + dev = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)), + nm_device_get_ip_ifindex(self)); + if (dev) + uprop = udev_device_get_property_value(dev, "NM_AUTO_DEFAULT_LINK_LOCAL_ONLY"); + + if (nm_udev_utils_property_as_boolean(uprop)) { + setting = nm_setting_ip4_config_new(); + g_object_set(setting, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL, + NULL); + nm_connection_add_setting(connection, setting); + + setting = nm_setting_ip6_config_new(); + g_object_set(setting, + NM_SETTING_IP_CONFIG_METHOD, + NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL, + NM_SETTING_IP_CONFIG_MAY_FAIL, + TRUE, + NULL); + nm_connection_add_setting(connection, setting); + } + + return connection; +} + +static const char * +get_s390_subchannels(NMDevice *device) +{ + nm_assert(NM_IS_DEVICE_ETHERNET(device)); + + return NM_DEVICE_ETHERNET_GET_PRIVATE(device)->subchannels; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(device); + NMSettingWired * s_wired = nm_connection_get_setting_wired(connection); + gboolean perm_hw_addr_is_fake; + const char * perm_hw_addr; + const char * mac = nm_device_get_hw_address(device); + const char * mac_prop = NM_SETTING_WIRED_MAC_ADDRESS; + GHashTableIter iter; + gpointer key, value; + + if (!s_wired) { + s_wired = (NMSettingWired *) nm_setting_wired_new(); + nm_connection_add_setting(connection, (NMSetting *) s_wired); + } + + g_object_set(nm_connection_get_setting_connection(connection), + NM_SETTING_CONNECTION_TYPE, + nm_connection_get_setting_pppoe(connection) ? NM_SETTING_PPPOE_SETTING_NAME + : NM_SETTING_WIRED_SETTING_NAME, + NULL); + + /* If the device reports a permanent address, use that for the MAC address + * and the current MAC, if different, is the cloned MAC. + */ + perm_hw_addr = nm_device_get_permanent_hw_address_full(device, TRUE, &perm_hw_addr_is_fake); + if (perm_hw_addr && !perm_hw_addr_is_fake) { + g_object_set(s_wired, NM_SETTING_WIRED_MAC_ADDRESS, perm_hw_addr, NULL); + + mac_prop = NULL; + if (mac && !nm_utils_hwaddr_matches(perm_hw_addr, -1, mac, -1)) + mac_prop = NM_SETTING_WIRED_CLONED_MAC_ADDRESS; + } + + if (mac_prop && mac && nm_utils_hwaddr_valid(mac, ETH_ALEN)) + g_object_set(s_wired, mac_prop, mac, NULL); + + /* We don't set the MTU as we don't know whether it was set explicitly */ + + /* s390 */ + if (priv->subchannels_dbus) + g_object_set(s_wired, NM_SETTING_WIRED_S390_SUBCHANNELS, priv->subchannels_dbus, NULL); + if (priv->s390_nettype) + g_object_set(s_wired, NM_SETTING_WIRED_S390_NETTYPE, priv->s390_nettype, NULL); + + _nm_setting_wired_clear_s390_options(s_wired); + g_hash_table_iter_init(&iter, priv->s390_options); + while (g_hash_table_iter_next(&iter, &key, &value)) + nm_setting_wired_add_s390_option(s_wired, (const char *) key, (const char *) value); +} + +static void +link_speed_update(NMDevice *device) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + guint32 speed; + + if (!nm_platform_ethtool_get_link_settings(nm_device_get_platform(device), + nm_device_get_ifindex(device), + NULL, + &speed, + NULL)) + return; + if (priv->speed == speed) + return; + + priv->speed = speed; + _LOGD(LOGD_PLATFORM | LOGD_ETHER, "speed is now %d Mb/s", speed); + _notify(self, PROP_SPEED); +} + +static void +carrier_changed_notify(NMDevice *device, gboolean carrier) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(device); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + if (priv->dcb_handle_carrier_changes) { + nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG); + + if (priv->dcb_timeout_id) { + _LOGD(LOGD_DCB, "carrier_changed() calling dcb_state()"); + dcb_state(device, FALSE); + } + } + + if (carrier) + link_speed_update(device); + + NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->carrier_changed_notify(device, carrier); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->link_changed(device, pllink); + if (!NM_IS_DEVICE_VETH(device) && pllink->initialized) + _update_s390_subchannels((NMDeviceEthernet *) device); +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + if (!NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->is_available(device, flags)) + return FALSE; + + return !!nm_device_get_initial_hw_address(device); +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + /* Only handle wired setting here, delegate other settings to parent class */ + if (nm_streq(setting_name, NM_SETTING_WIRED_SETTING_NAME)) { + return nm_device_hash_check_invalid_keys( + diffs, + NM_SETTING_WIRED_SETTING_NAME, + error, + NM_SETTING_WIRED_MTU, /* reapplied with IP config */ + NM_SETTING_WIRED_SPEED, + NM_SETTING_WIRED_DUPLEX, + NM_SETTING_WIRED_AUTO_NEGOTIATE, + NM_SETTING_WIRED_WAKE_ON_LAN, + NM_SETTING_WIRED_WAKE_ON_LAN_PASSWORD); + } + + device_class = NM_DEVICE_CLASS(nm_device_ethernet_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static void +reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new) +{ + NMDeviceEthernet *self = NM_DEVICE_ETHERNET(device); + NMDeviceState state = nm_device_get_state(device); + + NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->reapply_connection(device, con_old, con_new); + + _LOGD(LOGD_DEVICE, "reapplying wired settings"); + + if (state >= NM_DEVICE_STATE_PREPARE) + link_negotiation_set(device); + if (state >= NM_DEVICE_STATE_CONFIG) + wake_on_lan_enable(device); +} + +static void +dispose(GObject *object) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(object); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + wired_secrets_cancel(self); + + supplicant_interface_release(self); + + nm_clear_g_source(&priv->pppoe_wait_id); + + nm_clear_g_source(&priv->dcb_timeout_id); + + nm_clear_g_signal_handler(self, &priv->carrier_id); + + G_OBJECT_CLASS(nm_device_ethernet_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(object); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + g_clear_object(&priv->supplicant.mgr); + g_free(priv->subchan1); + g_free(priv->subchan2); + g_free(priv->subchan3); + g_free(priv->subchannels); + g_strfreev(priv->subchannels_dbus); + g_free(priv->s390_nettype); + g_hash_table_destroy(priv->s390_options); + + G_OBJECT_CLASS(nm_device_ethernet_parent_class)->finalize(object); +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceEthernet * self = NM_DEVICE_ETHERNET(object); + NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_SPEED: + g_value_set_uint(value, priv->speed); + break; + case PROP_S390_SUBCHANNELS: + g_value_set_boxed(value, priv->subchannels_dbus); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static const NMDBusInterfaceInfoExtended interface_info_device_wired = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_WIRED, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PermHwAddress", + "s", + NM_DEVICE_PERM_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Speed", + "u", + NM_DEVICE_ETHERNET_SPEED), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("S390Subchannels", + "as", + NM_DEVICE_ETHERNET_S390_SUBCHANNELS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", + "b", + NM_DEVICE_CARRIER), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ethernet_class_init(NMDeviceEthernetClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + g_type_class_add_private(object_class, sizeof(NMDeviceEthernetPrivate)); + + object_class->dispose = dispose; + object_class->finalize = finalize; + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wired); + + device_class->connection_type_supported = NM_SETTING_WIRED_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_ETHERNET); + + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->new_default_connection = new_default_connection; + + device_class->act_stage1_prepare_also_for_external_or_assume = TRUE; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->get_configured_mtu = get_configured_mtu; + device_class->deactivate = deactivate; + device_class->get_s390_subchannels = get_s390_subchannels; + device_class->update_connection = update_connection; + device_class->carrier_changed_notify = carrier_changed_notify; + device_class->link_changed = link_changed; + device_class->is_available = is_available; + device_class->can_reapply_change = can_reapply_change; + device_class->reapply_connection = reapply_connection; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_SPEED] = g_param_spec_uint(NM_DEVICE_ETHERNET_SPEED, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_S390_SUBCHANNELS] = + g_param_spec_boxed(NM_DEVICE_ETHERNET_S390_SUBCHANNELS, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_ETHERNET_DEVICE_FACTORY (nm_ethernet_device_factory_get_type()) +#define NM_ETHERNET_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ETHERNET_DEVICE_FACTORY, NMEthernetDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_ETHERNET, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Ethernet", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_ETHERNET, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_ETHERNET, + NULL); +} + +static gboolean +match_connection(NMDeviceFactory *factory, NMConnection *connection) +{ + const char * type = nm_connection_get_connection_type(connection); + NMSettingPppoe *s_pppoe; + + if (nm_streq(type, NM_SETTING_WIRED_SETTING_NAME)) + return TRUE; + + nm_assert(nm_streq(type, NM_SETTING_PPPOE_SETTING_NAME)); + s_pppoe = nm_connection_get_setting_pppoe(connection); + + return !nm_setting_pppoe_get_parent(s_pppoe); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + ETHERNET, + Ethernet, + ethernet, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_ETHERNET) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIRED_SETTING_NAME, + NM_SETTING_PPPOE_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->match_connection = match_connection;); diff --git a/src/core/devices/nm-device-ethernet.h b/src/core/devices/nm-device-ethernet.h new file mode 100644 index 00000000..6e134d71 --- /dev/null +++ b/src/core/devices/nm-device-ethernet.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2010 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_ETHERNET_H__ +#define __NETWORKMANAGER_DEVICE_ETHERNET_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_ETHERNET (nm_device_ethernet_get_type()) +#define NM_DEVICE_ETHERNET(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernet)) +#define NM_DEVICE_ETHERNET_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetClass)) +#define NM_IS_DEVICE_ETHERNET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ETHERNET)) +#define NM_IS_DEVICE_ETHERNET_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_ETHERNET)) +#define NM_DEVICE_ETHERNET_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetClass)) + +#define NM_DEVICE_ETHERNET_SPEED "speed" +#define NM_DEVICE_ETHERNET_S390_SUBCHANNELS "s390-subchannels" + +struct _NMDeviceEthernetPrivate; + +typedef struct { + NMDevice parent; + struct _NMDeviceEthernetPrivate *_priv; +} NMDeviceEthernet; + +typedef struct { + NMDeviceClass parent; +} NMDeviceEthernetClass; + +GType nm_device_ethernet_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_ETHERNET_H__ */ diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c new file mode 100644 index 00000000..81124a8d --- /dev/null +++ b/src/core/devices/nm-device-factory.c @@ -0,0 +1,413 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 - 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-factory.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <gmodule.h> + +#include "platform/nm-platform.h" +#include "nm-utils.h" +#include "nm-core-internal.h" +#include "nm-setting-bluetooth.h" + +#define PLUGIN_PREFIX "libnm-device-plugin-" + +/*****************************************************************************/ + +enum { DEVICE_ADDED, LAST_SIGNAL }; + +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_ABSTRACT_TYPE(NMDeviceFactory, nm_device_factory, G_TYPE_OBJECT) + +/*****************************************************************************/ + +static void +nm_device_factory_get_supported_types(NMDeviceFactory * factory, + const NMLinkType ** out_link_types, + const char *const **out_setting_types) +{ + g_return_if_fail(NM_IS_DEVICE_FACTORY(factory)); + + NM_DEVICE_FACTORY_GET_CLASS(factory)->get_supported_types(factory, + out_link_types, + out_setting_types); +} + +void +nm_device_factory_start(NMDeviceFactory *factory) +{ + g_return_if_fail(factory != NULL); + + if (NM_DEVICE_FACTORY_GET_CLASS(factory)->start) + NM_DEVICE_FACTORY_GET_CLASS(factory)->start(factory); +} + +NMDevice * +nm_device_factory_create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore, + GError ** error) +{ + NMDeviceFactoryClass *klass; + NMDevice * device; + gboolean ignore = FALSE; + + g_return_val_if_fail(factory, NULL); + g_return_val_if_fail(iface && *iface, NULL); + if (plink) { + g_return_val_if_fail(!connection, NULL); + g_return_val_if_fail(strcmp(iface, plink->name) == 0, NULL); + nm_assert(factory == nm_device_factory_manager_find_factory_for_link_type(plink->type)); + } else if (connection) + nm_assert(factory == nm_device_factory_manager_find_factory_for_connection(connection)); + else + g_return_val_if_reached(NULL); + + klass = NM_DEVICE_FACTORY_GET_CLASS(factory); + if (!klass->create_device) { + g_set_error(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "Device factory %s cannot manage new devices", + G_OBJECT_TYPE_NAME(factory)); + NM_SET_OUT(out_ignore, FALSE); + return NULL; + } + + device = klass->create_device(factory, iface, plink, connection, &ignore); + NM_SET_OUT(out_ignore, ignore); + if (!device) { + if (ignore) { + g_set_error(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "Device factory %s ignores device %s", + G_OBJECT_TYPE_NAME(factory), + iface); + } else { + g_set_error(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "Device factory %s failed to create device %s", + G_OBJECT_TYPE_NAME(factory), + iface); + } + } + return device; +} + +const char * +nm_device_factory_get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + g_return_val_if_fail(factory != NULL, NULL); + g_return_val_if_fail(connection != NULL, NULL); + + if (!nm_connection_is_virtual(connection)) + return NULL; + + if (NM_DEVICE_FACTORY_GET_CLASS(factory)->get_connection_parent) + return NM_DEVICE_FACTORY_GET_CLASS(factory)->get_connection_parent(factory, connection); + return NULL; +} + +char * +nm_device_factory_get_connection_iface(NMDeviceFactory *factory, + NMConnection * connection, + const char * parent_iface, + GError ** error) +{ + NMDeviceFactoryClass *klass; + char * ifname; + + g_return_val_if_fail(factory != NULL, NULL); + g_return_val_if_fail(connection != NULL, NULL); + g_return_val_if_fail(!error || !*error, NULL); + + klass = NM_DEVICE_FACTORY_GET_CLASS(factory); + + ifname = g_strdup(nm_connection_get_interface_name(connection)); + if (!ifname && klass->get_connection_iface) + ifname = klass->get_connection_iface(factory, connection, parent_iface); + + if (!ifname) { + g_set_error(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "failed to determine interface name: error determine name for %s", + nm_connection_get_connection_type(connection)); + return NULL; + } + + return ifname; +} + +/*****************************************************************************/ + +static void +nm_device_factory_init(NMDeviceFactory *self) +{} + +static void +nm_device_factory_class_init(NMDeviceFactoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + signals[DEVICE_ADDED] = g_signal_new(NM_DEVICE_FACTORY_DEVICE_ADDED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + NM_TYPE_DEVICE); +} + +/*****************************************************************************/ + +static GHashTable *factories_by_link = NULL; +static GHashTable *factories_by_setting = NULL; + +static void __attribute__((destructor)) _cleanup(void) +{ + nm_clear_pointer(&factories_by_link, g_hash_table_unref); + nm_clear_pointer(&factories_by_setting, g_hash_table_unref); +} + +NMDeviceFactory * +nm_device_factory_manager_find_factory_for_link_type(NMLinkType link_type) +{ + g_return_val_if_fail(factories_by_link, NULL); + + return g_hash_table_lookup(factories_by_link, GUINT_TO_POINTER(link_type)); +} + +NMDeviceFactory * +nm_device_factory_manager_find_factory_for_connection(NMConnection *connection) +{ + NMDeviceFactoryClass *klass; + NMDeviceFactory * factory; + const char * type; + GSList * list; + + g_return_val_if_fail(factories_by_setting, NULL); + + type = nm_connection_get_connection_type(connection); + list = g_hash_table_lookup(factories_by_setting, type); + + for (; list; list = g_slist_next(list)) { + factory = list->data; + klass = NM_DEVICE_FACTORY_GET_CLASS(factory); + if (!klass->match_connection || klass->match_connection(factory, connection)) + return factory; + } + + return NULL; +} + +void +nm_device_factory_manager_for_each_factory(NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data) +{ + GHashTableIter iter; + NMDeviceFactory *factory; + GSList * list_iter, *list = NULL; + + if (factories_by_link) { + g_hash_table_iter_init(&iter, factories_by_link); + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &factory)) { + if (!g_slist_find(list, factory)) + list = g_slist_prepend(list, factory); + } + } + + if (factories_by_setting) { + g_hash_table_iter_init(&iter, factories_by_setting); + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &list_iter)) { + for (; list_iter; list_iter = g_slist_next(list_iter)) { + if (!g_slist_find(list, list_iter->data)) + list = g_slist_prepend(list, list_iter->data); + } + } + } + + for (list_iter = list; list_iter; list_iter = list_iter->next) + callback(list_iter->data, user_data); + + g_slist_free(list); +} + +static gboolean +_add_factory(NMDeviceFactory * factory, + const char * path, + NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data) +{ + const NMLinkType * link_types = NULL; + const char *const *setting_types = NULL; + GSList * list, *list2; + int i; + + g_return_val_if_fail(factories_by_link, FALSE); + g_return_val_if_fail(factories_by_setting, FALSE); + + nm_device_factory_get_supported_types(factory, &link_types, &setting_types); + + g_return_val_if_fail((link_types && link_types[0] > NM_LINK_TYPE_UNKNOWN) + || (setting_types && setting_types[0]), + FALSE); + + for (i = 0; link_types && link_types[i] > NM_LINK_TYPE_UNKNOWN; i++) + g_hash_table_insert(factories_by_link, + GUINT_TO_POINTER(link_types[i]), + g_object_ref(factory)); + for (i = 0; setting_types && setting_types[i]; i++) { + list = g_hash_table_lookup(factories_by_setting, (char *) setting_types[i]); + if (list) { + list2 = g_slist_append(list, g_object_ref(factory)); + nm_assert(list == list2); + } else { + list = g_slist_append(list, g_object_ref(factory)); + g_hash_table_insert(factories_by_setting, (char *) setting_types[i], list); + } + } + + callback(factory, user_data); + + nm_log(path ? LOGL_INFO : LOGL_DEBUG, + LOGD_PLATFORM, + NULL, + NULL, + "Loaded device plugin: %s (%s)", + G_OBJECT_TYPE_NAME(factory), + path ?: "internal"); + return TRUE; +} + +static void +_load_internal_factory(GType factory_gtype, + NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data) +{ + gs_unref_object NMDeviceFactory *factory = NULL; + + factory = g_object_new(factory_gtype, NULL); + _add_factory(factory, NULL, callback, user_data); +} + +static void +factories_list_unref(GSList *list) +{ + g_slist_free_full(list, g_object_unref); +} + +static void +load_factories_from_dir(const char * dirname, + NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data) +{ + NMDeviceFactory *factory; + GError * error = NULL; + char ** path, **paths; + + paths = nm_utils_read_plugin_paths(dirname, PLUGIN_PREFIX); + if (!paths) + return; + + for (path = paths; *path; path++) { + GModule * plugin; + NMDeviceFactoryCreateFunc create_func; + const char * item; + + item = strrchr(*path, '/'); + g_assert(item); + + plugin = g_module_open(*path, G_MODULE_BIND_LOCAL); + + if (!plugin) { + nm_log_warn(LOGD_PLATFORM, "(%s): failed to load plugin: %s", item, g_module_error()); + continue; + } + + if (!g_module_symbol(plugin, "nm_device_factory_create", (gpointer) &create_func)) { + nm_log_warn(LOGD_PLATFORM, + "(%s): failed to find device factory creator: %s", + item, + g_module_error()); + g_module_close(plugin); + continue; + } + + /* after loading glib types from the plugin, we cannot unload the library anymore. + * Make it resident. */ + g_module_make_resident(plugin); + + factory = create_func(&error); + if (!factory) { + nm_log_warn(LOGD_PLATFORM, + "(%s): failed to initialize device factory: %s", + item, + NM_G_ERROR_MSG(error)); + g_clear_error(&error); + continue; + } + g_clear_error(&error); + + _add_factory(factory, g_module_name(plugin), callback, user_data); + + g_object_unref(factory); + } + g_strfreev(paths); +} + +void +nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data) +{ + g_return_if_fail(factories_by_link == NULL); + g_return_if_fail(factories_by_setting == NULL); + + factories_by_link = g_hash_table_new_full(nm_direct_hash, NULL, NULL, g_object_unref); + factories_by_setting = g_hash_table_new_full(nm_str_hash, + g_str_equal, + NULL, + (GDestroyNotify) factories_list_unref); + +#define _ADD_INTERNAL(get_type_fcn) \ + G_STMT_START \ + { \ + GType get_type_fcn(void); \ + _load_internal_factory(get_type_fcn(), callback, user_data); \ + } \ + G_STMT_END + + _ADD_INTERNAL(nm_6lowpan_device_factory_get_type); + _ADD_INTERNAL(nm_bond_device_factory_get_type); + _ADD_INTERNAL(nm_bridge_device_factory_get_type); + _ADD_INTERNAL(nm_dummy_device_factory_get_type); + _ADD_INTERNAL(nm_ethernet_device_factory_get_type); + _ADD_INTERNAL(nm_infiniband_device_factory_get_type); + _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type); + _ADD_INTERNAL(nm_macsec_device_factory_get_type); + _ADD_INTERNAL(nm_macvlan_device_factory_get_type); + _ADD_INTERNAL(nm_ppp_device_factory_get_type); + _ADD_INTERNAL(nm_tun_device_factory_get_type); + _ADD_INTERNAL(nm_veth_device_factory_get_type); + _ADD_INTERNAL(nm_vlan_device_factory_get_type); + _ADD_INTERNAL(nm_vrf_device_factory_get_type); + _ADD_INTERNAL(nm_vxlan_device_factory_get_type); + _ADD_INTERNAL(nm_wireguard_device_factory_get_type); + _ADD_INTERNAL(nm_wpan_device_factory_get_type); + + load_factories_from_dir(NMPLUGINDIR, callback, user_data); +} diff --git a/src/core/devices/nm-device-factory.h b/src/core/devices/nm-device-factory.h new file mode 100644 index 00000000..d9b50563 --- /dev/null +++ b/src/core/devices/nm-device-factory.h @@ -0,0 +1,237 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2007 - 2014 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_FACTORY_H__ +#define __NETWORKMANAGER_DEVICE_FACTORY_H__ + +#include "nm-dbus-interface.h" +#include "nm-device.h" + +/* WARNING: this file is private API between NetworkManager and its internal + * device plugins. Its API can change at any time and is not guaranteed to be + * stable. NM and device plugins are distributed together and this API is + * not meant to enable third-party plugins. + */ + +#define NM_TYPE_DEVICE_FACTORY (nm_device_factory_get_type()) +#define NM_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactory)) +#define NM_DEVICE_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_FACTORY, NMDeviceFactoryClass)) +#define NM_IS_DEVICE_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_FACTORY)) +#define NM_IS_DEVICE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_FACTORY)) +#define NM_DEVICE_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactoryClass)) + +#define NM_DEVICE_FACTORY_DEVICE_ADDED "device-added" + +typedef struct { + GObject parent; +} NMDeviceFactory; + +typedef struct { + GObjectClass parent; + + /** + * get_supported_types: + * @factory: the #NMDeviceFactory + * @out_link_types: on return, a %NM_LINK_TYPE_NONE terminated + * list of #NMLinkType that the plugin supports + * @out_setting_types: on return, a %NULL terminated list of + * base-type #NMSetting names that the plugin can create devices for + * + * Returns the #NMLinkType and #NMSetting names that this plugin + * supports. This function MUST be implemented. + */ + void (*get_supported_types)(NMDeviceFactory * factory, + const NMLinkType ** out_link_types, + const char *const **out_setting_types); + + /** + * start: + * @factory: the #NMDeviceFactory + * + * Start the factory and discover any existing devices that the factory + * can manage. + */ + void (*start)(NMDeviceFactory *factory); + + /** + * match_connection: + * @connection: the #NMConnection + * + * Check if the factory supports the given connection. + */ + gboolean (*match_connection)(NMDeviceFactory *factory, NMConnection *connection); + + /** + * get_connection_parent: + * @factory: the #NMDeviceFactory + * @connection: the #NMConnection to return the parent name for, if supported + * + * Given a connection, returns the parent interface name, parent connection + * UUID, or parent device permanent hardware address for @connection. + * + * Returns: the parent interface name, parent connection UUID, parent + * device permanent hardware address, or %NULL + */ + const char *(*get_connection_parent)(NMDeviceFactory *factory, NMConnection *connection); + + /** + * get_connection_iface: + * @factory: the #NMDeviceFactory + * @connection: the #NMConnection to return the interface name for + * @parent_iface: optional parent interface name for virtual devices + * + * Given a connection, returns the interface name that a device activating + * that connection would have. + * + * Returns: the interface name, or %NULL + */ + char *(*get_connection_iface)(NMDeviceFactory *factory, + NMConnection * connection, + const char * parent_iface); + + /** + * create_device: + * @factory: the #NMDeviceFactory + * @iface: the interface name of the device + * @plink: the #NMPlatformLink if backed by a kernel device + * @connection: the #NMConnection if not backed by a kernel device + * @out_ignore: on return, %TRUE if the link should be ignored + * + * The plugin should create a new unrealized device using the details given + * by @iface and @plink or @connection. If both @iface and @plink are given, + * they are guaranteed to match. If both @iface and @connection are given, + * @iface is guaranteed to be the interface name that @connection specifies. + * + * If the plugin cannot create a #NMDevice for the link and wants the + * core to ignore it, set @out_ignore to %TRUE and return %NULL. + * + * Returns: the new unrealized #NMDevice, or %NULL + */ + NMDevice *(*create_device)(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore); + +} NMDeviceFactoryClass; + +GType nm_device_factory_get_type(void); + +/*****************************************************************************/ + +/** + * nm_device_factory_create: + * @error: an error if creation of the factory failed, or %NULL + * + * Creates a #GObject that implements the #NMDeviceFactory interface. This + * function must not emit any signals or perform any actions that would cause + * devices or components to be created immediately. Instead these should be + * deferred to the "start" interface method. + * + * Returns: the #GObject implementing #NMDeviceFactory or %NULL + */ +NMDeviceFactory *nm_device_factory_create(GError **error); + +/* Should match nm_device_factory_create() */ +typedef NMDeviceFactory *(*NMDeviceFactoryCreateFunc)(GError **error); + +/*****************************************************************************/ + +const char *nm_device_factory_get_connection_parent(NMDeviceFactory *factory, + NMConnection * connection); + +char *nm_device_factory_get_connection_iface(NMDeviceFactory *factory, + NMConnection * connection, + const char * parent_iface, + GError ** error); + +void nm_device_factory_start(NMDeviceFactory *factory); + +NMDevice *nm_device_factory_create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore, + GError ** error); + +#define NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(...) \ + { \ + static NMLinkType const _link_types_declared[] = {__VA_ARGS__, NM_LINK_TYPE_NONE}; \ + _link_types = _link_types_declared; \ + } +#define NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(...) \ + { \ + static const char *const _setting_types_declared[] = {__VA_ARGS__, NULL}; \ + _setting_types = _setting_types_declared; \ + } + +#define NM_DEVICE_FACTORY_DECLARE_TYPES(...) \ + static void get_supported_types(NMDeviceFactory * factory, \ + const NMLinkType ** out_link_types, \ + const char *const **out_setting_types) \ + { \ + static NMLinkType const _link_types_null[1] = {NM_LINK_TYPE_NONE}; \ + static const char *const _setting_types_null[1] = {NULL}; \ + \ + const NMLinkType * _link_types = _link_types_null; \ + const char *const *_setting_types = _setting_types_null; \ + \ + { \ + __VA_ARGS__; \ + } \ + \ + NM_SET_OUT(out_link_types, _link_types); \ + NM_SET_OUT(out_setting_types, _setting_types); \ + } + +/************************************************************************** + * INTERNAL DEVICE FACTORY FUNCTIONS - devices provided by plugins should + * not use these functions. + **************************************************************************/ + +#define NM_DEVICE_FACTORY_DEFINE_INTERNAL(upper, mixed, lower, st_code, dfi_code) \ + typedef struct { \ + NMDeviceFactory parent; \ + } NM##mixed##DeviceFactory; \ + typedef struct { \ + NMDeviceFactoryClass parent; \ + } NM##mixed##DeviceFactoryClass; \ + \ + GType nm_##lower##_device_factory_get_type(void); \ + \ + G_DEFINE_TYPE(NM##mixed##DeviceFactory, nm_##lower##_device_factory, NM_TYPE_DEVICE_FACTORY) \ + \ + NM_DEVICE_FACTORY_DECLARE_TYPES(st_code) \ + \ + static void nm_##lower##_device_factory_init(NM##mixed##DeviceFactory *self) {} \ + \ + static void nm_##lower##_device_factory_class_init(NM##mixed##DeviceFactoryClass *klass) \ + { \ + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); \ + \ + factory_class->get_supported_types = get_supported_types; \ + dfi_code \ + } + +/************************************************************************** + * PRIVATE FACTORY FUNCTIONS - for factory consumers (eg, NMManager). + **************************************************************************/ + +typedef void (*NMDeviceFactoryManagerFactoryFunc)(NMDeviceFactory *factory, gpointer user_data); + +void nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data); + +NMDeviceFactory *nm_device_factory_manager_find_factory_for_link_type(NMLinkType link_type); + +NMDeviceFactory *nm_device_factory_manager_find_factory_for_connection(NMConnection *connection); + +void nm_device_factory_manager_for_each_factory(NMDeviceFactoryManagerFactoryFunc callback, + gpointer user_data); + +#endif /* __NETWORKMANAGER_DEVICE_FACTORY_H__ */ diff --git a/src/core/devices/nm-device-generic.c b/src/core/devices/nm-device-generic.c new file mode 100644 index 00000000..a319666a --- /dev/null +++ b/src/core/devices/nm-device-generic.c @@ -0,0 +1,238 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-generic.h" + +#include "nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-core-internal.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_TYPE_DESCRIPTION, ); + +typedef struct { + char *type_description; +} NMDeviceGenericPrivate; + +struct _NMDeviceGeneric { + NMDevice parent; + NMDeviceGenericPrivate _priv; +}; + +struct _NMDeviceGenericClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceGeneric, nm_device_generic, NM_TYPE_DEVICE) + +#define NM_DEVICE_GENERIC_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceGeneric, NM_IS_DEVICE_GENERIC, NMDevice) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + int ifindex = nm_device_get_ifindex(device); + + if (ifindex > 0 + && nm_platform_link_supports_carrier_detect(nm_device_get_platform(device), ifindex)) + return NM_DEVICE_CAP_CARRIER_DETECT; + else + return NM_DEVICE_CAP_NONE; +} + +static const char * +get_type_description(NMDevice *device) +{ + if (NM_DEVICE_GENERIC_GET_PRIVATE(device)->type_description) + return NM_DEVICE_GENERIC_GET_PRIVATE(device)->type_description; + return NM_DEVICE_CLASS(nm_device_generic_parent_class)->get_type_description(device); +} + +static void +realize_start_notify(NMDevice *device, const NMPlatformLink *plink) +{ + NMDeviceGeneric * self = NM_DEVICE_GENERIC(device); + NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self); + int ifindex; + + NM_DEVICE_CLASS(nm_device_generic_parent_class)->realize_start_notify(device, plink); + + nm_clear_g_free(&priv->type_description); + ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self)); + if (ifindex > 0) + priv->type_description = + g_strdup(nm_platform_link_get_type_name(nm_device_get_platform(device), ifindex)); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingConnection *s_con; + + if (!NM_DEVICE_CLASS(nm_device_generic_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_con = nm_connection_get_setting_connection(connection); + if (!nm_setting_connection_get_interface_name(s_con)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "generic profiles need an interface name"); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMSettingConnection *s_con; + + if (!nm_connection_get_setting_generic(connection)) + nm_connection_add_setting(connection, nm_setting_generic_new()); + + s_con = nm_connection_get_setting_connection(connection); + g_assert(s_con); + g_object_set(G_OBJECT(s_con), + NM_SETTING_CONNECTION_INTERFACE_NAME, + nm_device_get_iface(device), + NULL); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceGeneric * self = NM_DEVICE_GENERIC(object); + NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_TYPE_DESCRIPTION: + g_value_set_string(value, priv->type_description); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceGeneric * self = NM_DEVICE_GENERIC(object); + NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_TYPE_DESCRIPTION: + priv->type_description = g_value_dup_string(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_generic_init(NMDeviceGeneric *self) +{} + +static GObject * +constructor(GType type, guint n_construct_params, GObjectConstructParam *construct_params) +{ + GObject *object; + + object = G_OBJECT_CLASS(nm_device_generic_parent_class) + ->constructor(type, n_construct_params, construct_params); + + nm_device_set_unmanaged_flags((NMDevice *) object, NM_UNMANAGED_BY_DEFAULT, TRUE); + + return object; +} + +NMDevice * +nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing) +{ + g_return_val_if_fail(plink != NULL, NULL); + + return g_object_new(NM_TYPE_DEVICE_GENERIC, + NM_DEVICE_IFACE, + plink->name, + NM_DEVICE_TYPE_DESC, + "Generic", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_GENERIC, + NM_DEVICE_NM_PLUGIN_MISSING, + nm_plugin_missing, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceGeneric * self = NM_DEVICE_GENERIC(object); + NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self); + + nm_clear_g_free(&priv->type_description); + + G_OBJECT_CLASS(nm_device_generic_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_generic = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_GENERIC, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L( + "TypeDescription", + "s", + NM_DEVICE_GENERIC_TYPE_DESCRIPTION), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_generic_class_init(NMDeviceGenericClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructor = constructor; + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_generic); + + device_class->connection_type_supported = NM_SETTING_GENERIC_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_GENERIC_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_ANY); + + device_class->realize_start_notify = realize_start_notify; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->get_type_description = get_type_description; + device_class->check_connection_compatible = check_connection_compatible; + device_class->update_connection = update_connection; + + obj_properties[PROP_TYPE_DESCRIPTION] = + g_param_spec_string(NM_DEVICE_GENERIC_TYPE_DESCRIPTION, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/nm-device-generic.h b/src/core/devices/nm-device-generic.h new file mode 100644 index 00000000..48c43523 --- /dev/null +++ b/src/core/devices/nm-device-generic.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_GENERIC_H__ +#define __NETWORKMANAGER_DEVICE_GENERIC_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_GENERIC (nm_device_generic_get_type()) +#define NM_DEVICE_GENERIC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGeneric)) +#define NM_DEVICE_GENERIC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENERIC, NMDeviceGenericClass)) +#define NM_IS_DEVICE_GENERIC(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENERIC)) +#define NM_IS_DEVICE_GENERIC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENERIC)) +#define NM_DEVICE_GENERIC_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGenericClass)) + +#define NM_DEVICE_GENERIC_TYPE_DESCRIPTION "type-description" + +typedef struct _NMDeviceGeneric NMDeviceGeneric; +typedef struct _NMDeviceGenericClass NMDeviceGenericClass; + +GType nm_device_generic_get_type(void); + +NMDevice *nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing); + +#endif /* __NETWORKMANAGER_DEVICE_GENERIC_H__ */ diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c new file mode 100644 index 00000000..f54ffcf0 --- /dev/null +++ b/src/core/devices/nm-device-infiniband.c @@ -0,0 +1,504 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-infiniband.h" + +#include <linux/if.h> +#include <linux/if_infiniband.h> + +#include "NetworkManagerUtils.h" +#include "nm-device-private.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-core-internal.h" + +#define NM_DEVICE_INFINIBAND_IS_PARTITION "is-partition" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_IS_PARTITION, ); + +typedef struct { + gboolean is_partition; + int parent_ifindex; + int p_key; +} NMDeviceInfinibandPrivate; + +struct _NMDeviceInfiniband { + NMDevice parent; + NMDeviceInfinibandPrivate _priv; +}; + +struct _NMDeviceInfinibandClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceInfiniband, nm_device_infiniband, NM_TYPE_DEVICE) + +#define NM_DEVICE_INFINIBAND_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceInfiniband, NM_IS_DEVICE_INFINIBAND, NMDevice) + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + guint32 caps = NM_DEVICE_CAP_CARRIER_DETECT; + + if (NM_DEVICE_INFINIBAND_GET_PRIVATE(device)->is_partition) + caps |= NM_DEVICE_CAP_IS_SOFTWARE; + + return caps; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + nm_auto_close int dirfd = -1; + NMSettingInfiniband *s_infiniband; + char ifname_verified[IFNAMSIZ]; + const char * transport_mode; + gboolean ok; + + s_infiniband = nm_device_get_applied_setting(device, NM_TYPE_SETTING_INFINIBAND); + + g_return_val_if_fail(s_infiniband, NM_ACT_STAGE_RETURN_FAILURE); + + transport_mode = nm_setting_infiniband_get_transport_mode(s_infiniband); + + dirfd = nm_platform_sysctl_open_netdir(nm_device_get_platform(device), + nm_device_get_ifindex(device), + ifname_verified); + if (dirfd < 0) { + if (nm_streq(transport_mode, "datagram")) + return NM_ACT_STAGE_RETURN_SUCCESS; + else { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_INFINIBAND_MODE); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + + /* With some drivers the interface must be down to set transport mode */ + nm_device_take_down(device, TRUE); + ok = nm_platform_sysctl_set(nm_device_get_platform(device), + NMP_SYSCTL_PATHID_NETDIR(dirfd, ifname_verified, "mode"), + transport_mode); + nm_device_bring_up(device, TRUE, NULL); + + if (!ok) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + return nm_device_get_configured_mtu_from_connection(device, + NM_TYPE_SETTING_INFINIBAND, + out_source); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingInfiniband *s_infiniband; + + if (!NM_DEVICE_CLASS(nm_device_infiniband_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_device_is_real(device)) { + const char *mac; + const char *hw_addr; + + s_infiniband = nm_connection_get_setting_infiniband(connection); + + mac = nm_setting_infiniband_get_mac_address(s_infiniband); + if (mac) { + hw_addr = nm_device_get_permanent_hw_address(device); + if (!hw_addr || !nm_utils_hwaddr_matches(mac, -1, hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "MAC address mismatches"); + return FALSE; + } + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingInfiniband *s_infiniband; + + s_infiniband = nm_connection_get_setting_infiniband(connection); + if (!s_infiniband) { + s_infiniband = (NMSettingInfiniband *) nm_setting_infiniband_new(); + nm_connection_add_setting(connection, NM_SETTING(s_infiniband)); + } + + nm_utils_complete_generic( + nm_device_get_platform(device), + connection, + NM_SETTING_INFINIBAND_SETTING_NAME, + existing_connections, + NULL, + _("InfiniBand connection"), + NULL, + nm_setting_infiniband_get_mac_address(s_infiniband) ? NULL : nm_device_get_iface(device), + TRUE); + + if (!nm_setting_infiniband_get_transport_mode(s_infiniband)) + g_object_set(G_OBJECT(s_infiniband), + NM_SETTING_INFINIBAND_TRANSPORT_MODE, + "datagram", + NULL); + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMSettingInfiniband *s_infiniband = nm_connection_get_setting_infiniband(connection); + const char * mac = nm_device_get_permanent_hw_address(device); + const char * transport_mode = "datagram"; + int ifindex; + + if (!s_infiniband) { + s_infiniband = (NMSettingInfiniband *) nm_setting_infiniband_new(); + nm_connection_add_setting(connection, (NMSetting *) s_infiniband); + } + + if (mac && !nm_utils_hwaddr_matches(mac, -1, NULL, INFINIBAND_ALEN)) + g_object_set(s_infiniband, NM_SETTING_INFINIBAND_MAC_ADDRESS, mac, NULL); + + ifindex = nm_device_get_ifindex(device); + if (ifindex > 0) { + if (!nm_platform_link_infiniband_get_properties(nm_device_get_platform(device), + ifindex, + NULL, + NULL, + &transport_mode)) + transport_mode = "datagram"; + } + g_object_set(G_OBJECT(s_infiniband), + NM_SETTING_INFINIBAND_TRANSPORT_MODE, + transport_mode, + NULL); +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + if (nm_streq(setting_name, NM_SETTING_INFINIBAND_SETTING_NAME)) { + return nm_device_hash_check_invalid_keys( + diffs, + NM_SETTING_INFINIBAND_SETTING_NAME, + error, + NM_SETTING_INFINIBAND_MTU); /* reapplied with IP config */ + } + + device_class = NM_DEVICE_CLASS(nm_device_infiniband_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + NMDeviceInfinibandPrivate *priv = NM_DEVICE_INFINIBAND_GET_PRIVATE(device); + NMSettingInfiniband * s_infiniband; + int r; + + s_infiniband = nm_connection_get_setting_infiniband(connection); + g_assert(s_infiniband); + + /* Can only create partitions at this time */ + priv->p_key = nm_setting_infiniband_get_p_key(s_infiniband); + if (priv->p_key < 0) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "only InfiniBand partitions can be created"); + return FALSE; + } + + if (!parent) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "InfiniBand partitions can not be created without a parent interface"); + return FALSE; + } + + if (!NM_IS_DEVICE_INFINIBAND(parent)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "Parent interface %s must be an InfiniBand interface", + nm_device_get_iface(parent)); + return FALSE; + } + + priv->parent_ifindex = nm_device_get_ifindex(parent); + if (priv->parent_ifindex <= 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "failed to get InfiniBand parent %s ifindex", + nm_device_get_iface(parent)); + return FALSE; + } + + r = nm_platform_link_infiniband_add(nm_device_get_platform(device), + priv->parent_ifindex, + priv->p_key, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create InfiniBand P_Key interface '%s' for '%s': %s", + nm_device_get_iface(device), + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + priv->is_partition = TRUE; + return TRUE; +} + +static gboolean +unrealize(NMDevice *device, GError **error) +{ + NMDeviceInfinibandPrivate *priv; + int r; + + g_return_val_if_fail(NM_IS_DEVICE_INFINIBAND(device), FALSE); + + priv = NM_DEVICE_INFINIBAND_GET_PRIVATE(device); + + if (priv->p_key < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Only InfiniBand partitions can be removed"); + return FALSE; + } + + r = nm_platform_link_infiniband_delete(nm_device_get_platform(device), + priv->parent_ifindex, + priv->p_key); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to remove InfiniBand P_Key interface '%s': %s", + nm_device_get_iface(device), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + case PROP_IS_PARTITION: + g_value_set_boolean(value, NM_DEVICE_INFINIBAND_GET_PRIVATE(object)->is_partition); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + switch (prop_id) { + case PROP_IS_PARTITION: + NM_DEVICE_INFINIBAND_GET_PRIVATE(object)->is_partition = g_value_get_boolean(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_infiniband_init(NMDeviceInfiniband *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_infiniband = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_INFINIBAND, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", + "b", + NM_DEVICE_CARRIER), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_infiniband_class_init(NMDeviceInfinibandClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_infiniband); + + device_class->connection_type_supported = NM_SETTING_INFINIBAND_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_INFINIBAND_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_INFINIBAND); + + device_class->can_reapply_change = can_reapply_change; + device_class->create_and_realize = create_and_realize; + device_class->unrealize = unrealize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->update_connection = update_connection; + + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->get_configured_mtu = get_configured_mtu; + + obj_properties[PROP_IS_PARTITION] = + g_param_spec_boolean(NM_DEVICE_INFINIBAND_IS_PARTITION, + "", + "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_INFINIBAND_DEVICE_FACTORY (nm_infiniband_device_factory_get_type()) +#define NM_INFINIBAND_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), \ + NM_TYPE_INFINIBAND_DEVICE_FACTORY, \ + NMInfinibandDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + gboolean is_partition = FALSE; + + if (plink) + is_partition = (plink->parent > 0 || plink->parent == NM_PLATFORM_LINK_OTHER_NETNS); + else if (connection) { + NMSettingInfiniband *s_infiniband; + + s_infiniband = nm_connection_get_setting_infiniband(connection); + g_return_val_if_fail(s_infiniband, NULL); + is_partition = !!nm_setting_infiniband_get_parent(s_infiniband) + || (nm_setting_infiniband_get_p_key(s_infiniband) >= 0 + && nm_setting_infiniband_get_mac_address(s_infiniband)); + } + + return g_object_new(NM_TYPE_DEVICE_INFINIBAND, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "InfiniBand", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_INFINIBAND, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_INFINIBAND, + /* NOTE: Partition should probably be a different link type! */ + NM_DEVICE_INFINIBAND_IS_PARTITION, + is_partition, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingInfiniband *s_infiniband; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_INFINIBAND_SETTING_NAME), + NULL); + + s_infiniband = nm_connection_get_setting_infiniband(connection); + g_assert(s_infiniband); + + return nm_setting_infiniband_get_parent(s_infiniband); +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + NMSettingInfiniband *s_infiniband; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_INFINIBAND_SETTING_NAME), + NULL); + + s_infiniband = nm_connection_get_setting_infiniband(connection); + g_assert(s_infiniband); + + if (!parent_iface) + return NULL; + + g_return_val_if_fail(g_strcmp0(parent_iface, nm_setting_infiniband_get_parent(s_infiniband)) + == 0, + NULL); + + return g_strdup(nm_setting_infiniband_get_virtual_interface_name(s_infiniband)); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + INFINIBAND, + Infiniband, + infiniband, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_INFINIBAND) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_INFINIBAND_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-infiniband.h b/src/core/devices/nm-device-infiniband.h new file mode 100644 index 00000000..69994a2d --- /dev/null +++ b/src/core/devices/nm-device-infiniband.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_INFINIBAND_H__ +#define __NETWORKMANAGER_DEVICE_INFINIBAND_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_INFINIBAND (nm_device_infiniband_get_type()) +#define NM_DEVICE_INFINIBAND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfiniband)) +#define NM_DEVICE_INFINIBAND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfinibandClass)) +#define NM_IS_DEVICE_INFINIBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_INFINIBAND)) +#define NM_IS_DEVICE_INFINIBAND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_INFINIBAND)) +#define NM_DEVICE_INFINIBAND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfinibandClass)) + +typedef struct _NMDeviceInfiniband NMDeviceInfiniband; +typedef struct _NMDeviceInfinibandClass NMDeviceInfinibandClass; + +GType nm_device_infiniband_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_INFINIBAND_H__ */ diff --git a/src/core/devices/nm-device-ip-tunnel.c b/src/core/devices/nm-device-ip-tunnel.c new file mode 100644 index 00000000..da6afb3b --- /dev/null +++ b/src/core/devices/nm-device-ip-tunnel.c @@ -0,0 +1,1299 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ip-tunnel.h" + +#include <netinet/in.h> +#include <linux/if.h> +#include <linux/ip.h> +#include <linux/if_tunnel.h> +#include <linux/ip6_tunnel.h> +#include <linux/if_ether.h> + +#include "nm-device-private.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-core-internal.h" +#include "settings/nm-settings.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceIPTunnel +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIPTunnel, + PROP_MODE, + PROP_LOCAL, + PROP_REMOTE, + PROP_TTL, + PROP_TOS, + PROP_PATH_MTU_DISCOVERY, + PROP_INPUT_KEY, + PROP_OUTPUT_KEY, + PROP_ENCAPSULATION_LIMIT, + PROP_FLOW_LABEL, + PROP_FLAGS, ); + +typedef struct { + NMIPTunnelMode mode; + char * local; + char * remote; + guint8 ttl; + guint8 tos; + gboolean path_mtu_discovery; + int addr_family; + char * input_key; + char * output_key; + guint8 encap_limit; + guint32 flow_label; + NMIPTunnelFlags flags; +} NMDeviceIPTunnelPrivate; + +struct _NMDeviceIPTunnel { + NMDevice parent; + NMDeviceIPTunnelPrivate _priv; +}; + +struct _NMDeviceIPTunnelClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceIPTunnel, nm_device_ip_tunnel, NM_TYPE_DEVICE) + +#define NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceIPTunnel, NM_IS_DEVICE_IP_TUNNEL, NMDevice) + +/*****************************************************************************/ + +static guint32 +ip6tnl_flags_setting_to_plat(NMIPTunnelFlags flags) +{ + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_IGN_ENCAP_LIMIT == IP6_TNL_F_IGN_ENCAP_LIMIT); + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_TCLASS == IP6_TNL_F_USE_ORIG_TCLASS); + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_FLOWLABEL == IP6_TNL_F_USE_ORIG_FLOWLABEL); + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_MIP6_DEV == IP6_TNL_F_MIP6_DEV); + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_RCV_DSCP_COPY == IP6_TNL_F_RCV_DSCP_COPY); + G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_FWMARK == IP6_TNL_F_USE_ORIG_FWMARK); + + /* NOTE: "accidentally", the numeric values correspond. + * For flags added in the future, that might no longer + * be the case. */ + return flags & _NM_IP_TUNNEL_FLAG_ALL_IP6TNL; +} + +static NMIPTunnelFlags +ip6tnl_flags_plat_to_setting(guint32 flags) +{ + return flags & ((guint32) _NM_IP_TUNNEL_FLAG_ALL_IP6TNL); +} + +/*****************************************************************************/ + +static gboolean +address_equal_pp(int addr_family, const char *a, const char *b) +{ + const NMIPAddr *addr_a = &nm_ip_addr_zero; + const NMIPAddr *addr_b = &nm_ip_addr_zero; + NMIPAddr addr_a_val; + NMIPAddr addr_b_val; + + nm_assert_addr_family(addr_family); + + if (a) { + if (!nm_utils_parse_inaddr_bin(addr_family, a, NULL, &addr_a_val)) + nm_assert_not_reached(); + addr_a = &addr_a_val; + } + if (b) { + if (!nm_utils_parse_inaddr_bin(addr_family, b, NULL, &addr_b_val)) + nm_assert_not_reached(); + addr_b = &addr_b_val; + } + + return nm_ip_addr_equal(addr_family, addr_a, addr_b); +} + +static gboolean +address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new) +{ + nm_assert_addr_family(addr_family); + nm_assert(p_addr); + nm_assert(!*p_addr || nm_utils_ipaddr_is_normalized(addr_family, *p_addr)); + + if (!addr_new || nm_ip_addr_is_null(addr_family, addr_new)) { + if (nm_clear_g_free(p_addr)) + return TRUE; + return FALSE; + } + + if (*p_addr) { + NMIPAddr addr_val; + + if (!nm_utils_parse_inaddr_bin(addr_family, *p_addr, NULL, &addr_val)) + nm_assert_not_reached(); + + if (nm_ip_addr_equal(addr_family, &addr_val, addr_new)) + return FALSE; + + g_free(*p_addr); + } + + *p_addr = nm_utils_inet_ntop_dup(addr_family, addr_new); + return TRUE; +} + +static void +update_properties_from_ifindex(NMDevice *device, int ifindex) +{ + NMDeviceIPTunnel * self = NM_DEVICE_IP_TUNNEL(device); + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self); + int parent_ifindex = 0; + NMIPAddr local = NM_IP_ADDR_INIT; + NMIPAddr remote = NM_IP_ADDR_INIT; + guint8 ttl = 0; + guint8 tos = 0; + guint8 encap_limit = 0; + gboolean pmtud = FALSE; + guint32 flow_label = 0; + NMIPTunnelFlags flags = NM_IP_TUNNEL_FLAG_NONE; + char * key; + + if (ifindex <= 0) { +clear: + nm_device_parent_set_ifindex(device, 0); + if (priv->local) { + nm_clear_g_free(&priv->local); + _notify(self, PROP_LOCAL); + } + if (priv->remote) { + nm_clear_g_free(&priv->remote); + _notify(self, PROP_REMOTE); + } + if (priv->input_key) { + nm_clear_g_free(&priv->input_key); + _notify(self, PROP_INPUT_KEY); + } + if (priv->output_key) { + nm_clear_g_free(&priv->output_key); + _notify(self, PROP_OUTPUT_KEY); + } + + goto out; + } + + if (NM_IN_SET(priv->mode, NM_IP_TUNNEL_MODE_GRE, NM_IP_TUNNEL_MODE_GRETAP)) { + const NMPlatformLnkGre *lnk; + + if (priv->mode == NM_IP_TUNNEL_MODE_GRE) + lnk = nm_platform_link_get_lnk_gre(nm_device_get_platform(device), ifindex, NULL); + else + lnk = nm_platform_link_get_lnk_gretap(nm_device_get_platform(device), ifindex, NULL); + if (!lnk) { + _LOGW(LOGD_PLATFORM, "could not read %s properties", "gre"); + goto clear; + } + + parent_ifindex = lnk->parent_ifindex; + local.addr4 = lnk->local; + remote.addr4 = lnk->remote; + ttl = lnk->ttl; + tos = lnk->tos; + pmtud = lnk->path_mtu_discovery; + + if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) { + key = g_strdup_printf("%u", lnk->input_key); + if (g_strcmp0(priv->input_key, key)) { + g_free(priv->input_key); + priv->input_key = key; + _notify(self, PROP_INPUT_KEY); + } else + g_free(key); + } else { + if (priv->input_key) { + nm_clear_g_free(&priv->input_key); + _notify(self, PROP_INPUT_KEY); + } + } + + if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) { + key = g_strdup_printf("%u", lnk->output_key); + if (g_strcmp0(priv->output_key, key)) { + g_free(priv->output_key); + priv->output_key = key; + _notify(self, PROP_OUTPUT_KEY); + } else + g_free(key); + } else { + if (priv->output_key) { + nm_clear_g_free(&priv->output_key); + _notify(self, PROP_OUTPUT_KEY); + } + } + } else if (priv->mode == NM_IP_TUNNEL_MODE_SIT) { + const NMPlatformLnkSit *lnk; + + lnk = nm_platform_link_get_lnk_sit(nm_device_get_platform(device), ifindex, NULL); + if (!lnk) { + _LOGW(LOGD_PLATFORM, "could not read %s properties", "sit"); + goto clear; + } + + parent_ifindex = lnk->parent_ifindex; + local.addr4 = lnk->local; + remote.addr4 = lnk->remote; + ttl = lnk->ttl; + tos = lnk->tos; + pmtud = lnk->path_mtu_discovery; + } else if (priv->mode == NM_IP_TUNNEL_MODE_IPIP) { + const NMPlatformLnkIpIp *lnk; + + lnk = nm_platform_link_get_lnk_ipip(nm_device_get_platform(device), ifindex, NULL); + if (!lnk) { + _LOGW(LOGD_PLATFORM, "could not read %s properties", "ipip"); + goto clear; + } + + parent_ifindex = lnk->parent_ifindex; + local.addr4 = lnk->local; + remote.addr4 = lnk->remote; + ttl = lnk->ttl; + tos = lnk->tos; + pmtud = lnk->path_mtu_discovery; + } else if (NM_IN_SET(priv->mode, + NM_IP_TUNNEL_MODE_IPIP6, + NM_IP_TUNNEL_MODE_IP6IP6, + NM_IP_TUNNEL_MODE_IP6GRE, + NM_IP_TUNNEL_MODE_IP6GRETAP)) { + const NMPlatformLnkIp6Tnl *lnk; + NMPlatform * plat = nm_device_get_platform(device); + + if (priv->mode == NM_IP_TUNNEL_MODE_IP6GRE) + lnk = nm_platform_link_get_lnk_ip6gre(plat, ifindex, NULL); + else if (priv->mode == NM_IP_TUNNEL_MODE_IP6GRETAP) + lnk = nm_platform_link_get_lnk_ip6gretap(plat, ifindex, NULL); + else + lnk = nm_platform_link_get_lnk_ip6tnl(plat, ifindex, NULL); + + if (!lnk) { + _LOGW(LOGD_PLATFORM, "could not read %s properties", "ip6tnl"); + goto clear; + } + + parent_ifindex = lnk->parent_ifindex; + local.addr6 = lnk->local; + remote.addr6 = lnk->remote; + ttl = lnk->ttl; + tos = lnk->tclass; + encap_limit = lnk->encap_limit; + flow_label = lnk->flow_label; + flags = ip6tnl_flags_plat_to_setting(lnk->flags); + + if (NM_IN_SET(priv->mode, NM_IP_TUNNEL_MODE_IP6GRE, NM_IP_TUNNEL_MODE_IP6GRETAP)) { + if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) { + key = g_strdup_printf("%u", lnk->input_key); + if (g_strcmp0(priv->input_key, key)) { + g_free(priv->input_key); + priv->input_key = key; + _notify(self, PROP_INPUT_KEY); + } else + g_free(key); + } else { + if (priv->input_key) { + nm_clear_g_free(&priv->input_key); + _notify(self, PROP_INPUT_KEY); + } + } + + if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) { + key = g_strdup_printf("%u", lnk->output_key); + if (g_strcmp0(priv->output_key, key)) { + g_free(priv->output_key); + priv->output_key = key; + _notify(self, PROP_OUTPUT_KEY); + } else + g_free(key); + } else { + if (priv->output_key) { + nm_clear_g_free(&priv->output_key); + _notify(self, PROP_OUTPUT_KEY); + } + } + } + } else + g_return_if_reached(); + + nm_device_parent_set_ifindex(device, parent_ifindex); + + if (address_set(priv->addr_family, &priv->local, &local)) + _notify(self, PROP_LOCAL); + if (address_set(priv->addr_family, &priv->remote, &remote)) + _notify(self, PROP_REMOTE); + +out: + + if (priv->ttl != ttl) { + priv->ttl = ttl; + _notify(self, PROP_TTL); + } + + if (priv->tos != tos) { + priv->tos = tos; + _notify(self, PROP_TOS); + } + + if (priv->path_mtu_discovery != pmtud) { + priv->path_mtu_discovery = pmtud; + _notify(self, PROP_PATH_MTU_DISCOVERY); + } + + if (priv->encap_limit != encap_limit) { + priv->encap_limit = encap_limit; + _notify(self, PROP_ENCAPSULATION_LIMIT); + } + + if (priv->flow_label != flow_label) { + priv->flow_label = flow_label; + _notify(self, PROP_FLOW_LABEL); + } + + if (priv->flags != flags) { + priv->flags = flags; + _notify(self, PROP_FLAGS); + } +} + +static void +update_properties(NMDevice *device) +{ + update_properties_from_ifindex(device, nm_device_get_ifindex(device)); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingIPTunnel *s_ip_tunnel; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_IP_TUNNEL_SETTING_NAME, + existing_connections, + NULL, + _("IP tunnel connection"), + NULL, + NULL, + TRUE); + + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + if (!s_ip_tunnel) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'tunnel' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceIPTunnel * self = NM_DEVICE_IP_TUNNEL(device); + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self); + NMSettingIPTunnel * s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + + if (!s_ip_tunnel) { + s_ip_tunnel = (NMSettingIPTunnel *) nm_setting_ip_tunnel_new(); + nm_connection_add_setting(connection, (NMSetting *) s_ip_tunnel); + } + + if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode) + g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_MODE, priv->mode, NULL); + + g_object_set( + s_ip_tunnel, + NM_SETTING_IP_TUNNEL_PARENT, + nm_device_parent_find_for_connection(device, nm_setting_ip_tunnel_get_parent(s_ip_tunnel)), + NULL); + + if (!address_equal_pp(priv->addr_family, + nm_setting_ip_tunnel_get_local(s_ip_tunnel), + priv->local)) + g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_LOCAL, priv->local, NULL); + + if (!address_equal_pp(priv->addr_family, + nm_setting_ip_tunnel_get_remote(s_ip_tunnel), + priv->remote)) + g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_REMOTE, priv->remote, NULL); + + if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) + g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_TTL, priv->ttl, NULL); + + if (nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos) + g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_TOS, priv->tos, NULL); + + if (nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel) != priv->path_mtu_discovery) { + g_object_set(G_OBJECT(s_ip_tunnel), + NM_SETTING_IP_TUNNEL_PATH_MTU_DISCOVERY, + priv->path_mtu_discovery, + NULL); + } + + if (nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel) != priv->encap_limit) { + g_object_set(G_OBJECT(s_ip_tunnel), + NM_SETTING_IP_TUNNEL_ENCAPSULATION_LIMIT, + priv->encap_limit, + NULL); + } + + if (nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel) != priv->flow_label) { + g_object_set(G_OBJECT(s_ip_tunnel), + NM_SETTING_IP_TUNNEL_FLOW_LABEL, + priv->flow_label, + NULL); + } + + if (NM_IN_SET(priv->mode, + NM_IP_TUNNEL_MODE_GRE, + NM_IP_TUNNEL_MODE_GRETAP, + NM_IP_TUNNEL_MODE_IP6GRE, + NM_IP_TUNNEL_MODE_IP6GRETAP)) { + if (g_strcmp0(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel), priv->input_key)) { + g_object_set(G_OBJECT(s_ip_tunnel), + NM_SETTING_IP_TUNNEL_INPUT_KEY, + priv->input_key, + NULL); + } + if (g_strcmp0(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel), priv->output_key)) { + g_object_set(G_OBJECT(s_ip_tunnel), + NM_SETTING_IP_TUNNEL_OUTPUT_KEY, + priv->output_key, + NULL); + } + } +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceIPTunnel * self = NM_DEVICE_IP_TUNNEL(device); + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self); + NMSettingIPTunnel * s_ip_tunnel; + const char * parent; + + if (!NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + + if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "incompatible IP tunnel mode"); + return FALSE; + } + + if (nm_device_is_real(device)) { + /* Check parent interface; could be an interface name or a UUID */ + parent = nm_setting_ip_tunnel_get_parent(s_ip_tunnel); + if (parent && !nm_device_match_parent(device, parent)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "IP tunnel parent mismatches"); + return FALSE; + } + + if (!address_equal_pp(priv->addr_family, + nm_setting_ip_tunnel_get_local(s_ip_tunnel), + priv->local)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "local IP tunnel address mismatches"); + return FALSE; + } + + if (!address_equal_pp(priv->addr_family, + nm_setting_ip_tunnel_get_remote(s_ip_tunnel), + priv->remote)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "remote IP tunnel address mismatches"); + return FALSE; + } + + if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "TTL of IP tunnel mismatches"); + return FALSE; + } + + if (nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "TOS of IP tunnel mismatches"); + return FALSE; + } + + if (priv->addr_family == AF_INET) { + if (nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel) + != priv->path_mtu_discovery) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "MTU discovery setting of IP tunnel mismatches"); + return FALSE; + } + } else { + if (nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel) != priv->encap_limit) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "encapsulation limit of IP tunnel mismatches"); + return FALSE; + } + + if (nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel) != priv->flow_label) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "flow-label of IP tunnel mismatches"); + return FALSE; + } + } + } + + return TRUE; +} + +static NMIPTunnelMode +platform_link_to_tunnel_mode(const NMPlatformLink *link) +{ + const NMPlatformLnkIp6Tnl *lnk; + + switch (link->type) { + case NM_LINK_TYPE_GRE: + return NM_IP_TUNNEL_MODE_GRE; + case NM_LINK_TYPE_GRETAP: + return NM_IP_TUNNEL_MODE_GRETAP; + case NM_LINK_TYPE_IP6TNL: + lnk = nm_platform_link_get_lnk_ip6tnl(NM_PLATFORM_GET, link->ifindex, NULL); + if (lnk) { + if (lnk->proto == IPPROTO_IPIP) + return NM_IP_TUNNEL_MODE_IPIP6; + if (lnk->proto == IPPROTO_IPV6) + return NM_IP_TUNNEL_MODE_IP6IP6; + } + return NM_IP_TUNNEL_MODE_UNKNOWN; + case NM_LINK_TYPE_IP6GRE: + return NM_IP_TUNNEL_MODE_IP6GRE; + case NM_LINK_TYPE_IP6GRETAP: + return NM_IP_TUNNEL_MODE_IP6GRETAP; + case NM_LINK_TYPE_IPIP: + return NM_IP_TUNNEL_MODE_IPIP; + case NM_LINK_TYPE_SIT: + return NM_IP_TUNNEL_MODE_SIT; + default: + g_return_val_if_reached(NM_IP_TUNNEL_MODE_UNKNOWN); + } +} + +static NMLinkType +tunnel_mode_to_link_type(NMIPTunnelMode tunnel_mode) +{ + switch (tunnel_mode) { + case NM_IP_TUNNEL_MODE_GRE: + return NM_LINK_TYPE_GRE; + case NM_IP_TUNNEL_MODE_GRETAP: + return NM_LINK_TYPE_GRETAP; + case NM_IP_TUNNEL_MODE_IPIP6: + case NM_IP_TUNNEL_MODE_IP6IP6: + return NM_LINK_TYPE_IP6TNL; + case NM_IP_TUNNEL_MODE_IP6GRE: + return NM_LINK_TYPE_IP6GRE; + case NM_IP_TUNNEL_MODE_IP6GRETAP: + return NM_LINK_TYPE_IP6GRETAP; + case NM_IP_TUNNEL_MODE_IPIP: + return NM_LINK_TYPE_IPIP; + case NM_IP_TUNNEL_MODE_SIT: + return NM_LINK_TYPE_SIT; + case NM_IP_TUNNEL_MODE_VTI: + case NM_IP_TUNNEL_MODE_VTI6: + case NM_IP_TUNNEL_MODE_ISATAP: + return NM_LINK_TYPE_UNKNOWN; + case NM_IP_TUNNEL_MODE_UNKNOWN: + break; + } + g_return_val_if_reached(NM_LINK_TYPE_UNKNOWN); +} + +/*****************************************************************************/ + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSettingIPTunnel * s_ip_tunnel; + NMPlatformLnkGre lnk_gre = {}; + NMPlatformLnkSit lnk_sit = {}; + NMPlatformLnkIpIp lnk_ipip = {}; + NMPlatformLnkIp6Tnl lnk_ip6tnl = {}; + const char * str; + gint64 val; + NMIPTunnelMode mode; + int r; + gs_free char * hwaddr = NULL; + guint8 mac_address[ETH_ALEN]; + gboolean mac_address_valid = FALSE; + + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + nm_assert(NM_IS_SETTING_IP_TUNNEL(s_ip_tunnel)); + + mode = nm_setting_ip_tunnel_get_mode(s_ip_tunnel); + + if (_nm_ip_tunnel_mode_is_layer2(mode) + && nm_device_hw_addr_get_cloned(device, connection, FALSE, &hwaddr, NULL, NULL) && hwaddr) { + /* FIXME: we set the MAC address when creating the interface, while the + * NMDevice is still unrealized. As we afterwards realize the device, it + * forgets the parameters for the cloned MAC address, and in stage 1 + * it might create a different MAC address. That should be fixed by + * better handling device realization. */ + if (!nm_utils_hwaddr_aton(hwaddr, mac_address, ETH_ALEN)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Invalid hardware address '%s'", + hwaddr); + g_return_val_if_reached(FALSE); + } + + mac_address_valid = TRUE; + } + + switch (mode) { + case NM_IP_TUNNEL_MODE_GRETAP: + lnk_gre.is_tap = TRUE; + /* fall-through */ + case NM_IP_TUNNEL_MODE_GRE: + if (parent) + lnk_gre.parent_ifindex = nm_device_get_ifindex(parent); + + str = nm_setting_ip_tunnel_get_local(s_ip_tunnel); + if (str) + inet_pton(AF_INET, str, &lnk_gre.local); + + str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel); + g_assert(str); + inet_pton(AF_INET, str, &lnk_gre.remote); + + lnk_gre.ttl = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel); + lnk_gre.tos = nm_setting_ip_tunnel_get_tos(s_ip_tunnel); + lnk_gre.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel); + + val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel), + 10, + 0, + G_MAXUINT32, + -1); + if (val != -1) { + lnk_gre.input_key = val; + lnk_gre.input_flags = NM_GRE_KEY; + } + + val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel), + 10, + 0, + G_MAXUINT32, + -1); + if (val != -1) { + lnk_gre.output_key = val; + lnk_gre.output_flags = NM_GRE_KEY; + } + + r = nm_platform_link_gre_add(nm_device_get_platform(device), + iface, + mac_address_valid ? mac_address : NULL, + mac_address_valid ? ETH_ALEN : 0, + &lnk_gre, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create GRE interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + break; + case NM_IP_TUNNEL_MODE_SIT: + if (parent) + lnk_sit.parent_ifindex = nm_device_get_ifindex(parent); + + str = nm_setting_ip_tunnel_get_local(s_ip_tunnel); + if (str) + inet_pton(AF_INET, str, &lnk_sit.local); + + str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel); + g_assert(str); + inet_pton(AF_INET, str, &lnk_sit.remote); + + lnk_sit.ttl = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel); + lnk_sit.tos = nm_setting_ip_tunnel_get_tos(s_ip_tunnel); + lnk_sit.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel); + + r = nm_platform_link_sit_add(nm_device_get_platform(device), iface, &lnk_sit, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create SIT interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + break; + case NM_IP_TUNNEL_MODE_IPIP: + if (parent) + lnk_ipip.parent_ifindex = nm_device_get_ifindex(parent); + + str = nm_setting_ip_tunnel_get_local(s_ip_tunnel); + if (str) + inet_pton(AF_INET, str, &lnk_ipip.local); + + str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel); + g_assert(str); + inet_pton(AF_INET, str, &lnk_ipip.remote); + + lnk_ipip.ttl = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel); + lnk_ipip.tos = nm_setting_ip_tunnel_get_tos(s_ip_tunnel); + lnk_ipip.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel); + + r = nm_platform_link_ipip_add(nm_device_get_platform(device), iface, &lnk_ipip, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create IPIP interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + break; + case NM_IP_TUNNEL_MODE_IPIP6: + case NM_IP_TUNNEL_MODE_IP6IP6: + case NM_IP_TUNNEL_MODE_IP6GRE: + case NM_IP_TUNNEL_MODE_IP6GRETAP: + if (parent) + lnk_ip6tnl.parent_ifindex = nm_device_get_ifindex(parent); + + str = nm_setting_ip_tunnel_get_local(s_ip_tunnel); + if (str) + inet_pton(AF_INET6, str, &lnk_ip6tnl.local); + + str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel); + g_assert(str); + inet_pton(AF_INET6, str, &lnk_ip6tnl.remote); + + lnk_ip6tnl.ttl = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel); + lnk_ip6tnl.tclass = nm_setting_ip_tunnel_get_tos(s_ip_tunnel); + lnk_ip6tnl.encap_limit = nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel); + lnk_ip6tnl.flow_label = nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel); + lnk_ip6tnl.flags = + ip6tnl_flags_setting_to_plat(nm_setting_ip_tunnel_get_flags(s_ip_tunnel)); + + if (NM_IN_SET(mode, NM_IP_TUNNEL_MODE_IP6GRE, NM_IP_TUNNEL_MODE_IP6GRETAP)) { + val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel), + 10, + 0, + G_MAXUINT32, + -1); + if (val != -1) { + lnk_ip6tnl.input_key = val; + lnk_ip6tnl.input_flags = NM_GRE_KEY; + } + + val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel), + 10, + 0, + G_MAXUINT32, + -1); + if (val != -1) { + lnk_ip6tnl.output_key = val; + lnk_ip6tnl.output_flags = NM_GRE_KEY; + } + + lnk_ip6tnl.is_gre = TRUE; + lnk_ip6tnl.is_tap = (mode == NM_IP_TUNNEL_MODE_IP6GRETAP); + + r = nm_platform_link_ip6gre_add(nm_device_get_platform(device), + iface, + mac_address_valid ? mac_address : NULL, + mac_address_valid ? ETH_ALEN : 0, + &lnk_ip6tnl, + out_plink); + } else { + lnk_ip6tnl.proto = nm_setting_ip_tunnel_get_mode(s_ip_tunnel) == NM_IP_TUNNEL_MODE_IPIP6 + ? IPPROTO_IPIP + : IPPROTO_IPV6; + r = nm_platform_link_ip6tnl_add(nm_device_get_platform(device), + iface, + &lnk_ip6tnl, + out_plink); + } + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create IPv6 tunnel interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + break; + default: + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create IP tunnel interface '%s' for '%s': mode %d not supported", + iface, + nm_connection_get_id(connection), + (int) nm_setting_ip_tunnel_get_mode(s_ip_tunnel)); + return FALSE; + } + + return TRUE; +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + return nm_device_get_configured_mtu_from_connection(device, + NM_TYPE_SETTING_IP_TUNNEL, + out_source); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +unrealize_notify(NMDevice *device) +{ + NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)->unrealize_notify(device); + + update_properties_from_ifindex(device, 0); +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + /* Only handle ip-tunnel setting here, delegate other settings to parent class */ + if (nm_streq(setting_name, NM_SETTING_IP_TUNNEL_SETTING_NAME)) { + return nm_device_hash_check_invalid_keys( + diffs, + NM_SETTING_IP_TUNNEL_SETTING_NAME, + error, + NM_SETTING_IP_TUNNEL_MTU); /* reapplied with IP config */ + } + + device_class = NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceIPTunnel * self = NM_DEVICE_IP_TUNNEL(device); + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self); + + if (_nm_ip_tunnel_mode_is_layer2(priv->mode) + && !nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) { + *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_MODE: + g_value_set_uint(value, priv->mode); + break; + case PROP_LOCAL: + g_value_set_string(value, priv->local); + break; + case PROP_REMOTE: + g_value_set_string(value, priv->remote); + break; + case PROP_TTL: + g_value_set_uchar(value, priv->ttl); + break; + case PROP_TOS: + g_value_set_uchar(value, priv->tos); + break; + case PROP_PATH_MTU_DISCOVERY: + g_value_set_boolean(value, priv->path_mtu_discovery); + break; + case PROP_INPUT_KEY: + g_value_set_string(value, priv->input_key); + break; + case PROP_OUTPUT_KEY: + g_value_set_string(value, priv->output_key); + break; + case PROP_ENCAPSULATION_LIMIT: + g_value_set_uchar(value, priv->encap_limit); + break; + case PROP_FLOW_LABEL: + g_value_set_uint(value, priv->flow_label); + break; + case PROP_FLAGS: + g_value_set_uint(value, priv->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_MODE: + priv->mode = g_value_get_uint(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +/*****************************************************************************/ + +static void +nm_device_ip_tunnel_init(NMDeviceIPTunnel *self) +{} + +static void +constructed(GObject *object) +{ + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object); + + if (NM_IN_SET(priv->mode, + NM_IP_TUNNEL_MODE_IPIP6, + NM_IP_TUNNEL_MODE_IP6IP6, + NM_IP_TUNNEL_MODE_IP6GRE, + NM_IP_TUNNEL_MODE_IP6GRETAP)) + priv->addr_family = AF_INET6; + else + priv->addr_family = AF_INET; + + G_OBJECT_CLASS(nm_device_ip_tunnel_parent_class)->constructed(object); +} + +static void +dispose(GObject *object) +{ + NMDeviceIPTunnel * self = NM_DEVICE_IP_TUNNEL(object); + NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self); + + nm_clear_g_free(&priv->local); + nm_clear_g_free(&priv->remote); + nm_clear_g_free(&priv->input_key); + nm_clear_g_free(&priv->output_key); + + G_OBJECT_CLASS(nm_device_ip_tunnel_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_ip_tunnel = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_IP_TUNNEL, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_DEVICE_IP_TUNNEL_MODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Local", + "s", + NM_DEVICE_IP_TUNNEL_LOCAL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Remote", + "s", + NM_DEVICE_IP_TUNNEL_REMOTE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ttl", "y", NM_DEVICE_IP_TUNNEL_TTL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tos", "y", NM_DEVICE_IP_TUNNEL_TOS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L( + "PathMtuDiscovery", + "b", + NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("InputKey", + "s", + NM_DEVICE_IP_TUNNEL_INPUT_KEY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("OutputKey", + "s", + NM_DEVICE_IP_TUNNEL_OUTPUT_KEY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L( + "EncapsulationLimit", + "y", + NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FlowLabel", + "u", + NM_DEVICE_IP_TUNNEL_FLOW_LABEL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Flags", + "u", + NM_DEVICE_IP_TUNNEL_FLAGS), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ip_tunnel); + + device_class->connection_type_supported = NM_SETTING_IP_TUNNEL_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_IP_TUNNEL_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_GRE, + NM_LINK_TYPE_GRETAP, + NM_LINK_TYPE_IP6TNL, + NM_LINK_TYPE_IP6GRE, + NM_LINK_TYPE_IP6GRETAP, + NM_LINK_TYPE_IPIP, + NM_LINK_TYPE_SIT); + + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->link_changed = link_changed; + device_class->can_reapply_change = can_reapply_change; + device_class->complete_connection = complete_connection; + device_class->update_connection = update_connection; + device_class->check_connection_compatible = check_connection_compatible; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->get_configured_mtu = get_configured_mtu; + device_class->unrealize_notify = unrealize_notify; + + obj_properties[PROP_MODE] = + g_param_spec_uint(NM_DEVICE_IP_TUNNEL_MODE, + "", + "", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LOCAL] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_LOCAL, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_REMOTE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_TTL, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_TOS, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_PATH_MTU_DISCOVERY] = + g_param_spec_boolean(NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_INPUT_KEY] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_INPUT_KEY, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_OUTPUT_KEY] = + g_param_spec_string(NM_DEVICE_IP_TUNNEL_OUTPUT_KEY, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ENCAPSULATION_LIMIT] = + g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_FLOW_LABEL] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FLOW_LABEL, + "", + "", + 0, + (1 << 20) - 1, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FLAGS, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_IP_TUNNEL_DEVICE_FACTORY (nm_ip_tunnel_device_factory_get_type()) +#define NM_IP_TUNNEL_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IP_TUNNEL_DEVICE_FACTORY, NMIPTunnelDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + NMSettingIPTunnel *s_ip_tunnel; + NMIPTunnelMode mode; + NMLinkType link_type; + + if (connection) { + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + mode = nm_setting_ip_tunnel_get_mode(s_ip_tunnel); + link_type = tunnel_mode_to_link_type(mode); + } else { + link_type = plink->type; + mode = platform_link_to_tunnel_mode(plink); + } + + if (mode == NM_IP_TUNNEL_MODE_UNKNOWN || link_type == NM_LINK_TYPE_UNKNOWN) + return NULL; + + return g_object_new(NM_TYPE_DEVICE_IP_TUNNEL, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "IPTunnel", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_IP_TUNNEL, + NM_DEVICE_LINK_TYPE, + link_type, + NM_DEVICE_IP_TUNNEL_MODE, + mode, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingIPTunnel *s_ip_tunnel; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_IP_TUNNEL_SETTING_NAME), + NULL); + + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + g_assert(s_ip_tunnel); + + return nm_setting_ip_tunnel_get_parent(s_ip_tunnel); +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + const char * ifname; + NMSettingIPTunnel *s_ip_tunnel; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_IP_TUNNEL_SETTING_NAME), + NULL); + + s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection); + g_assert(s_ip_tunnel); + + if (nm_setting_ip_tunnel_get_parent(s_ip_tunnel) && !parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + + return g_strdup(ifname); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + IP_TUNNEL, + IPTunnel, + ip_tunnel, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_GRE, + NM_LINK_TYPE_GRETAP, + NM_LINK_TYPE_SIT, + NM_LINK_TYPE_IPIP, + NM_LINK_TYPE_IP6TNL, + NM_LINK_TYPE_IP6GRE, + NM_LINK_TYPE_IP6GRETAP) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_IP_TUNNEL_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-ip-tunnel.h b/src/core/devices/nm-device-ip-tunnel.h new file mode 100644 index 00000000..0a941cc2 --- /dev/null +++ b/src/core/devices/nm-device-ip-tunnel.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__ +#define __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__ + +#include "nm-core-types.h" +#include "nm-device.h" + +#define NM_TYPE_DEVICE_IP_TUNNEL (nm_device_ip_tunnel_get_type()) +#define NM_DEVICE_IP_TUNNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnel)) +#define NM_DEVICE_IP_TUNNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnelClass)) +#define NM_IS_DEVICE_IP_TUNNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IP_TUNNEL)) +#define NM_IS_DEVICE_IP_TUNNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_IP_TUNNEL)) +#define NM_DEVICE_IP_TUNNEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnelClass)) + +#define NM_DEVICE_IP_TUNNEL_MODE "mode" +#define NM_DEVICE_IP_TUNNEL_LOCAL "local" +#define NM_DEVICE_IP_TUNNEL_REMOTE "remote" +#define NM_DEVICE_IP_TUNNEL_TTL "ttl" +#define NM_DEVICE_IP_TUNNEL_TOS "tos" +#define NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY "path-mtu-discovery" +#define NM_DEVICE_IP_TUNNEL_INPUT_KEY "input-key" +#define NM_DEVICE_IP_TUNNEL_OUTPUT_KEY "output-key" +#define NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT "encapsulation-limit" +#define NM_DEVICE_IP_TUNNEL_FLOW_LABEL "flow-label" +#define NM_DEVICE_IP_TUNNEL_FLAGS "flags" + +typedef struct _NMDeviceIPTunnel NMDeviceIPTunnel; +typedef struct _NMDeviceIPTunnelClass NMDeviceIPTunnelClass; + +GType nm_device_ip_tunnel_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__ */ diff --git a/src/core/devices/nm-device-logging.h b/src/core/devices/nm-device-logging.h new file mode 100644 index 00000000..844e9949 --- /dev/null +++ b/src/core/devices/nm-device-logging.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_LOGGING_H__ +#define __NETWORKMANAGER_DEVICE_LOGGING_H__ + +#include "nm-device.h" + +#if !_NM_CC_SUPPORT_GENERIC + #define _NM_DEVICE_CAST(self) ((NMDevice *) (self)) +#elif !defined(_NMLOG_DEVICE_TYPE) + #define _NM_DEVICE_CAST(self) \ + _Generic((self), NMDevice * \ + : ((NMDevice *) (self)), NMDevice *const \ + : ((NMDevice *) (self))) +#else + #define _NM_DEVICE_CAST(self) \ + _Generic((self), \ + _NMLOG_DEVICE_TYPE * : ((NMDevice *) (self)), \ + _NMLOG_DEVICE_TYPE * const: ((NMDevice *) (self)), \ + NMDevice * : ((NMDevice *) (self)), \ + NMDevice * const: ((NMDevice *) (self))) +#endif + +#undef _NMLOG_ENABLED +#define _NMLOG_ENABLED(level, domain) (nm_logging_enabled((level), (domain))) +#define _NMLOG(level, domain, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + const NMLogDomain _domain = (domain); \ + \ + if (nm_logging_enabled(_level, _domain)) { \ + typeof(*self) *const _self = (self); \ + const char *const _ifname = _nm_device_get_iface(_NM_DEVICE_CAST(_self)); \ + \ + nm_log_obj(_level, \ + _domain, \ + _ifname, \ + NULL, \ + _self, \ + "device", \ + "%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + NM_PRINT_FMT_QUOTED(_ifname, "(", _ifname, ")", "[null]") \ + _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +#endif /* __NETWORKMANAGER_DEVICE_LOGGING_H__ */ diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c new file mode 100644 index 00000000..51b820a1 --- /dev/null +++ b/src/core/devices/nm-device-macsec.c @@ -0,0 +1,1090 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-macsec.h" + +#include <linux/if_ether.h> + +#include "nm-act-request.h" +#include "nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-manager.h" +#include "nm-setting-macsec.h" +#include "nm-core-internal.h" +#include "supplicant/nm-supplicant-manager.h" +#include "supplicant/nm-supplicant-interface.h" +#include "supplicant/nm-supplicant-config.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceMacsec +#include "nm-device-logging.h" + +/*****************************************************************************/ + +#define SUPPLICANT_LNK_TIMEOUT_SEC 15 + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacsec, + PROP_SCI, + PROP_CIPHER_SUITE, + PROP_ICV_LENGTH, + PROP_WINDOW, + PROP_ENCODING_SA, + PROP_ENCRYPT, + PROP_PROTECT, + PROP_INCLUDE_SCI, + PROP_ES, + PROP_SCB, + PROP_REPLAY_PROTECT, + PROP_VALIDATION, ); + +typedef struct { + NMPlatformLnkMacsec props; + gulong parent_state_id; + gulong parent_mtu_id; + + struct { + NMSupplicantManager * mgr; + NMSupplMgrCreateIfaceHandle *create_handle; + NMSupplicantInterface * iface; + + gulong iface_state_id; + + guint con_timeout_id; + guint lnk_timeout_id; + + bool is_associated : 1; + } supplicant; + + NMActRequestGetSecretsCallId *macsec_secrets_id; +} NMDeviceMacsecPrivate; + +struct _NMDeviceMacsec { + NMDevice parent; + NMDeviceMacsecPrivate _priv; +}; + +struct _NMDeviceMacsecClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceMacsec, nm_device_macsec, NM_TYPE_DEVICE) + +#define NM_DEVICE_MACSEC_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceMacsec, NM_IS_DEVICE_MACSEC, NMDevice) + +/******************************************************************/ + +static void macsec_secrets_cancel(NMDeviceMacsec *self); + +/******************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE(validation_mode_to_string, + guint8, + NM_UTILS_LOOKUP_DEFAULT_WARN("<unknown>"), + NM_UTILS_LOOKUP_STR_ITEM(0, "disable"), + NM_UTILS_LOOKUP_STR_ITEM(1, "check"), + NM_UTILS_LOOKUP_STR_ITEM(2, "strict"), ); + +static void +parent_state_changed(NMDevice * parent, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason, + gpointer user_data) +{ + NMDeviceMacsec *self = NM_DEVICE_MACSEC(user_data); + + /* We'll react to our own carrier state notifications. Ignore the parent's. */ + if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER) + return; + + nm_device_set_unmanaged_by_flags(NM_DEVICE(self), + NM_UNMANAGED_PARENT, + !nm_device_get_managed(parent, FALSE), + reason); +} + +static void +parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data) +{ + /* the MTU of a MACsec device is limited by the parent's MTU. + * + * When the parent's MTU changes, try to re-set the MTU. */ + nm_device_commit_mtu(user_data); +} + +static void +parent_changed_notify(NMDevice *device, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(device); + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + NM_DEVICE_CLASS(nm_device_macsec_parent_class) + ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent); + + /* note that @self doesn't have to clear @parent_state_id on dispose, + * because NMDevice's dispose() will unset the parent, which in turn calls + * parent_changed_notify(). */ + nm_clear_g_signal_handler(old_parent, &priv->parent_state_id); + nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id); + + if (new_parent) { + priv->parent_state_id = g_signal_connect(new_parent, + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(parent_state_changed), + device); + priv->parent_mtu_id = g_signal_connect(new_parent, + "notify::" NM_DEVICE_MTU, + G_CALLBACK(parent_mtu_maybe_changed), + device); + + /* Set parent-dependent unmanaged flag */ + nm_device_set_unmanaged_by_flags(device, + NM_UNMANAGED_PARENT, + !nm_device_get_managed(new_parent, FALSE), + NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED); + } + + /* Recheck availability now that the parent has changed */ + if (new_ifindex > 0) { + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_PARENT_CHANGED, + NM_DEVICE_STATE_REASON_PARENT_CHANGED); + } +} + +static void +update_properties(NMDevice *device) +{ + NMDeviceMacsec * self; + NMDeviceMacsecPrivate * priv; + const NMPlatformLink * plink = NULL; + const NMPlatformLnkMacsec *props = NULL; + int ifindex; + + g_return_if_fail(NM_IS_DEVICE_MACSEC(device)); + self = NM_DEVICE_MACSEC(device); + priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + ifindex = nm_device_get_ifindex(device); + g_return_if_fail(ifindex > 0); + props = nm_platform_link_get_lnk_macsec(nm_device_get_platform(device), ifindex, &plink); + + if (!props) { + _LOGW(LOGD_PLATFORM, "could not get macsec properties"); + return; + } + + g_object_freeze_notify((GObject *) device); + + if (priv->props.parent_ifindex != props->parent_ifindex) + nm_device_parent_set_ifindex(device, props->parent_ifindex); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED(sci, PROP_SCI); + CHECK_PROPERTY_CHANGED(cipher_suite, PROP_CIPHER_SUITE); + CHECK_PROPERTY_CHANGED(window, PROP_WINDOW); + CHECK_PROPERTY_CHANGED(icv_length, PROP_ICV_LENGTH); + CHECK_PROPERTY_CHANGED(encoding_sa, PROP_ENCODING_SA); + CHECK_PROPERTY_CHANGED(validation, PROP_VALIDATION); + CHECK_PROPERTY_CHANGED(encrypt, PROP_ENCRYPT); + CHECK_PROPERTY_CHANGED(protect, PROP_PROTECT); + CHECK_PROPERTY_CHANGED(include_sci, PROP_INCLUDE_SCI); + CHECK_PROPERTY_CHANGED(es, PROP_ES); + CHECK_PROPERTY_CHANGED(scb, PROP_SCB); + CHECK_PROPERTY_CHANGED(replay_protect, PROP_REPLAY_PROTECT); + + g_object_thaw_notify((GObject *) device); +} + +static NMSupplicantConfig * +build_supplicant_config(NMDeviceMacsec *self, GError **error) +{ + gs_unref_object NMSupplicantConfig *config = NULL; + NMSettingMacsec * s_macsec; + NMSetting8021x * s_8021x; + NMConnection * connection; + const char * con_uuid; + guint32 mtu; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + g_return_val_if_fail(connection, NULL); + + con_uuid = nm_connection_get_uuid(connection); + mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), + nm_device_get_ifindex(NM_DEVICE(self))); + + config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE); + + s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC); + + g_return_val_if_fail(s_macsec, NULL); + + if (!nm_supplicant_config_add_setting_macsec(config, s_macsec, error)) { + g_prefix_error(error, "macsec-setting: "); + return NULL; + } + + if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) { + s_8021x = nm_connection_get_setting_802_1x(connection); + if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) { + g_prefix_error(error, "802-1x-setting: "); + return NULL; + } + } + + return g_steal_pointer(&config); +} + +static void +supplicant_interface_release(NMDeviceMacsec *self) +{ + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + nm_clear_pointer(&priv->supplicant.create_handle, + nm_supplicant_manager_create_interface_cancel); + + nm_clear_g_source(&priv->supplicant.lnk_timeout_id); + nm_clear_g_source(&priv->supplicant.con_timeout_id); + nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id); + + if (priv->supplicant.iface) { + nm_supplicant_interface_disconnect(priv->supplicant.iface); + g_clear_object(&priv->supplicant.iface); + } +} + +static void +macsec_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * connection, + GError * error, + gpointer user_data) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(user_data); + NMDevice * device = NM_DEVICE(self); + NMDeviceMacsecPrivate *priv; + + g_return_if_fail(NM_IS_DEVICE_MACSEC(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + g_return_if_fail(priv->macsec_secrets_id == call_id); + + priv->macsec_secrets_id = NULL; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + g_return_if_fail(nm_act_request_get_settings_connection(req) == connection); + + if (error) { + _LOGW(LOGD_ETHER, "%s", error->message); + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +macsec_secrets_cancel(NMDeviceMacsec *self) +{ + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + if (priv->macsec_secrets_id) + nm_act_request_cancel_secrets(NULL, priv->macsec_secrets_id); + nm_assert(!priv->macsec_secrets_id); +} + +static void +macsec_secrets_get_secrets(NMDeviceMacsec * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags) +{ + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + NMActRequest * req; + + macsec_secrets_cancel(self); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv->macsec_secrets_id = + nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, macsec_secrets_cb, self); + g_return_if_fail(priv->macsec_secrets_id); +} + +static gboolean +supplicant_lnk_timeout_cb(gpointer user_data) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(user_data); + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + NMDevice * dev = NM_DEVICE(self); + NMActRequest * req; + NMConnection * applied_connection; + const char * setting_name; + + priv->supplicant.lnk_timeout_id = 0; + + req = nm_device_get_act_request(dev); + + if (nm_device_get_state(dev) == NM_DEVICE_STATE_ACTIVATED) { + nm_device_state_changed(dev, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT); + return G_SOURCE_REMOVE; + } + + /* Disconnect event during initial authentication and credentials + * ARE checked - we are likely to have wrong key. Ask the user for + * another one. + */ + if (nm_device_get_state(dev) != NM_DEVICE_STATE_CONFIG) + goto time_out; + + nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req)); + + applied_connection = nm_act_request_get_applied_connection(req); + setting_name = nm_connection_need_secrets(applied_connection, NULL); + if (!setting_name) + goto time_out; + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: disconnected during authentication, asking for new key."); + supplicant_interface_release(self); + + nm_device_state_changed(dev, + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + macsec_secrets_get_secrets(self, setting_name, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW); + + return G_SOURCE_REMOVE; + +time_out: + _LOGW(LOGD_DEVICE | LOGD_ETHER, "link timed out."); + nm_device_state_changed(dev, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + + return G_SOURCE_REMOVE; +} + +static void +supplicant_iface_state_is_completed(NMDeviceMacsec *self, NMSupplicantInterfaceState state) +{ + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + if (state == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) { + nm_clear_g_source(&priv->supplicant.lnk_timeout_id); + nm_clear_g_source(&priv->supplicant.con_timeout_id); + + nm_device_bring_up(NM_DEVICE(self), TRUE, NULL); + + /* If this is the initial association during device activation, + * schedule the next activation stage. + */ + if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) { + _LOGI(LOGD_DEVICE, "Activation: Stage 2 of 5 (Device Configure) successful."); + nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self)); + } + return; + } + + if (!priv->supplicant.lnk_timeout_id && !priv->supplicant.con_timeout_id) + priv->supplicant.lnk_timeout_id = + g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self); +} + +static void +supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data) +{ + NMDeviceMacsec * self; + NMDeviceMacsecPrivate *priv; + + if (nm_utils_error_is_cancelled_or_disposing(error)) + return; + + self = user_data; + priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + if (error) { + supplicant_interface_release(self); + nm_device_queue_state(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED); + return; + } + + nm_assert(!priv->supplicant.lnk_timeout_id); + nm_assert(!priv->supplicant.is_associated); + + priv->supplicant.is_associated = TRUE; + supplicant_iface_state_is_completed(self, + nm_supplicant_interface_get_state(priv->supplicant.iface)); +} + +static gboolean +supplicant_iface_start(NMDeviceMacsec *self) +{ + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + gs_unref_object NMSupplicantConfig *config = NULL; + gs_free_error GError *error = NULL; + + config = build_supplicant_config(self, &error); + if (!config) { + _LOGE(LOGD_DEVICE, "Activation: couldn't build security configuration: %s", error->message); + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED); + return FALSE; + } + + nm_supplicant_interface_disconnect(priv->supplicant.iface); + nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self); + return TRUE; +} + +static void +supplicant_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(user_data); + NMDeviceMacsecPrivate * priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + NMSupplicantInterfaceState new_state = new_state_i; + NMSupplicantInterfaceState old_state = old_state_i; + + _LOGI(LOGD_DEVICE, + "supplicant interface state: %s -> %s", + nm_supplicant_interface_state_to_string(old_state), + nm_supplicant_interface_state_to_string(new_state)); + + if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) { + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + if (!supplicant_iface_start(self)) + return; + } + + if (priv->supplicant.is_associated) + supplicant_iface_state_is_completed(self, new_state); +} + +static gboolean +handle_auth_or_fail(NMDeviceMacsec *self, NMActRequest *req, gboolean new_secrets) +{ + const char * setting_name; + NMConnection *applied_connection; + + if (!nm_device_auth_retries_try_next(NM_DEVICE(self))) + return FALSE; + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_NONE); + + nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req)); + + applied_connection = nm_act_request_get_applied_connection(req); + setting_name = nm_connection_need_secrets(applied_connection, NULL); + if (!setting_name) { + _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets."); + return FALSE; + } + + macsec_secrets_get_secrets( + self, + setting_name, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0)); + return TRUE; +} + +static gboolean +supplicant_connection_timeout_cb(gpointer user_data) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(user_data); + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMActRequest * req; + NMSettingsConnection * connection; + guint64 timestamp = 0; + gboolean new_secrets = TRUE; + + priv->supplicant.con_timeout_id = 0; + + /* Authentication failed; either driver problems, the encryption key is + * wrong, the passwords or certificates were wrong or the Ethernet switch's + * port is not configured for 802.1x. */ + _LOGW(LOGD_DEVICE, "Activation: (macsec) association took too long."); + + supplicant_interface_release(self); + + req = nm_device_get_act_request(device); + connection = nm_act_request_get_settings_connection(req); + g_return_val_if_fail(connection, G_SOURCE_REMOVE); + + /* Ask for new secrets only if we've never activated this connection + * before. If we've connected before, don't bother the user with dialogs, + * just retry or fail, and if we never connect the user can fix the + * password somewhere else. */ + if (nm_settings_connection_get_timestamp(connection, ×tamp)) + new_secrets = !timestamp; + + if (!handle_auth_or_fail(self, req, new_secrets)) { + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return G_SOURCE_REMOVE; + } + + _LOGW(LOGD_DEVICE, "Activation: (macsec) asking for new secrets"); + + if (!priv->supplicant.lnk_timeout_id && priv->supplicant.iface) { + NMSupplicantInterfaceState state; + + state = nm_supplicant_interface_get_state(priv->supplicant.iface); + if (state != NM_SUPPLICANT_INTERFACE_STATE_COMPLETED + && nm_supplicant_interface_state_is_operational(state)) + priv->supplicant.lnk_timeout_id = + g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self); + } + + return G_SOURCE_REMOVE; +} + +static void +supplicant_interface_create_cb(NMSupplicantManager * supplicant_manager, + NMSupplMgrCreateIfaceHandle *handle, + NMSupplicantInterface * iface, + GError * error, + gpointer user_data) +{ + NMDeviceMacsec * self; + NMDeviceMacsecPrivate *priv; + guint timeout; + + if (nm_utils_error_is_cancelled(error)) + return; + + self = user_data; + priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + nm_assert(priv->supplicant.create_handle == handle); + + priv->supplicant.create_handle = NULL; + + if (error) { + _LOGE(LOGD_DEVICE, "Couldn't initialize supplicant interface: %s", error->message); + supplicant_interface_release(self); + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + priv->supplicant.iface = g_object_ref(iface); + priv->supplicant.is_associated = FALSE; + + priv->supplicant.iface_state_id = g_signal_connect(priv->supplicant.iface, + NM_SUPPLICANT_INTERFACE_STATE, + G_CALLBACK(supplicant_iface_state_cb), + self); + + timeout = nm_device_get_supplicant_timeout(NM_DEVICE(self)); + priv->supplicant.con_timeout_id = + g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self); + + if (nm_supplicant_interface_state_is_operational(nm_supplicant_interface_get_state(iface))) + supplicant_iface_start(self); +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(device); + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + NMConnection * connection; + NMDevice * parent; + const char * setting_name; + int ifindex; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + if (!priv->supplicant.mgr) + priv->supplicant.mgr = g_object_ref(nm_supplicant_manager_get()); + + /* If we need secrets, get them */ + setting_name = nm_connection_need_secrets(connection, NULL); + if (setting_name) { + NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self)); + + _LOGI(LOGD_DEVICE, + "Activation: connection '%s' has security, but secrets are required.", + nm_connection_get_id(connection)); + + if (!handle_auth_or_fail(self, req, FALSE)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + _LOGI(LOGD_DEVICE | LOGD_ETHER, + "Activation: connection '%s' requires no security. No secrets needed.", + nm_connection_get_id(connection)); + + supplicant_interface_release(self); + + parent = nm_device_parent_get_device(NM_DEVICE(self)); + g_return_val_if_fail(parent, NM_ACT_STAGE_RETURN_FAILURE); + ifindex = nm_device_get_ifindex(parent); + g_return_val_if_fail(ifindex > 0, NM_ACT_STAGE_RETURN_FAILURE); + + priv->supplicant.create_handle = + nm_supplicant_manager_create_interface(priv->supplicant.mgr, + ifindex, + NM_SUPPLICANT_DRIVER_MACSEC, + supplicant_interface_create_cb, + self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceMacsec *self = NM_DEVICE_MACSEC(device); + + supplicant_interface_release(self); +} + +/******************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + /* We assume MACsec interfaces always support carrier detect */ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +/******************************************************************/ + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + if (!nm_device_parent_get_device(device)) + return FALSE; + return NM_DEVICE_CLASS(nm_device_macsec_parent_class)->is_available(device, flags); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSettingMacsec * s_macsec; + NMPlatformLnkMacsec lnk = {}; + int parent_ifindex; + const char * hw_addr; + union { + struct { + guint8 mac[6]; + guint16 port; + } s; + guint64 u; + } sci; + int r; + + s_macsec = nm_connection_get_setting_macsec(connection); + g_assert(s_macsec); + + if (!parent) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "MACsec devices can not be created without a parent interface"); + return FALSE; + } + + lnk.encrypt = nm_setting_macsec_get_encrypt(s_macsec); + + hw_addr = nm_device_get_hw_address(parent); + if (!hw_addr) { + g_set_error(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "can't read parent MAC"); + return FALSE; + } + + nm_utils_hwaddr_aton(hw_addr, sci.s.mac, ETH_ALEN); + sci.s.port = htons(nm_setting_macsec_get_port(s_macsec)); + lnk.sci = be64toh(sci.u); + lnk.validation = nm_setting_macsec_get_validation(s_macsec); + lnk.include_sci = nm_setting_macsec_get_send_sci(s_macsec); + + parent_ifindex = nm_device_get_ifindex(parent); + g_warn_if_fail(parent_ifindex > 0); + + r = nm_platform_link_macsec_add(nm_device_get_platform(device), + iface, + parent_ifindex, + &lnk, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create macsec interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + nm_device_parent_set_ifindex(device, parent_ifindex); + + return TRUE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_macsec_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + if (new_state > NM_DEVICE_STATE_ACTIVATED) + macsec_secrets_cancel(NM_DEVICE_MACSEC(device)); +} + +/******************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceMacsec * self = NM_DEVICE_MACSEC(object); + NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_SCI: + g_value_set_uint64(value, priv->props.sci); + break; + case PROP_CIPHER_SUITE: + g_value_set_uint64(value, priv->props.cipher_suite); + break; + case PROP_ICV_LENGTH: + g_value_set_uchar(value, priv->props.icv_length); + break; + case PROP_WINDOW: + g_value_set_uint(value, priv->props.window); + break; + case PROP_ENCODING_SA: + g_value_set_uchar(value, priv->props.encoding_sa); + break; + case PROP_ENCRYPT: + g_value_set_boolean(value, priv->props.encrypt); + break; + case PROP_PROTECT: + g_value_set_boolean(value, priv->props.protect); + break; + case PROP_INCLUDE_SCI: + g_value_set_boolean(value, priv->props.include_sci); + break; + case PROP_ES: + g_value_set_boolean(value, priv->props.es); + break; + case PROP_SCB: + g_value_set_boolean(value, priv->props.scb); + break; + case PROP_REPLAY_PROTECT: + g_value_set_boolean(value, priv->props.replay_protect); + break; + case PROP_VALIDATION: + g_value_set_string(value, validation_mode_to_string(priv->props.validation)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +nm_device_macsec_init(NMDeviceMacsec *self) +{} + +static void +dispose(GObject *object) +{ + NMDeviceMacsec *self = NM_DEVICE_MACSEC(object); + + macsec_secrets_cancel(self); + supplicant_interface_release(self); + + G_OBJECT_CLASS(nm_device_macsec_parent_class)->dispose(object); + + nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_state_id == 0); + nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_mtu_id == 0); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_macsec = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_MACSEC, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Sci", "t", NM_DEVICE_MACSEC_SCI), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IcvLength", + "y", + NM_DEVICE_MACSEC_ICV_LENGTH), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("CipherSuite", + "t", + NM_DEVICE_MACSEC_CIPHER_SUITE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Window", + "u", + NM_DEVICE_MACSEC_WINDOW), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("EncodingSa", + "y", + NM_DEVICE_MACSEC_ENCODING_SA), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Validation", + "s", + NM_DEVICE_MACSEC_VALIDATION), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Encrypt", + "b", + NM_DEVICE_MACSEC_ENCRYPT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Protect", + "b", + NM_DEVICE_MACSEC_PROTECT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IncludeSci", + "b", + NM_DEVICE_MACSEC_INCLUDE_SCI), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Es", "b", NM_DEVICE_MACSEC_ES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Scb", "b", NM_DEVICE_MACSEC_SCB), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ReplayProtect", + "b", + NM_DEVICE_MACSEC_REPLAY_PROTECT), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_macsec_class_init(NMDeviceMacsecClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + object_class->dispose = dispose; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_macsec); + + device_class->connection_type_supported = NM_SETTING_MACSEC_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_MACSEC_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_MACSEC); + device_class->mtu_parent_delta = 32; + + device_class->act_stage2_config = act_stage2_config; + device_class->create_and_realize = create_and_realize; + device_class->deactivate = deactivate; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->link_changed = link_changed; + device_class->is_available = is_available; + device_class->parent_changed_notify = parent_changed_notify; + device_class->state_changed = device_state_changed; + device_class->get_configured_mtu = nm_device_get_configured_mtu_wired_parent; + + obj_properties[PROP_SCI] = g_param_spec_uint64(NM_DEVICE_MACSEC_SCI, + "", + "", + 0, + G_MAXUINT64, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_CIPHER_SUITE] = + g_param_spec_uint64(NM_DEVICE_MACSEC_CIPHER_SUITE, + "", + "", + 0, + G_MAXUINT64, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_ICV_LENGTH] = g_param_spec_uchar(NM_DEVICE_MACSEC_ICV_LENGTH, + "", + "", + 0, + G_MAXUINT8, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_WINDOW] = g_param_spec_uint(NM_DEVICE_MACSEC_WINDOW, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_ENCODING_SA] = + g_param_spec_uchar(NM_DEVICE_MACSEC_ENCODING_SA, + "", + "", + 0, + 3, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_VALIDATION] = + g_param_spec_string(NM_DEVICE_MACSEC_VALIDATION, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_ENCRYPT] = g_param_spec_boolean(NM_DEVICE_MACSEC_ENCRYPT, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_PROTECT] = g_param_spec_boolean(NM_DEVICE_MACSEC_PROTECT, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_INCLUDE_SCI] = + g_param_spec_boolean(NM_DEVICE_MACSEC_INCLUDE_SCI, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_ES] = g_param_spec_boolean(NM_DEVICE_MACSEC_ES, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_SCB] = g_param_spec_boolean(NM_DEVICE_MACSEC_SCB, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_REPLAY_PROTECT] = + g_param_spec_boolean(NM_DEVICE_MACSEC_REPLAY_PROTECT, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*************************************************************/ + +#define NM_TYPE_MACSEC_DEVICE_FACTORY (nm_macsec_device_factory_get_type()) +#define NM_MACSEC_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACSEC_DEVICE_FACTORY, NMMacsecDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_MACSEC, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Macsec", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_MACSEC, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_MACSEC, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingMacsec *s_macsec; + NMSettingWired * s_wired; + const char * parent = NULL; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACSEC_SETTING_NAME), NULL); + + s_macsec = nm_connection_get_setting_macsec(connection); + g_assert(s_macsec); + + parent = nm_setting_macsec_get_parent(s_macsec); + if (parent) + return parent; + + /* Try the hardware address from the MACsec connection's hardware setting */ + s_wired = nm_connection_get_setting_wired(connection); + if (s_wired) + return nm_setting_wired_get_mac_address(s_wired); + + return NULL; +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + NMSettingMacsec *s_macsec; + const char * ifname; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACSEC_SETTING_NAME), NULL); + + s_macsec = nm_connection_get_setting_macsec(connection); + g_assert(s_macsec); + + if (!parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + return g_strdup(ifname); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + MACSEC, + Macsec, + macsec, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_MACSEC) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_MACSEC_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;) diff --git a/src/core/devices/nm-device-macsec.h b/src/core/devices/nm-device-macsec.h new file mode 100644 index 00000000..e91fe51c --- /dev/null +++ b/src/core/devices/nm-device-macsec.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NM_DEVICE_MACSEC_H__ +#define __NM_DEVICE_MACSEC_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_MACSEC (nm_device_macsec_get_type()) +#define NM_DEVICE_MACSEC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsec)) +#define NM_DEVICE_MACSEC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsecClass)) +#define NM_IS_DEVICE_MACSEC(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACSEC)) +#define NM_IS_DEVICE_MACSEC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MACSEC)) +#define NM_DEVICE_MACSEC_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsecClass)) + +#define NM_DEVICE_MACSEC_SCI "sci" +#define NM_DEVICE_MACSEC_CIPHER_SUITE "cipher-suite" +#define NM_DEVICE_MACSEC_ICV_LENGTH "icv-length" +#define NM_DEVICE_MACSEC_WINDOW "window" +#define NM_DEVICE_MACSEC_ENCODING_SA "encoding-sa" +#define NM_DEVICE_MACSEC_VALIDATION "validation" +#define NM_DEVICE_MACSEC_ENCRYPT "encrypt" +#define NM_DEVICE_MACSEC_PROTECT "protect" +#define NM_DEVICE_MACSEC_INCLUDE_SCI "include-sci" +#define NM_DEVICE_MACSEC_ES "es" +#define NM_DEVICE_MACSEC_SCB "scb" +#define NM_DEVICE_MACSEC_REPLAY_PROTECT "replay-protect" + +typedef struct _NMDeviceMacsec NMDeviceMacsec; +typedef struct _NMDeviceMacsecClass NMDeviceMacsecClass; + +GType nm_device_macsec_get_type(void); + +#endif /* __NM_DEVICE_MACSEC_H__ */ diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c new file mode 100644 index 00000000..e8b39ed6 --- /dev/null +++ b/src/core/devices/nm-device-macvlan.c @@ -0,0 +1,665 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-macvlan.h" + +#include <linux/if_link.h> + +#include "nm-device-private.h" +#include "settings/nm-settings.h" +#include "nm-act-request.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-macvlan.h" +#include "nm-setting-wired.h" +#include "nm-active-connection.h" +#include "nm-ip4-config.h" +#include "nm-utils.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceMacvlan +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacvlan, PROP_MODE, PROP_NO_PROMISC, PROP_TAP, ); + +typedef struct { + gulong parent_state_id; + gulong parent_mtu_id; + NMPlatformLnkMacvlan props; +} NMDeviceMacvlanPrivate; + +struct _NMDeviceMacvlan { + NMDevice parent; + NMDeviceMacvlanPrivate _priv; +}; + +struct _NMDeviceMacvlanClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceMacvlan, nm_device_macvlan, NM_TYPE_DEVICE) + +#define NM_DEVICE_MACVLAN_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceMacvlan, NM_IS_DEVICE_MACVLAN, NMDevice) + +/*****************************************************************************/ + +static int modes[][2] = { + {NM_SETTING_MACVLAN_MODE_VEPA, MACVLAN_MODE_VEPA}, + {NM_SETTING_MACVLAN_MODE_BRIDGE, MACVLAN_MODE_BRIDGE}, + {NM_SETTING_MACVLAN_MODE_PRIVATE, MACVLAN_MODE_PRIVATE}, + {NM_SETTING_MACVLAN_MODE_PASSTHRU, MACVLAN_MODE_PASSTHRU}, +}; + +static int +setting_mode_to_platform(int mode) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS(modes); i++) { + if (modes[i][0] == mode) + return modes[i][1]; + } + + return 0; +} + +static int +platform_mode_to_setting(int mode) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS(modes); i++) { + if (modes[i][1] == mode) + return modes[i][0]; + } + + return 0; +} + +static const char * +platform_mode_to_string(guint mode) +{ + switch (mode) { + case MACVLAN_MODE_PRIVATE: + return "private"; + case MACVLAN_MODE_VEPA: + return "vepa"; + case MACVLAN_MODE_BRIDGE: + return "bridge"; + case MACVLAN_MODE_PASSTHRU: + return "passthru"; + default: + return "unknown"; + } +} + +/*****************************************************************************/ + +static void +parent_state_changed(NMDevice * parent, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason, + gpointer user_data) +{ + NMDeviceMacvlan *self = NM_DEVICE_MACVLAN(user_data); + + /* We'll react to our own carrier state notifications. Ignore the parent's. */ + if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER) + return; + + nm_device_set_unmanaged_by_flags(NM_DEVICE(self), + NM_UNMANAGED_PARENT, + !nm_device_get_managed(parent, FALSE), + reason); +} + +static void +parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data) +{ + /* the MTU of a macvlan/macvtap device is limited by the parent's MTU. + * + * When the parent's MTU changes, try to re-set the MTU. */ + nm_device_commit_mtu(user_data); +} + +static void +parent_changed_notify(NMDevice *device, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + NMDeviceMacvlan * self = NM_DEVICE_MACVLAN(device); + NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(self); + + NM_DEVICE_CLASS(nm_device_macvlan_parent_class) + ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent); + + /* note that @self doesn't have to clear @parent_state_id on dispose, + * because NMDevice's dispose() will unset the parent, which in turn calls + * parent_changed_notify(). */ + nm_clear_g_signal_handler(old_parent, &priv->parent_state_id); + nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id); + + if (new_parent) { + priv->parent_state_id = g_signal_connect(new_parent, + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(parent_state_changed), + device); + priv->parent_mtu_id = g_signal_connect(new_parent, + "notify::" NM_DEVICE_MTU, + G_CALLBACK(parent_mtu_maybe_changed), + device); + + /* Set parent-dependent unmanaged flag */ + nm_device_set_unmanaged_by_flags(device, + NM_UNMANAGED_PARENT, + !nm_device_get_managed(new_parent, FALSE), + NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED); + } + + if (new_ifindex > 0) { + /* Recheck availability now that the parent has changed */ + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_PARENT_CHANGED, + NM_DEVICE_STATE_REASON_PARENT_CHANGED); + } +} + +static void +update_properties(NMDevice *device) +{ + NMDeviceMacvlan * self = NM_DEVICE_MACVLAN(device); + NMDeviceMacvlanPrivate * priv = NM_DEVICE_MACVLAN_GET_PRIVATE(self); + GObject * object = G_OBJECT(device); + const NMPlatformLnkMacvlan *props; + const NMPlatformLink * plink; + + if (priv->props.tap) + props = nm_platform_link_get_lnk_macvtap(nm_device_get_platform(device), + nm_device_get_ifindex(device), + &plink); + else + props = nm_platform_link_get_lnk_macvlan(nm_device_get_platform(device), + nm_device_get_ifindex(device), + &plink); + + if (!props) { + _LOGW(LOGD_PLATFORM, + "could not get %s properties", + priv->props.tap ? "macvtap" : "macvlan"); + return; + } + + g_object_freeze_notify(object); + + nm_device_parent_set_ifindex(device, plink->parent); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED(mode, PROP_MODE); + CHECK_PROPERTY_CHANGED(no_promisc, PROP_NO_PROMISC); + + g_object_thaw_notify(object); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_macvlan_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSettingMacvlan * s_macvlan; + NMPlatformLnkMacvlan lnk = {}; + int parent_ifindex; + int r; + + s_macvlan = nm_connection_get_setting_macvlan(connection); + g_return_val_if_fail(s_macvlan, FALSE); + + parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0; + + if (parent_ifindex <= 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "MACVLAN devices can not be created without a parent interface"); + g_return_val_if_fail(!parent, FALSE); + return FALSE; + } + + lnk.mode = setting_mode_to_platform(nm_setting_macvlan_get_mode(s_macvlan)); + if (!lnk.mode) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "unsupported MACVLAN mode %u in connection %s", + nm_setting_macvlan_get_mode(s_macvlan), + nm_connection_get_uuid(connection)); + return FALSE; + } + lnk.no_promisc = !nm_setting_macvlan_get_promiscuous(s_macvlan); + lnk.tap = nm_setting_macvlan_get_tap(s_macvlan); + + r = nm_platform_link_macvlan_add(nm_device_get_platform(device), + iface, + parent_ifindex, + &lnk, + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create %s interface '%s' for '%s': %s", + lnk.tap ? "macvtap" : "macvlan", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + /* We assume MACVLAN interfaces always support carrier detect */ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +/*****************************************************************************/ + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + if (!nm_device_parent_get_device(device)) + return FALSE; + return NM_DEVICE_CLASS(nm_device_macvlan_parent_class)->is_available(device, flags); +} + +/*****************************************************************************/ + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(device); + NMSettingMacvlan * s_macvlan; + const char * parent = NULL; + + if (!NM_DEVICE_CLASS(nm_device_macvlan_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_macvlan = nm_connection_get_setting_macvlan(connection); + + if (nm_setting_macvlan_get_tap(s_macvlan) != priv->props.tap) { + if (priv->props.tap) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvtap device does not match macvlan profile"); + } else { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvlan device does not match macvtap profile"); + } + return FALSE; + } + + /* Before the device is realized some properties will not be set */ + if (nm_device_is_real(device)) { + if (setting_mode_to_platform(nm_setting_macvlan_get_mode(s_macvlan)) != priv->props.mode) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvlan mode setting differs"); + return FALSE; + } + + if (nm_setting_macvlan_get_promiscuous(s_macvlan) == priv->props.no_promisc) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvlan promiscuous setting differs"); + return FALSE; + } + + /* Check parent interface; could be an interface name or a UUID */ + parent = nm_setting_macvlan_get_parent(s_macvlan); + if (parent) { + if (!nm_device_match_parent(device, parent)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvlan parent setting differs"); + return FALSE; + } + } else { + /* Parent could be a MAC address in an NMSettingWired */ + if (!nm_device_match_parent_hwaddr(device, connection, TRUE)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "macvlan parent mac setting differs"); + return FALSE; + } + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingMacvlan *s_macvlan; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_MACVLAN_SETTING_NAME, + existing_connections, + NULL, + _("MACVLAN connection"), + NULL, + NULL, + TRUE); + + s_macvlan = nm_connection_get_setting_macvlan(connection); + if (!s_macvlan) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'macvlan' setting is required."); + return FALSE; + } + + /* If there's no MACVLAN interface, no parent, and no hardware address in the + * settings, then there's not enough information to complete the setting. + */ + if (!nm_setting_macvlan_get_parent(s_macvlan) + && !nm_device_match_parent_hwaddr(device, connection, TRUE)) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "The 'macvlan' setting had no interface name, parent, or hardware address."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(device); + NMSettingMacvlan * s_macvlan = nm_connection_get_setting_macvlan(connection); + int new_mode; + + if (!s_macvlan) { + s_macvlan = (NMSettingMacvlan *) nm_setting_macvlan_new(); + nm_connection_add_setting(connection, (NMSetting *) s_macvlan); + } + + new_mode = platform_mode_to_setting(priv->props.mode); + if (new_mode != nm_setting_macvlan_get_mode(s_macvlan)) + g_object_set(s_macvlan, NM_SETTING_MACVLAN_MODE, new_mode, NULL); + + if (priv->props.no_promisc == nm_setting_macvlan_get_promiscuous(s_macvlan)) + g_object_set(s_macvlan, NM_SETTING_MACVLAN_PROMISCUOUS, !priv->props.no_promisc, NULL); + + if (priv->props.tap != nm_setting_macvlan_get_tap(s_macvlan)) + g_object_set(s_macvlan, NM_SETTING_MACVLAN_TAP, !!priv->props.tap, NULL); + + g_object_set( + s_macvlan, + NM_SETTING_MACVLAN_PARENT, + nm_device_parent_find_for_connection(device, nm_setting_macvlan_get_parent(s_macvlan)), + NULL); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_MODE: + g_value_set_string(value, platform_mode_to_string(priv->props.mode)); + break; + case PROP_NO_PROMISC: + g_value_set_boolean(value, priv->props.no_promisc); + break; + case PROP_TAP: + g_value_set_boolean(value, priv->props.tap); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_TAP: + priv->props.tap = g_value_get_boolean(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + } +} + +/*****************************************************************************/ + +static void +nm_device_macvlan_init(NMDeviceMacvlan *self) +{} + +#if NM_MORE_ASSERTS +static void +dispose(GObject *object) +{ + G_OBJECT_CLASS(nm_device_macvlan_parent_class)->dispose(object); + + nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_state_id == 0); + nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_mtu_id == 0); +} +#endif + +static const NMDBusInterfaceInfoExtended interface_info_device_macvlan = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_MACVLAN, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "s", NM_DEVICE_MACVLAN_MODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NoPromisc", + "b", + NM_DEVICE_MACVLAN_NO_PROMISC), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tab", + "b", + NM_DEVICE_MACVLAN_TAP), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_macvlan_class_init(NMDeviceMacvlanClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + +#if NM_MORE_ASSERTS + object_class->dispose = dispose; +#endif + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_macvlan); + + device_class->connection_type_supported = NM_SETTING_MACVLAN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_MACVLAN_SETTING_NAME; + device_class->link_types = + NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_MACVLAN, NM_LINK_TYPE_MACVTAP); + device_class->mtu_parent_delta = 0; + + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->get_configured_mtu = nm_device_get_configured_mtu_wired_parent; + device_class->is_available = is_available; + device_class->link_changed = link_changed; + device_class->parent_changed_notify = parent_changed_notify; + device_class->update_connection = update_connection; + + obj_properties[PROP_MODE] = g_param_spec_string(NM_DEVICE_MACVLAN_MODE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_NO_PROMISC] = + g_param_spec_boolean(NM_DEVICE_MACVLAN_NO_PROMISC, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TAP] = + g_param_spec_boolean(NM_DEVICE_MACVLAN_TAP, + "", + "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_MACVLAN_DEVICE_FACTORY (nm_macvlan_device_factory_get_type()) +#define NM_MACVLAN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACVLAN_DEVICE_FACTORY, NMMacvlanDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + NMSettingMacvlan *s_macvlan; + NMLinkType link_type; + gboolean tap; + + if (connection) { + s_macvlan = nm_connection_get_setting_macvlan(connection); + g_assert(s_macvlan); + tap = nm_setting_macvlan_get_tap(s_macvlan); + } else { + g_assert(plink); + tap = plink->type == NM_LINK_TYPE_MACVTAP; + } + + link_type = tap ? NM_LINK_TYPE_MACVTAP : NM_LINK_TYPE_MACVLAN; + + return g_object_new(NM_TYPE_DEVICE_MACVLAN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Macvlan", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_MACVLAN, + NM_DEVICE_LINK_TYPE, + link_type, + NM_DEVICE_MACVLAN_TAP, + tap, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingMacvlan *s_macvlan; + NMSettingWired * s_wired; + const char * parent = NULL; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACVLAN_SETTING_NAME), NULL); + + s_macvlan = nm_connection_get_setting_macvlan(connection); + g_assert(s_macvlan); + + parent = nm_setting_macvlan_get_parent(s_macvlan); + if (parent) + return parent; + + /* Try the hardware address from the MACVLAN connection's hardware setting */ + s_wired = nm_connection_get_setting_wired(connection); + if (s_wired) + return nm_setting_wired_get_mac_address(s_wired); + + return NULL; +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + NMSettingMacvlan *s_macvlan; + const char * ifname; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACVLAN_SETTING_NAME), NULL); + + s_macvlan = nm_connection_get_setting_macvlan(connection); + g_assert(s_macvlan); + + if (!parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + return g_strdup(ifname); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + MACVLAN, + Macvlan, + macvlan, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_MACVLAN, NM_LINK_TYPE_MACVTAP) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_MACVLAN_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-macvlan.h b/src/core/devices/nm-device-macvlan.h new file mode 100644 index 00000000..109a2bcd --- /dev/null +++ b/src/core/devices/nm-device-macvlan.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_MACVLAN_H__ +#define __NETWORKMANAGER_DEVICE_MACVLAN_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_MACVLAN (nm_device_macvlan_get_type()) +#define NM_DEVICE_MACVLAN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlan)) +#define NM_DEVICE_MACVLAN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlanClass)) +#define NM_IS_DEVICE_MACVLAN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACVLAN)) +#define NM_IS_DEVICE_MACVLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MACVLAN)) +#define NM_DEVICE_MACVLAN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlanClass)) + +#define NM_DEVICE_MACVLAN_MODE "mode" +#define NM_DEVICE_MACVLAN_NO_PROMISC "no-promisc" +#define NM_DEVICE_MACVLAN_TAP "tap" + +typedef struct _NMDeviceMacvlan NMDeviceMacvlan; +typedef struct _NMDeviceMacvlanClass NMDeviceMacvlanClass; + +GType nm_device_macvlan_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_MACVLAN_H__ */ diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c new file mode 100644 index 00000000..4040f2d3 --- /dev/null +++ b/src/core/devices/nm-device-ppp.c @@ -0,0 +1,375 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ppp.h" + +#include "nm-ip4-config.h" +#include "nm-act-request.h" +#include "nm-device-factory.h" +#include "nm-device-private.h" +#include "nm-manager.h" +#include "nm-setting-pppoe.h" +#include "platform/nm-platform.h" +#include "ppp/nm-ppp-manager.h" +#include "ppp/nm-ppp-manager-call.h" +#include "ppp/nm-ppp-status.h" + +#define _NMLOG_DEVICE_TYPE NMDevicePpp +#include "nm-device-logging.h" + +/*****************************************************************************/ + +typedef struct _NMDevicePppPrivate { + NMPPPManager *ppp_manager; + NMIP4Config * ip4_config; +} NMDevicePppPrivate; + +struct _NMDevicePpp { + NMDevice parent; + NMDevicePppPrivate _priv; +}; + +struct _NMDevicePppClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDevicePpp, nm_device_ppp, NM_TYPE_DEVICE) + +#define NM_DEVICE_PPP_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDevicePpp, NM_IS_DEVICE_PPP, NMDevice) + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + switch (status) { + case NM_PPP_STATUS_DISCONNECT: + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_PPP_DISCONNECT); + break; + case NM_PPP_STATUS_DEAD: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED); + break; + default: + break; + } +} + +static void +ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDevicePpp * self = NM_DEVICE_PPP(device); + gs_free char *old_name = NULL; + gs_free_error GError *error = NULL; + + if (!nm_device_take_over_link(device, ifindex, &old_name, &error)) { + _LOGW(LOGD_DEVICE | LOGD_PPP, + "could not take control of link %d: %s", + ifindex, + error->message); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return; + } + + if (old_name) + nm_manager_remove_device(NM_MANAGER_GET, old_name, NM_DEVICE_TYPE_PPP); + + nm_device_activate_schedule_stage3_ip_config_start(device); +} + +static void +ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDevicePpp * self = NM_DEVICE_PPP(device); + NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self); + + _LOGT(LOGD_DEVICE | LOGD_PPP, "received IPv4 config from pppd"); + + if (nm_device_get_state(device) == NM_DEVICE_STATE_IP_CONFIG) { + if (nm_device_activate_ip4_state_in_conf(device)) { + nm_device_activate_schedule_ip_config_result(device, + AF_INET, + NM_IP_CONFIG_CAST(config)); + return; + } + } else { + if (priv->ip4_config) + g_object_unref(priv->ip4_config); + priv->ip4_config = g_object_ref(config); + } +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingPppoe *s_pppoe; + + if (!NM_DEVICE_CLASS(nm_device_ppp_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_pppoe = nm_connection_get_setting_pppoe(connection); + if (!s_pppoe || !nm_setting_pppoe_get_parent(s_pppoe)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "the connection doesn't specify a PPPoE parent interface"); + return FALSE; + } + + return TRUE; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDevicePpp * self = NM_DEVICE_PPP(device); + NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self); + NMSettingPppoe * s_pppoe; + NMActRequest * req; + GError * error = NULL; + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + s_pppoe = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE); + g_return_val_if_fail(s_pppoe, NM_ACT_STAGE_RETURN_FAILURE); + + g_clear_object(&priv->ip4_config); + + priv->ppp_manager = nm_ppp_manager_create(nm_setting_pppoe_get_parent(s_pppoe), &error); + + if (priv->ppp_manager) { + nm_ppp_manager_set_route_parameters(priv->ppp_manager, + nm_device_get_route_table(device, AF_INET), + nm_device_get_route_metric(device, AF_INET), + nm_device_get_route_table(device, AF_INET6), + nm_device_get_route_metric(device, AF_INET6)); + } + + if (!priv->ppp_manager + || !nm_ppp_manager_start(priv->ppp_manager, + req, + nm_setting_pppoe_get_username(s_pppoe), + 30, + 0, + &error)) { + _LOGW(LOGD_DEVICE | LOGD_PPP, "PPPoE failed to start: %s", error->message); + g_error_free(error); + + g_clear_object(&priv->ppp_manager); + + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_STATE_CHANGED, + G_CALLBACK(ppp_state_changed), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IFINDEX_SET, + G_CALLBACK(ppp_ifindex_set), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IP4_CONFIG, + G_CALLBACK(ppp_ip4_config), + self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + if (addr_family == AF_INET) { + NMDevicePpp * self = NM_DEVICE_PPP(device); + NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self); + + if (priv->ip4_config) { + if (out_config) + *out_config = g_steal_pointer(&priv->ip4_config); + else + g_clear_object(&priv->ip4_config); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + /* Wait IPCP termination */ + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + return NM_DEVICE_CLASS(nm_device_ppp_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + int parent_ifindex; + + if (!parent) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "PPP devices can not be created without a parent interface"); + return FALSE; + } + + parent_ifindex = nm_device_get_ifindex(parent); + g_warn_if_fail(parent_ifindex > 0); + + nm_device_parent_set_ifindex(device, parent_ifindex); + + /* The interface is created later */ + + return TRUE; +} + +static void +deactivate(NMDevice *device) +{ + NMDevicePpp * self = NM_DEVICE_PPP(device); + NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self); + + if (priv->ppp_manager) { + nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL); + g_clear_object(&priv->ppp_manager); + } +} + +static void +nm_device_ppp_init(NMDevicePpp *self) +{} + +static void +dispose(GObject *object) +{ + NMDevicePpp * self = NM_DEVICE_PPP(object); + NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self); + + g_clear_object(&priv->ip4_config); + + G_OBJECT_CLASS(nm_device_ppp_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_ppp = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_PPP, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ppp_class_init(NMDevicePppClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->dispose = dispose; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ppp); + + device_class->connection_type_supported = NM_SETTING_PPPOE_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_PPPOE_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_PPP); + + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->check_connection_compatible = check_connection_compatible; + device_class->create_and_realize = create_and_realize; + device_class->deactivate = deactivate; + device_class->get_generic_capabilities = get_generic_capabilities; +} + +/*****************************************************************************/ + +#define NM_TYPE_PPP_DEVICE_FACTORY (nm_ppp_device_factory_get_type()) +#define NM_PPP_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_DEVICE_FACTORY, NMPppDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_PPP, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Ppp", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_PPP, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_PPP, + NULL); +} + +static gboolean +match_connection(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingPppoe *s_pppoe; + + s_pppoe = nm_connection_get_setting_pppoe(connection); + nm_assert(s_pppoe); + + return !!nm_setting_pppoe_get_parent(s_pppoe); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingPppoe *s_pppoe; + + nm_assert(nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME)); + + s_pppoe = nm_connection_get_setting_pppoe(connection); + nm_assert(s_pppoe); + + return nm_setting_pppoe_get_parent(s_pppoe); +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + nm_assert(nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME)); + + if (!parent_iface) + return NULL; + + return g_strdup(nm_connection_get_interface_name(connection)); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + PPP, + Ppp, + ppp, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_PPP) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_PPPOE_SETTING_NAME), + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface; + factory_class->create_device = create_device; + factory_class->match_connection = match_connection;); diff --git a/src/core/devices/nm-device-ppp.h b/src/core/devices/nm-device-ppp.h new file mode 100644 index 00000000..24c119ab --- /dev/null +++ b/src/core/devices/nm-device-ppp.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_PPP_H__ +#define __NETWORKMANAGER_DEVICE_PPP_H__ + +#define NM_TYPE_DEVICE_PPP (nm_device_ppp_get_type()) +#define NM_DEVICE_PPP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_PPP, NMDevicePpp)) +#define NM_DEVICE_PPP_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_PPP, NMDevicePppClass)) +#define NM_IS_DEVICE_PPP(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_PPP)) +#define NM_IS_DEVICE_PPP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_PPP)) +#define NM_DEVICE_PPP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_PPP, NMDevicePppClass)) + +typedef struct _NMDevicePpp NMDevicePpp; +typedef struct _NMDevicePppClass NMDevicePppClass; + +GType nm_device_ppp_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_PPP_H__ */ diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h new file mode 100644 index 00000000..8675a699 --- /dev/null +++ b/src/core/devices/nm-device-private.h @@ -0,0 +1,213 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2007 - 2008 Novell, Inc. + * Copyright (C) 2007 - 2011 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_PRIVATE_H__ +#define __NETWORKMANAGER_DEVICE_PRIVATE_H__ + +#include "nm-device.h" + +/* This file should only be used by subclasses of NMDevice */ + +typedef enum { + NM_DEVICE_STAGE_STATE_INIT = 0, + NM_DEVICE_STAGE_STATE_PENDING = 1, + NM_DEVICE_STAGE_STATE_COMPLETED = 2, +} NMDeviceStageState; + +typedef enum { + NM_DEVICE_IP_STATE_NONE, + NM_DEVICE_IP_STATE_WAIT, + NM_DEVICE_IP_STATE_CONF, + NM_DEVICE_IP_STATE_DONE, + NM_DEVICE_IP_STATE_FAIL, +} NMDeviceIPState; + +enum NMActStageReturn { + NM_ACT_STAGE_RETURN_FAILURE = 0, /* Hard failure of activation */ + NM_ACT_STAGE_RETURN_SUCCESS, /* Activation stage done */ + NM_ACT_STAGE_RETURN_POSTPONE, /* Long-running operation in progress */ + NM_ACT_STAGE_RETURN_IP_WAIT, /* IP config stage is waiting (state IP_WAIT) */ + NM_ACT_STAGE_RETURN_IP_DONE, /* IP config stage is done (state IP_DONE), + * For the ip-config stage, this is similar to + * NM_ACT_STAGE_RETURN_SUCCESS, except that no + * IP config should be committed. */ + NM_ACT_STAGE_RETURN_IP_FAIL, /* IP config stage failed (state IP_FAIL), activation may proceed */ +}; + +#define NM_DEVICE_CAP_NONSTANDARD_CARRIER 0x80000000 +#define NM_DEVICE_CAP_IS_NON_KERNEL 0x40000000 + +#define NM_DEVICE_CAP_INTERNAL_MASK 0xc0000000 + +void nm_device_arp_announce(NMDevice *self); + +NMSettings *nm_device_get_settings(NMDevice *self); + +NMManager *nm_device_get_manager(NMDevice *self); + +gboolean nm_device_set_ip_ifindex(NMDevice *self, int ifindex); + +gboolean nm_device_set_ip_iface(NMDevice *self, const char *iface); + +void nm_device_activate_schedule_stage3_ip_config_start(NMDevice *device); + +gboolean nm_device_activate_stage3_ip_start(NMDevice *self, int addr_family); + +gboolean nm_device_bring_up(NMDevice *self, gboolean wait, gboolean *no_firmware); + +void nm_device_take_down(NMDevice *self, gboolean block); + +gboolean nm_device_take_over_link(NMDevice *self, int ifindex, char **old_name, GError **error); + +gboolean nm_device_hw_addr_set(NMDevice * device, + const char *addr, + const char *detail, + gboolean set_permanent); +gboolean nm_device_hw_addr_set_cloned(NMDevice *device, NMConnection *connection, gboolean is_wifi); +gboolean nm_device_hw_addr_reset(NMDevice *device, const char *detail); + +void nm_device_set_firmware_missing(NMDevice *self, gboolean missing); + +void nm_device_activate_schedule_stage1_device_prepare(NMDevice *device, gboolean do_sync); +void nm_device_activate_schedule_stage2_device_config(NMDevice *device, gboolean do_sync); + +void +nm_device_activate_schedule_ip_config_result(NMDevice *device, int addr_family, NMIPConfig *config); + +void nm_device_activate_schedule_ip_config_timeout(NMDevice *device, int addr_family); + +NMDeviceIPState nm_device_activate_get_ip_state(NMDevice *self, int addr_family); + +static inline gboolean +nm_device_activate_ip4_state_in_conf(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_CONF; +} + +static inline gboolean +nm_device_activate_ip4_state_in_wait(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_WAIT; +} + +static inline gboolean +nm_device_activate_ip4_state_done(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_DONE; +} + +static inline gboolean +nm_device_activate_ip6_state_in_conf(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_CONF; +} + +static inline gboolean +nm_device_activate_ip6_state_in_wait(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_WAIT; +} + +static inline gboolean +nm_device_activate_ip6_state_done(NMDevice *self) +{ + return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_DONE; +} + +void nm_device_set_dhcp_anycast_address(NMDevice *device, const char *addr); + +gboolean nm_device_dhcp4_renew(NMDevice *device, gboolean release); +gboolean nm_device_dhcp6_renew(NMDevice *device, gboolean release); + +void nm_device_recheck_available_connections(NMDevice *device); + +void +nm_device_master_check_slave_physical_port(NMDevice *self, NMDevice *slave, NMLogDomain log_domain); + +void nm_device_master_release_slaves(NMDevice *self); + +void nm_device_set_carrier(NMDevice *self, gboolean carrier); + +void nm_device_queue_recheck_assume(NMDevice *device); +void nm_device_queue_recheck_available(NMDevice * device, + NMDeviceStateReason available_reason, + NMDeviceStateReason unavailable_reason); + +void nm_device_set_dev2_ip_config(NMDevice *device, int addr_family, NMIPConfig *config); + +gboolean nm_device_hw_addr_is_explict(NMDevice *device); + +void nm_device_ip_method_failed(NMDevice *self, int addr_family, NMDeviceStateReason reason); + +gboolean nm_device_sysctl_ip_conf_set(NMDevice * self, + int addr_family, + const char *property, + const char *value); + +NMIP4Config *nm_device_ip4_config_new(NMDevice *self); + +NMIP6Config *nm_device_ip6_config_new(NMDevice *self); + +NMIPConfig *nm_device_ip_config_new(NMDevice *self, int addr_family); + +NML3ConfigData *nm_device_create_l3_config_data(NMDevice *self); + +/*****************************************************************************/ + +gint64 nm_device_get_configured_mtu_from_connection_default(NMDevice * self, + const char *property_name, + guint32 max_mtu); + +guint32 nm_device_get_configured_mtu_from_connection(NMDevice * device, + GType setting_type, + NMDeviceMtuSource *out_source); + +guint32 nm_device_get_configured_mtu_for_wired(NMDevice * self, + NMDeviceMtuSource *out_source, + gboolean * out_force); + +guint32 nm_device_get_configured_mtu_wired_parent(NMDevice * self, + NMDeviceMtuSource *out_source, + gboolean * out_force); + +void nm_device_commit_mtu(NMDevice *self); + +/*****************************************************************************/ + +#define NM_DEVICE_DEFINE_LINK_TYPES(...) \ + ((NM_NARG(__VA_ARGS__) == 0) ? NULL : ({ \ + static const NMLinkType _types[NM_NARG(__VA_ARGS__) + 1] = { \ + __VA_ARGS__ _NM_MACRO_COMMA_IF_ARGS(__VA_ARGS__) NM_LINK_TYPE_NONE, \ + }; \ + \ + nm_assert(_types[NM_NARG(__VA_ARGS__)] == NM_LINK_TYPE_NONE); \ + _types; \ + })) + +gboolean _nm_device_hash_check_invalid_keys(GHashTable * hash, + const char * setting_name, + GError ** error, + const char *const *whitelist); +#define nm_device_hash_check_invalid_keys(hash, setting_name, error, ...) \ + _nm_device_hash_check_invalid_keys(hash, setting_name, error, NM_MAKE_STRV(__VA_ARGS__)) + +gboolean nm_device_match_parent(NMDevice *device, const char *parent); +gboolean nm_device_match_parent_hwaddr(NMDevice * device, + NMConnection *connection, + gboolean fail_if_no_hwaddr); + +/*****************************************************************************/ + +void nm_device_auth_request(NMDevice * self, + GDBusMethodInvocation * context, + NMConnection * connection, + const char * permission, + gboolean allow_interaction, + GCancellable * cancellable, + NMManagerDeviceAuthRequestFunc callback, + gpointer user_data); + +#endif /* NM_DEVICE_PRIVATE_H */ diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c new file mode 100644 index 00000000..edca69e9 --- /dev/null +++ b/src/core/devices/nm-device-tun.c @@ -0,0 +1,571 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-tun.h" + +#include <stdlib.h> +#include <sys/types.h> +#include <linux/if_tun.h> + +#include "nm-act-request.h" +#include "nm-device-private.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-tun.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceTun +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceTun, + PROP_OWNER, + PROP_GROUP, + PROP_MODE, + PROP_NO_PI, + PROP_VNET_HDR, + PROP_MULTI_QUEUE, ); + +typedef struct { + NMPlatformLnkTun props; +} NMDeviceTunPrivate; + +struct _NMDeviceTun { + NMDevice parent; + NMDeviceTunPrivate _priv; +}; + +struct _NMDeviceTunClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceTun, nm_device_tun, NM_TYPE_DEVICE) + +#define NM_DEVICE_TUN_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceTun, NM_IS_DEVICE_TUN, NMDevice) + +/*****************************************************************************/ + +static void +update_properties_from_struct(NMDeviceTun *self, const NMPlatformLnkTun *props) +{ + NMDeviceTunPrivate * priv = NM_DEVICE_TUN_GET_PRIVATE(self); + const NMPlatformLnkTun props0 = {}; + + if (!props) { + /* allow passing %NULL to reset all properties. */ + props = &props0; + } + + g_object_freeze_notify(G_OBJECT(self)); + +#define CHECK_PROPERTY_CHANGED_VALID(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field \ + || priv->props.field##_valid != props->field##_valid) { \ + priv->props.field##_valid = props->field##_valid; \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED_VALID(owner, PROP_OWNER); + CHECK_PROPERTY_CHANGED_VALID(group, PROP_GROUP); + CHECK_PROPERTY_CHANGED(type, PROP_MODE); + CHECK_PROPERTY_CHANGED(pi, PROP_NO_PI); + CHECK_PROPERTY_CHANGED(vnet_hdr, PROP_VNET_HDR); + CHECK_PROPERTY_CHANGED(multi_queue, PROP_MULTI_QUEUE); + + g_object_thaw_notify(G_OBJECT(self)); +} + +static void +update_properties(NMDeviceTun *self) +{ + NMPlatformLnkTun props_storage; + const NMPlatformLnkTun *props = NULL; + int ifindex; + + ifindex = nm_device_get_ifindex(NM_DEVICE(self)); + if (ifindex > 0 + && nm_platform_link_tun_get_properties(nm_device_get_platform(NM_DEVICE(self)), + ifindex, + &props_storage)) + props = &props_storage; + + update_properties_from_struct(self, props); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_tun_parent_class)->link_changed(device, pllink); + update_properties(NM_DEVICE_TUN(device)); +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingTun *s_tun; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_TUN_SETTING_NAME, + existing_connections, + NULL, + _("TUN connection"), + NULL, + NULL, + TRUE); + + s_tun = nm_connection_get_setting_tun(connection); + if (!s_tun) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'tun' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceTun * self = NM_DEVICE_TUN(device); + NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self); + NMSettingTun * s_tun; + NMSettingTunMode mode; + char s_buf[100]; + const char * str; + + /* Note: since we read tun properties from sysctl for older kernels, + * we don't get proper change notifications. Make sure that all our + * tun properties are up to date at this point. We should not do this, + * if we would entirely rely on netlink events. */ + update_properties(NM_DEVICE_TUN(device)); + + switch (priv->props.type) { + case IFF_TUN: + mode = NM_SETTING_TUN_MODE_TUN; + break; + case IFF_TAP: + mode = NM_SETTING_TUN_MODE_TAP; + break; + default: + /* Huh? */ + return; + } + + s_tun = nm_connection_get_setting_tun(connection); + if (!s_tun) { + s_tun = (NMSettingTun *) nm_setting_tun_new(); + nm_connection_add_setting(connection, (NMSetting *) s_tun); + } + + if (mode != nm_setting_tun_get_mode(s_tun)) + g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_MODE, (guint) mode, NULL); + + str = priv->props.owner_valid ? nm_sprintf_buf(s_buf, "%" G_GINT32_FORMAT, priv->props.owner) + : NULL; + if (!nm_streq0(str, nm_setting_tun_get_owner(s_tun))) + g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_OWNER, str, NULL); + + str = priv->props.group_valid ? nm_sprintf_buf(s_buf, "%" G_GINT32_FORMAT, priv->props.group) + : NULL; + if (!nm_streq0(str, nm_setting_tun_get_group(s_tun))) + g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_GROUP, str, NULL); + + if (priv->props.pi != nm_setting_tun_get_pi(s_tun)) + g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_PI, (gboolean) priv->props.pi, NULL); + if (priv->props.vnet_hdr != nm_setting_tun_get_vnet_hdr(s_tun)) + g_object_set(G_OBJECT(s_tun), + NM_SETTING_TUN_VNET_HDR, + (gboolean) priv->props.vnet_hdr, + NULL); + if (priv->props.multi_queue != nm_setting_tun_get_multi_queue(s_tun)) + g_object_set(G_OBJECT(s_tun), + NM_SETTING_TUN_MULTI_QUEUE, + (gboolean) priv->props.multi_queue, + NULL); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMPlatformLnkTun props = {}; + NMSettingTun * s_tun; + gint64 owner; + gint64 group; + int r; + + s_tun = nm_connection_get_setting_tun(connection); + g_return_val_if_fail(s_tun, FALSE); + + switch (nm_setting_tun_get_mode(s_tun)) { + case NM_SETTING_TUN_MODE_TAP: + props.type = IFF_TAP; + break; + case NM_SETTING_TUN_MODE_TUN: + props.type = IFF_TUN; + break; + default: + g_return_val_if_reached(FALSE); + } + + owner = _nm_utils_ascii_str_to_int64(nm_setting_tun_get_owner(s_tun), 10, 0, G_MAXINT32, -1); + if (owner != -1) { + props.owner_valid = TRUE; + props.owner = owner; + } + group = _nm_utils_ascii_str_to_int64(nm_setting_tun_get_group(s_tun), 10, 0, G_MAXINT32, -1); + if (group != -1) { + props.group_valid = TRUE; + props.group = group; + } + + props.pi = nm_setting_tun_get_pi(s_tun); + props.vnet_hdr = nm_setting_tun_get_vnet_hdr(s_tun); + props.multi_queue = nm_setting_tun_get_multi_queue(s_tun); + props.persist = TRUE; + + r = nm_platform_link_tun_add(nm_device_get_platform(device), iface, &props, out_plink, NULL); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create TUN/TAP interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +static gboolean +_same_og(const char *str, gboolean og_valid, guint32 og_num) +{ + gint64 v; + + v = _nm_utils_ascii_str_to_int64(str, 10, 0, G_MAXINT32, -1); + return (!og_valid && (v == (gint64) -1)) || (og_valid && (((guint32) v) == og_num)); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceTun * self = NM_DEVICE_TUN(device); + NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self); + NMSettingTunMode mode; + NMSettingTun * s_tun; + + if (!NM_DEVICE_CLASS(nm_device_tun_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_device_is_real(device)) { + switch (priv->props.type) { + case IFF_TUN: + mode = NM_SETTING_TUN_MODE_TUN; + break; + case IFF_TAP: + mode = NM_SETTING_TUN_MODE_TAP; + break; + default: + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "invalid tun type on device"); + return FALSE; + } + + s_tun = nm_connection_get_setting_tun(connection); + + if (mode != nm_setting_tun_get_mode(s_tun)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun mode setting mismatches"); + return FALSE; + } + if (!_same_og(nm_setting_tun_get_owner(s_tun), + priv->props.owner_valid, + priv->props.owner)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun owner setting mismatches"); + return FALSE; + } + if (!_same_og(nm_setting_tun_get_group(s_tun), + priv->props.group_valid, + priv->props.group)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun group setting mismatches"); + return FALSE; + } + if (nm_setting_tun_get_pi(s_tun) != priv->props.pi) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun pi setting mismatches"); + return FALSE; + } + if (nm_setting_tun_get_vnet_hdr(s_tun) != priv->props.vnet_hdr) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun vnet-hdr setting mismatches"); + return FALSE; + } + if (nm_setting_tun_get_multi_queue(s_tun) != priv->props.multi_queue) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "tun multi-queue setting mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceTun * self = NM_DEVICE_TUN(device); + NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self); + + if (priv->props.type == IFF_TUN) { + /* Nothing to do for TUN devices */ + } else { + if (!nm_device_hw_addr_set_cloned(device, + nm_device_get_applied_connection(device), + FALSE)) { + *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +unrealize_notify(NMDevice *device) +{ + NM_DEVICE_CLASS(nm_device_tun_parent_class)->unrealize_notify(device); + update_properties_from_struct(NM_DEVICE_TUN(device), NULL); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceTun * self = NM_DEVICE_TUN(object); + NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self); + const char * s; + + switch (prop_id) { + case PROP_OWNER: + g_value_set_int64(value, + priv->props.owner_valid ? (gint64) priv->props.owner : (gint64) -1); + break; + case PROP_GROUP: + g_value_set_int64(value, + priv->props.group_valid ? (gint64) priv->props.group : (gint64) -1); + break; + case PROP_MODE: + switch (priv->props.type) { + case IFF_TUN: + s = "tun"; + break; + case IFF_TAP: + s = "tap"; + break; + default: + s = NULL; + break; + } + g_value_set_static_string(value, s); + break; + case PROP_NO_PI: + g_value_set_boolean(value, !priv->props.pi); + break; + case PROP_VNET_HDR: + g_value_set_boolean(value, priv->props.vnet_hdr); + break; + case PROP_MULTI_QUEUE: + g_value_set_boolean(value, priv->props.multi_queue); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_tun_init(NMDeviceTun *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_tun = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_TUN, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Owner", "x", NM_DEVICE_TUN_OWNER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Group", "x", NM_DEVICE_TUN_GROUP), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "s", NM_DEVICE_TUN_MODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NoPi", "b", NM_DEVICE_TUN_NO_PI), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("VnetHdr", + "b", + NM_DEVICE_TUN_VNET_HDR), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("MultiQueue", + "b", + NM_DEVICE_TUN_MULTI_QUEUE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_tun_class_init(NMDeviceTunClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_tun); + + device_class->connection_type_supported = NM_SETTING_TUN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_TUN_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_TUN); + + device_class->link_changed = link_changed; + device_class->complete_connection = complete_connection; + device_class->check_connection_compatible = check_connection_compatible; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->unrealize_notify = unrealize_notify; + device_class->update_connection = update_connection; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + + obj_properties[PROP_OWNER] = g_param_spec_int64(NM_DEVICE_TUN_OWNER, + "", + "", + -1, + G_MAXUINT32, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_GROUP] = g_param_spec_int64(NM_DEVICE_TUN_GROUP, + "", + "", + -1, + G_MAXUINT32, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MODE] = g_param_spec_string(NM_DEVICE_TUN_MODE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_NO_PI] = g_param_spec_boolean(NM_DEVICE_TUN_NO_PI, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_VNET_HDR] = g_param_spec_boolean(NM_DEVICE_TUN_VNET_HDR, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MULTI_QUEUE] = + g_param_spec_boolean(NM_DEVICE_TUN_MULTI_QUEUE, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_TUN_DEVICE_FACTORY (nm_tun_device_factory_get_type()) +#define NM_TUN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TUN_DEVICE_FACTORY, NMTunDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + g_return_val_if_fail(!plink || plink->type == NM_LINK_TYPE_TUN, NULL); + g_return_val_if_fail(!connection + || nm_streq0(nm_connection_get_connection_type(connection), + NM_SETTING_TUN_SETTING_NAME), + NULL); + + return g_object_new(NM_TYPE_DEVICE_TUN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Tun", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_TUN, + NM_DEVICE_LINK_TYPE, + (guint) NM_LINK_TYPE_TUN, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + TUN, + Tun, + tun, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_TUN) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_TUN_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-tun.h b/src/core/devices/nm-device-tun.h new file mode 100644 index 00000000..84497ea0 --- /dev/null +++ b/src/core/devices/nm-device-tun.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_TUN_H__ +#define __NETWORKMANAGER_DEVICE_TUN_H__ + +#include "nm-device-generic.h" + +#define NM_TYPE_DEVICE_TUN (nm_device_tun_get_type()) +#define NM_DEVICE_TUN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TUN, NMDeviceTun)) +#define NM_DEVICE_TUN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TUN, NMDeviceTunClass)) +#define NM_IS_DEVICE_TUN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TUN)) +#define NM_IS_DEVICE_TUN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_TUN)) +#define NM_DEVICE_TUN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_TUN, NMDeviceTunClass)) + +#define NM_DEVICE_TUN_OWNER "owner" +#define NM_DEVICE_TUN_GROUP "group" +#define NM_DEVICE_TUN_MODE "mode" +#define NM_DEVICE_TUN_NO_PI "no-pi" +#define NM_DEVICE_TUN_VNET_HDR "vnet-hdr" +#define NM_DEVICE_TUN_MULTI_QUEUE "multi-queue" + +typedef struct _NMDeviceTun NMDeviceTun; +typedef struct _NMDeviceTunClass NMDeviceTunClass; + +GType nm_device_tun_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_TUN_H__ */ diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c new file mode 100644 index 00000000..e0ba843d --- /dev/null +++ b/src/core/devices/nm-device-veth.c @@ -0,0 +1,232 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <stdlib.h> + +#include "nm-core-internal.h" +#include "nm-device-veth.h" +#include "nm-device-private.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-veth.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceVeth +#include "nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceVeth { + NMDeviceEthernet parent; +}; + +struct _NMDeviceVethClass { + NMDeviceEthernetClass parent; +}; + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVeth, PROP_PEER, ); + +/*****************************************************************************/ + +G_DEFINE_TYPE(NMDeviceVeth, nm_device_veth, NM_TYPE_DEVICE_ETHERNET) + +/*****************************************************************************/ + +static void +update_properties(NMDevice *device) +{ + NMDevice *peer; + int ifindex, peer_ifindex; + + ifindex = nm_device_get_ifindex(device); + + if (ifindex <= 0 + || !nm_platform_link_veth_get_properties(nm_device_get_platform(device), + ifindex, + &peer_ifindex)) + peer_ifindex = 0; + + nm_device_parent_set_ifindex(device, peer_ifindex); + + peer = nm_device_parent_get_device(device); + if (peer && NM_IS_DEVICE_VETH(peer) && nm_device_parent_get_ifindex(peer) <= 0) + update_properties(peer); +} + +static gboolean +can_unmanaged_external_down(NMDevice *self) +{ + /* Unless running in a container, an udev rule causes these to be + * unmanaged. If there's no udev then we're probably in a container + * and should IFF_UP and configure the veth ourselves even if we + * didn't create it. */ + return FALSE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_veth_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMSettingVeth *s_veth; + int r; + + s_veth = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VETH); + if (!s_veth) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Profile %s (%s) is not a suitable veth profile", + nm_connection_get_id(connection), + nm_connection_get_uuid(connection)); + return FALSE; + } + + r = nm_platform_link_veth_add(nm_device_get_platform(device), + iface, + nm_setting_veth_get_peer(s_veth), + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create veth interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + return TRUE; +} + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +/*****************************************************************************/ + +static void +nm_device_veth_init(NMDeviceVeth *self) +{} + +static void +parent_changed_notify(NMDevice *device, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + NM_DEVICE_CLASS(nm_device_veth_parent_class) + ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent); + _notify(NM_DEVICE_VETH(device), PROP_PEER); +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceVeth *self = NM_DEVICE_VETH(object); + NMDevice * peer; + + switch (prop_id) { + case PROP_PEER: + peer = nm_device_parent_get_device(NM_DEVICE(self)); + if (peer && !NM_IS_DEVICE_VETH(peer)) + peer = NULL; + nm_dbus_utils_g_value_set_object_path(value, peer); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static const NMDBusInterfaceInfoExtended interface_info_device_veth = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_VETH, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Peer", + "o", + NM_DEVICE_VETH_PEER), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_veth_class_init(NMDeviceVethClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_veth); + + device_class->connection_type_supported = NULL; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VETH); + + device_class->can_unmanaged_external_down = can_unmanaged_external_down; + device_class->link_changed = link_changed; + device_class->parent_changed_notify = parent_changed_notify; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + + obj_properties[PROP_PEER] = g_param_spec_string(NM_DEVICE_VETH_PEER, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_VETH_DEVICE_FACTORY (nm_veth_device_factory_get_type()) +#define NM_VETH_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VETH_DEVICE_FACTORY, NMVethDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_VETH, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Veth", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_VETH, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_VETH, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + VETH, + Veth, + veth, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VETH) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VETH_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-veth.h b/src/core/devices/nm-device-veth.h new file mode 100644 index 00000000..d43a0a4b --- /dev/null +++ b/src/core/devices/nm-device-veth.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_VETH_H__ +#define __NETWORKMANAGER_DEVICE_VETH_H__ + +#include "nm-device-ethernet.h" + +#define NM_TYPE_DEVICE_VETH (nm_device_veth_get_type()) +#define NM_DEVICE_VETH(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VETH, NMDeviceVeth)) +#define NM_DEVICE_VETH_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VETH, NMDeviceVethClass)) +#define NM_IS_DEVICE_VETH(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VETH)) +#define NM_IS_DEVICE_VETH_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VETH)) +#define NM_DEVICE_VETH_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VETH, NMDeviceVethClass)) + +#define NM_DEVICE_VETH_PEER "peer" + +typedef struct _NMDeviceVeth NMDeviceVeth; +typedef struct _NMDeviceVethClass NMDeviceVethClass; + +GType nm_device_veth_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_VETH_H__ */ diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c new file mode 100644 index 00000000..bfde60ef --- /dev/null +++ b/src/core/devices/nm-device-vlan.c @@ -0,0 +1,688 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2012 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-vlan.h" + +#include <sys/socket.h> + +#include "nm-manager.h" +#include "nm-utils.h" +#include "NetworkManagerUtils.h" +#include "nm-device-private.h" +#include "settings/nm-settings.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-manager.h" +#include "nm-core-internal.h" +#include "platform/nmp-object.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceVlan +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVlan, PROP_VLAN_ID, ); + +typedef struct { + gulong parent_state_id; + gulong parent_hwaddr_id; + gulong parent_mtu_id; + guint vlan_id; +} NMDeviceVlanPrivate; + +struct _NMDeviceVlan { + NMDevice parent; + NMDeviceVlanPrivate _priv; +}; + +struct _NMDeviceVlanClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceVlan, nm_device_vlan, NM_TYPE_DEVICE) + +#define NM_DEVICE_VLAN_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceVlan, NM_IS_DEVICE_VLAN, NMDevice) + +/*****************************************************************************/ + +static void +parent_state_changed(NMDevice * parent, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason, + gpointer user_data) +{ + NMDeviceVlan *self = NM_DEVICE_VLAN(user_data); + + /* We'll react to our own carrier state notifications. Ignore the parent's. */ + if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER) + return; + + nm_device_set_unmanaged_by_flags(NM_DEVICE(self), + NM_UNMANAGED_PARENT, + !nm_device_get_managed(parent, FALSE), + reason); +} + +static void +parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data) +{ + /* the MTU of a VLAN device is limited by the parent's MTU. + * + * When the parent's MTU changes, try to re-set the MTU. */ + nm_device_commit_mtu(user_data); +} + +static void +parent_hwaddr_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceVlan * self = NM_DEVICE_VLAN(device); + NMConnection * connection; + const char * new_mac, *old_mac; + NMSettingIPConfig *s_ip6; + + /* Never touch assumed devices */ + if (nm_device_sys_iface_state_is_external_or_assume(device)) + return; + + connection = nm_device_get_applied_connection(device); + if (!connection) + return; + + /* Update the VLAN MAC only if configuration does not specify one */ + if (nm_device_hw_addr_is_explict(device)) + return; + + old_mac = nm_device_get_hw_address(device); + new_mac = nm_device_get_hw_address(parent); + if (nm_streq0(old_mac, new_mac)) + return; + + _LOGD(LOGD_VLAN, + "parent hardware address changed to %s%s%s", + NM_PRINT_FMT_QUOTE_STRING(new_mac)); + if (new_mac) { + nm_device_hw_addr_set(device, new_mac, "vlan-parent", TRUE); + nm_device_arp_announce(device); + /* When changing the hw address the interface is taken down, + * removing the IPv6 configuration; reapply it. + */ + s_ip6 = nm_connection_get_setting_ip6_config(connection); + if (s_ip6) + nm_device_reactivate_ip_config(device, AF_INET6, s_ip6, s_ip6); + } +} + +static void +parent_changed_notify(NMDevice *device, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + NMDeviceVlan * self = NM_DEVICE_VLAN(device); + NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(self); + + NM_DEVICE_CLASS(nm_device_vlan_parent_class) + ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent); + + /* note that @self doesn't have to clear @parent_state_id on dispose, + * because NMDevice's dispose() will unset the parent, which in turn calls + * parent_changed_notify(). */ + nm_clear_g_signal_handler(old_parent, &priv->parent_state_id); + nm_clear_g_signal_handler(old_parent, &priv->parent_hwaddr_id); + nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id); + + if (new_parent) { + priv->parent_state_id = g_signal_connect(new_parent, + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(parent_state_changed), + device); + + priv->parent_hwaddr_id = g_signal_connect(new_parent, + "notify::" NM_DEVICE_HW_ADDRESS, + G_CALLBACK(parent_hwaddr_maybe_changed), + device); + parent_hwaddr_maybe_changed(new_parent, NULL, self); + + priv->parent_mtu_id = g_signal_connect(new_parent, + "notify::" NM_DEVICE_MTU, + G_CALLBACK(parent_mtu_maybe_changed), + device); + parent_mtu_maybe_changed(new_parent, NULL, self); + + /* Set parent-dependent unmanaged flag */ + nm_device_set_unmanaged_by_flags(device, + NM_UNMANAGED_PARENT, + !nm_device_get_managed(new_parent, FALSE), + NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED); + } + + /* Recheck availability now that the parent has changed */ + if (new_ifindex > 0) { + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_PARENT_CHANGED, + NM_DEVICE_STATE_REASON_PARENT_CHANGED); + } +} + +static void +update_properties(NMDevice *device) +{ + NMDeviceVlanPrivate * priv; + const NMPlatformLink * plink = NULL; + const NMPlatformLnkVlan *plnk = NULL; + int ifindex; + int parent_ifindex = 0; + guint vlan_id; + + g_return_if_fail(NM_IS_DEVICE_VLAN(device)); + + priv = NM_DEVICE_VLAN_GET_PRIVATE(device); + + ifindex = nm_device_get_ifindex(device); + + if (ifindex > 0) + plnk = nm_platform_link_get_lnk_vlan(nm_device_get_platform(device), ifindex, &plink); + + if (plnk && plink->parent > 0) + parent_ifindex = plink->parent; + + g_object_freeze_notify((GObject *) device); + + nm_device_parent_set_ifindex(device, parent_ifindex); + + vlan_id = plnk ? plnk->id : 0; + if (vlan_id != priv->vlan_id) { + priv->vlan_id = vlan_id; + _notify((NMDeviceVlan *) device, PROP_VLAN_ID); + } + + g_object_thaw_notify((GObject *) device); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_vlan_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(device); + const char * iface = nm_device_get_iface(device); + NMSettingVlan * s_vlan; + int parent_ifindex; + guint vlan_id; + int r; + + s_vlan = nm_connection_get_setting_vlan(connection); + g_assert(s_vlan); + + if (!parent) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "VLAN devices can not be created without a parent interface"); + return FALSE; + } + + parent_ifindex = nm_device_get_ifindex(parent); + if (parent_ifindex <= 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_MISSING_DEPENDENCIES, + "cannot retrieve ifindex of interface %s (%s)", + nm_device_get_iface(parent), + nm_device_get_type_desc(parent)); + return FALSE; + } + + if (!nm_device_supports_vlans(parent)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "no support for VLANs on interface %s of type %s", + nm_device_get_iface(parent), + nm_device_get_type_desc(parent)); + return FALSE; + } + + vlan_id = nm_setting_vlan_get_id(s_vlan); + + r = nm_platform_link_vlan_add(nm_device_get_platform(device), + iface, + parent_ifindex, + vlan_id, + nm_setting_vlan_get_flags(s_vlan), + out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create VLAN interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + nm_device_parent_set_ifindex(device, parent_ifindex); + if (vlan_id != priv->vlan_id) { + priv->vlan_id = vlan_id; + _notify((NMDeviceVlan *) device, PROP_VLAN_ID); + } + + return TRUE; +} + +static void +unrealize_notify(NMDevice *device) +{ + NMDeviceVlan * self = NM_DEVICE_VLAN(device); + NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(self); + + NM_DEVICE_CLASS(nm_device_vlan_parent_class)->unrealize_notify(device); + + if (priv->vlan_id != 0) { + priv->vlan_id = 0; + _notify(self, PROP_VLAN_ID); + } +} + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + /* We assume VLAN interfaces always support carrier detect */ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +/*****************************************************************************/ + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + if (!nm_device_parent_get_device(device)) + return FALSE; + return NM_DEVICE_CLASS(nm_device_vlan_parent_class)->is_available(device, flags); +} + +/*****************************************************************************/ + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(device); + NMSettingVlan * s_vlan; + const char * parent; + + if (!NM_DEVICE_CLASS(nm_device_vlan_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_device_is_real(device)) { + s_vlan = nm_connection_get_setting_vlan(connection); + + if (nm_setting_vlan_get_id(s_vlan) != priv->vlan_id) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vlan id setting mismatches"); + return FALSE; + } + + /* Check parent interface; could be an interface name or a UUID */ + parent = nm_setting_vlan_get_parent(s_vlan); + if (parent) { + if (!nm_device_match_parent(device, parent)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vlan parent setting differs"); + return FALSE; + } + } else { + /* Parent could be a MAC address in an NMSettingWired */ + if (!nm_device_match_parent_hwaddr(device, connection, TRUE)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vlan parent mac setting differs"); + return FALSE; + } + } + } + + return TRUE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + if (!nm_device_is_real(device)) + return TRUE; + + return NM_DEVICE_CLASS(nm_device_vlan_parent_class) + ->check_connection_available(device, connection, flags, specific_object, error); +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingVlan *s_vlan; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_VLAN_SETTING_NAME, + existing_connections, + NULL, + _("VLAN connection"), + NULL, + NULL, + TRUE); + + s_vlan = nm_connection_get_setting_vlan(connection); + if (!s_vlan) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'vlan' setting is required."); + return FALSE; + } + + /* If there's no VLAN interface, no parent, and no hardware address in the + * settings, then there's not enough information to complete the setting. + */ + if (!nm_setting_vlan_get_parent(s_vlan) + && !nm_device_match_parent_hwaddr(device, connection, TRUE)) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "The 'vlan' setting had no interface name, parent, or hardware address."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceVlanPrivate * priv = NM_DEVICE_VLAN_GET_PRIVATE(device); + NMSettingVlan * s_vlan = nm_connection_get_setting_vlan(connection); + int ifindex = nm_device_get_ifindex(device); + const NMPlatformLink *plink; + const NMPObject * polnk; + guint vlan_id; + guint vlan_flags; + + if (!s_vlan) { + s_vlan = (NMSettingVlan *) nm_setting_vlan_new(); + nm_connection_add_setting(connection, (NMSetting *) s_vlan); + } + + polnk = nm_platform_link_get_lnk(nm_device_get_platform(device), + ifindex, + NM_LINK_TYPE_VLAN, + &plink); + + if (polnk) + vlan_id = polnk->lnk_vlan.id; + else + vlan_id = priv->vlan_id; + if (vlan_id != nm_setting_vlan_get_id(s_vlan)) + g_object_set(s_vlan, NM_SETTING_VLAN_ID, vlan_id, NULL); + + g_object_set(s_vlan, + NM_SETTING_VLAN_PARENT, + nm_device_parent_find_for_connection(device, nm_setting_vlan_get_parent(s_vlan)), + NULL); + + if (polnk) + vlan_flags = polnk->lnk_vlan.flags; + else + vlan_flags = NM_VLAN_FLAG_REORDER_HEADERS; + if (vlan_flags != nm_setting_vlan_get_flags(s_vlan)) + g_object_set(s_vlan, NM_SETTING_VLAN_FLAGS, (NMVlanFlags) vlan_flags, NULL); + + if (polnk) { + _nm_setting_vlan_set_priorities(s_vlan, + NM_VLAN_INGRESS_MAP, + polnk->_lnk_vlan.ingress_qos_map, + polnk->_lnk_vlan.n_ingress_qos_map); + _nm_setting_vlan_set_priorities(s_vlan, + NM_VLAN_EGRESS_MAP, + polnk->_lnk_vlan.egress_qos_map, + polnk->_lnk_vlan.n_egress_qos_map); + } else { + _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_INGRESS_MAP, NULL, 0); + _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_EGRESS_MAP, NULL, 0); + } +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDevice * parent_device; + NMSettingVlan *s_vlan; + + /* Change MAC address to parent's one if needed */ + parent_device = nm_device_parent_get_device(device); + if (parent_device) { + parent_hwaddr_maybe_changed(parent_device, NULL, device); + parent_mtu_maybe_changed(parent_device, NULL, device); + } + + s_vlan = nm_device_get_applied_setting(device, NM_TYPE_SETTING_VLAN); + if (s_vlan) { + gs_free NMVlanQosMapping *ingress_map = NULL; + gs_free NMVlanQosMapping *egress_map = NULL; + guint n_ingress_map = 0; + guint n_egress_map = 0; + + _nm_setting_vlan_get_priorities(s_vlan, NM_VLAN_INGRESS_MAP, &ingress_map, &n_ingress_map); + _nm_setting_vlan_get_priorities(s_vlan, NM_VLAN_EGRESS_MAP, &egress_map, &n_egress_map); + + nm_platform_link_vlan_change(nm_device_get_platform(device), + nm_device_get_ifindex(device), + NM_VLAN_FLAGS_ALL, + nm_setting_vlan_get_flags(s_vlan), + TRUE, + ingress_map, + n_ingress_map, + TRUE, + egress_map, + n_egress_map); + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_VLAN_ID: + g_value_set_uint(value, priv->vlan_id); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_vlan_init(NMDeviceVlan *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_vlan = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_VLAN, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("VlanId", + "u", + NM_DEVICE_VLAN_ID), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_vlan_class_init(NMDeviceVlanClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vlan); + + device_class->connection_type_supported = NM_SETTING_VLAN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_VLAN_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VLAN); + device_class->mtu_parent_delta = 0; /* VLANs can have the same MTU of parent */ + + device_class->create_and_realize = create_and_realize; + device_class->link_changed = link_changed; + device_class->unrealize_notify = unrealize_notify; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->get_configured_mtu = nm_device_get_configured_mtu_wired_parent; + device_class->is_available = is_available; + device_class->parent_changed_notify = parent_changed_notify; + + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + device_class->update_connection = update_connection; + + obj_properties[PROP_VLAN_ID] = g_param_spec_uint(NM_DEVICE_VLAN_ID, + "", + "", + 0, + 4095, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_VLAN_DEVICE_FACTORY (nm_vlan_device_factory_get_type()) +#define NM_VLAN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VLAN_DEVICE_FACTORY, NMVlanDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_VLAN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_DRIVER, + "8021q", + NM_DEVICE_TYPE_DESC, + "VLAN", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_VLAN, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_VLAN, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingVlan * s_vlan; + NMSettingWired *s_wired; + const char * parent = NULL; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VLAN_SETTING_NAME), NULL); + + s_vlan = nm_connection_get_setting_vlan(connection); + g_assert(s_vlan); + + parent = nm_setting_vlan_get_parent(s_vlan); + if (parent) + return parent; + + /* Try the hardware address from the VLAN connection's hardware setting */ + s_wired = nm_connection_get_setting_wired(connection); + if (s_wired) + return nm_setting_wired_get_mac_address(s_wired); + + return NULL; +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + const char * ifname; + NMSettingVlan *s_vlan; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VLAN_SETTING_NAME), NULL); + + s_vlan = nm_connection_get_setting_vlan(connection); + g_assert(s_vlan); + + if (!parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + if (ifname) + return g_strdup(ifname); + + /* If the connection doesn't specify the interface name for the VLAN + * device, we create one for it using the VLAN ID and the parent + * interface's name. + */ + return nm_utils_new_vlan_name(parent_iface, nm_setting_vlan_get_id(s_vlan)); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + VLAN, + Vlan, + vlan, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VLAN) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VLAN_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-vlan.h b/src/core/devices/nm-device-vlan.h new file mode 100644 index 00000000..5270706f --- /dev/null +++ b/src/core/devices/nm-device-vlan.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2012 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_VLAN_H__ +#define __NETWORKMANAGER_DEVICE_VLAN_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_VLAN (nm_device_vlan_get_type()) +#define NM_DEVICE_VLAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlan)) +#define NM_DEVICE_VLAN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VLAN, NMDeviceVlanClass)) +#define NM_IS_DEVICE_VLAN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VLAN)) +#define NM_IS_DEVICE_VLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VLAN)) +#define NM_DEVICE_VLAN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlanClass)) + +typedef enum { + NM_VLAN_ERROR_CONNECTION_NOT_VLAN = 0, /*< nick=ConnectionNotVlan >*/ + NM_VLAN_ERROR_CONNECTION_INVALID, /*< nick=ConnectionInvalid >*/ + NM_VLAN_ERROR_CONNECTION_INCOMPATIBLE, /*< nick=ConnectionIncompatible >*/ +} NMVlanError; + +#define NM_DEVICE_VLAN_ID "vlan-id" + +typedef struct _NMDeviceVlan NMDeviceVlan; +typedef struct _NMDeviceVlanClass NMDeviceVlanClass; + +GType nm_device_vlan_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_VLAN_H__ */ diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c new file mode 100644 index 00000000..4fec59ba --- /dev/null +++ b/src/core/devices/nm-device-vrf.c @@ -0,0 +1,375 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-vrf.h" + +#include "nm-core-internal.h" +#include "nm-device-factory.h" +#include "nm-device-private.h" +#include "nm-manager.h" +#include "nm-setting-vrf.h" +#include "platform/nm-platform.h" +#include "settings/nm-settings.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceVrf +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVrf, PROP_TABLE, ); + +typedef struct { + NMPlatformLnkVrf props; +} NMDeviceVrfPrivate; + +struct _NMDeviceVrf { + NMDevice parent; + NMDeviceVrfPrivate _priv; +}; + +struct _NMDeviceVrfClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceVrf, nm_device_vrf, NM_TYPE_DEVICE) + +#define NM_DEVICE_VRF_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceVrf, NM_IS_DEVICE_VRF, NMDevice) + +/*****************************************************************************/ + +static void +do_update_properties(NMDeviceVrf *self, const NMPlatformLnkVrf *props) +{ + NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(self); + GObject * object = G_OBJECT(self); + NMPlatformLnkVrf props_null; + + if (!props) { + props_null = (NMPlatformLnkVrf){}; + props = &props_null; + } + + g_object_freeze_notify(object); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED(table, PROP_TABLE); + + g_object_thaw_notify(object); +} + +static void +update_properties(NMDevice *device) +{ + NMDeviceVrf * self = NM_DEVICE_VRF(device); + const NMPlatformLnkVrf *props; + + props = nm_platform_link_get_lnk_vrf(nm_device_get_platform(device), + nm_device_get_ifindex(device), + NULL); + if (!props) { + _LOGW(LOGD_PLATFORM, "could not get vrf properties"); + return; + } + + do_update_properties(self, props); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_vrf_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static void +unrealize_notify(NMDevice *device) +{ + NMDeviceVrf *self = NM_DEVICE_VRF(device); + + NM_DEVICE_CLASS(nm_device_vrf_parent_class)->unrealize_notify(device); + + do_update_properties(self, NULL); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMPlatformLnkVrf props = {}; + NMSettingVrf * s_vrf; + int r; + + s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF); + nm_assert(s_vrf); + + props.table = nm_setting_vrf_get_table(s_vrf); + + r = nm_platform_link_vrf_add(nm_device_get_platform(device), iface, &props, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create VRF interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(device); + NMSettingVrf * s_vrf; + + if (!NM_DEVICE_CLASS(nm_device_vrf_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_device_is_real(device)) { + s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF); + + if (priv->props.table != nm_setting_vrf_get_table(s_vrf)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vrf table mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingVrf *s_vrf; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_VRF_SETTING_NAME, + existing_connections, + NULL, + _("VRF connection"), + NULL, + NULL, + TRUE); + + s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF); + if (!s_vrf) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'vrf' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(device); + NMSettingVrf * s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF); + + if (!s_vrf) { + s_vrf = (NMSettingVrf *) nm_setting_vrf_new(); + nm_connection_add_setting(connection, (NMSetting *) s_vrf); + } + + if (priv->props.table != nm_setting_vrf_get_table(s_vrf)) + g_object_set(G_OBJECT(s_vrf), NM_SETTING_VRF_TABLE, priv->props.table, NULL); +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + NMDeviceVrf *self = NM_DEVICE_VRF(device); + gboolean success = TRUE; + const char * slave_iface = nm_device_get_ip_iface(slave); + + nm_device_master_check_slave_physical_port(device, slave, LOGD_DEVICE); + + if (configure) { + nm_device_take_down(slave, TRUE); + success = nm_platform_link_enslave(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + nm_device_get_ip_ifindex(slave)); + nm_device_bring_up(slave, TRUE, NULL); + + if (!success) + return FALSE; + + _LOGI(LOGD_DEVICE, "enslaved VRF slave %s", slave_iface); + } else + _LOGI(LOGD_BOND, "VRF slave %s was enslaved", slave_iface); + + return TRUE; +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{ + NMDeviceVrf *self = NM_DEVICE_VRF(device); + gboolean success; + int ifindex_slave; + int ifindex; + + if (configure) { + ifindex = nm_device_get_ifindex(device); + if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex)) + configure = FALSE; + } + + ifindex_slave = nm_device_get_ip_ifindex(slave); + + if (ifindex_slave <= 0) + _LOGD(LOGD_DEVICE, "VRF slave %s is already released", nm_device_get_ip_iface(slave)); + + if (configure) { + if (ifindex_slave > 0) { + success = nm_platform_link_release(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + ifindex_slave); + + if (success) { + _LOGI(LOGD_DEVICE, "released VRF slave %s", nm_device_get_ip_iface(slave)); + } else { + _LOGW(LOGD_DEVICE, "failed to release VRF slave %s", nm_device_get_ip_iface(slave)); + } + } + } else { + if (ifindex_slave > 0) { + _LOGI(LOGD_DEVICE, "VRF slave %s was released", nm_device_get_ip_iface(slave)); + } + } +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_TABLE: + g_value_set_uint(value, priv->props.table); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_vrf_init(NMDeviceVrf *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_vrf = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_VRF, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Table", "u", NM_DEVICE_VRF_TABLE), ), ), +}; + +static void +nm_device_vrf_class_init(NMDeviceVrfClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vrf); + + device_class->connection_type_supported = NM_SETTING_VRF_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_VRF_SETTING_NAME; + device_class->is_master = TRUE; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VRF); + + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + device_class->link_changed = link_changed; + device_class->unrealize_notify = unrealize_notify; + device_class->create_and_realize = create_and_realize; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->update_connection = update_connection; + + obj_properties[PROP_TABLE] = g_param_spec_uint(NM_DEVICE_VRF_TABLE, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_VRF_DEVICE_FACTORY (nm_vrf_device_factory_get_type()) +#define NM_VRF_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VRF_DEVICE_FACTORY, NMVrfDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_VRF, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Vrf", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_VRF, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_VRF, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + VRF, + Vrf, + vrf, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VRF) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VRF_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-vrf.h b/src/core/devices/nm-device-vrf.h new file mode 100644 index 00000000..5169041c --- /dev/null +++ b/src/core/devices/nm-device-vrf.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef __NETWORKMANAGER_DEVICE_VRF_H__ +#define __NETWORKMANAGER_DEVICE_VRF_H__ + +#include "nm-device-generic.h" + +#define NM_TYPE_DEVICE_VRF (nm_device_vrf_get_type()) +#define NM_DEVICE_VRF(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrf)) +#define NM_DEVICE_VRF_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VRF, NMDeviceVrfClass)) +#define NM_IS_DEVICE_VRF(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VRF)) +#define NM_IS_DEVICE_VRF_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VRF)) +#define NM_DEVICE_VRF_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrfClass)) + +#define NM_DEVICE_VRF_TABLE "table" + +typedef struct _NMDeviceVrf NMDeviceVrf; +typedef struct _NMDeviceVrfClass NMDeviceVrfClass; + +GType nm_device_vrf_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_VRF_H__ */ diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c new file mode 100644 index 00000000..f16a52c4 --- /dev/null +++ b/src/core/devices/nm-device-vxlan.c @@ -0,0 +1,813 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-vxlan.h" + +#include "nm-device-private.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" +#include "nm-utils.h" +#include "nm-device-factory.h" +#include "nm-setting-vxlan.h" +#include "nm-setting-wired.h" +#include "settings/nm-settings.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceVxlan +#include "nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVxlan, + PROP_ID, + PROP_LOCAL, + PROP_GROUP, + PROP_TOS, + PROP_TTL, + PROP_LEARNING, + PROP_AGEING, + PROP_LIMIT, + PROP_SRC_PORT_MIN, + PROP_SRC_PORT_MAX, + PROP_DST_PORT, + PROP_PROXY, + PROP_RSC, + PROP_L2MISS, + PROP_L3MISS, ); + +typedef struct { + NMPlatformLnkVxlan props; +} NMDeviceVxlanPrivate; + +struct _NMDeviceVxlan { + NMDevice parent; + NMDeviceVxlanPrivate _priv; +}; + +struct _NMDeviceVxlanClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceVxlan, nm_device_vxlan, NM_TYPE_DEVICE) + +#define NM_DEVICE_VXLAN_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceVxlan, NM_IS_DEVICE_VXLAN, NMDevice) + +/*****************************************************************************/ + +static void +update_properties(NMDevice *device) +{ + NMDeviceVxlan * self = NM_DEVICE_VXLAN(device); + NMDeviceVxlanPrivate * priv = NM_DEVICE_VXLAN_GET_PRIVATE(self); + GObject * object = G_OBJECT(device); + const NMPlatformLnkVxlan *props; + + props = nm_platform_link_get_lnk_vxlan(nm_device_get_platform(device), + nm_device_get_ifindex(device), + NULL); + if (!props) { + _LOGW(LOGD_PLATFORM, "could not get vxlan properties"); + return; + } + + g_object_freeze_notify(object); + + if (priv->props.parent_ifindex != props->parent_ifindex) + nm_device_parent_set_ifindex(device, props->parent_ifindex); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->props.field != props->field) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + +#define CHECK_PROPERTY_CHANGED_IN6ADDR(field, prop) \ + G_STMT_START \ + { \ + if (memcmp(&priv->props.field, &props->field, sizeof(props->field)) != 0) { \ + priv->props.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED(id, PROP_ID); + CHECK_PROPERTY_CHANGED(local, PROP_LOCAL); + CHECK_PROPERTY_CHANGED_IN6ADDR(local6, PROP_LOCAL); + CHECK_PROPERTY_CHANGED(group, PROP_GROUP); + CHECK_PROPERTY_CHANGED_IN6ADDR(group6, PROP_GROUP); + CHECK_PROPERTY_CHANGED(tos, PROP_TOS); + CHECK_PROPERTY_CHANGED(ttl, PROP_TTL); + CHECK_PROPERTY_CHANGED(learning, PROP_LEARNING); + CHECK_PROPERTY_CHANGED(ageing, PROP_AGEING); + CHECK_PROPERTY_CHANGED(limit, PROP_LIMIT); + CHECK_PROPERTY_CHANGED(src_port_min, PROP_SRC_PORT_MIN); + CHECK_PROPERTY_CHANGED(src_port_max, PROP_SRC_PORT_MAX); + CHECK_PROPERTY_CHANGED(dst_port, PROP_DST_PORT); + CHECK_PROPERTY_CHANGED(proxy, PROP_PROXY); + CHECK_PROPERTY_CHANGED(rsc, PROP_RSC); + CHECK_PROPERTY_CHANGED(l2miss, PROP_L2MISS); + CHECK_PROPERTY_CHANGED(l3miss, PROP_L3MISS); + + g_object_thaw_notify(object); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_vxlan_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static void +unrealize_notify(NMDevice *device) +{ + NMDeviceVxlan * self = NM_DEVICE_VXLAN(device); + NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(self); + guint i; + + NM_DEVICE_CLASS(nm_device_vxlan_parent_class)->unrealize_notify(device); + + memset(&priv->props, 0, sizeof(NMPlatformLnkVxlan)); + + for (i = 1; i < _PROPERTY_ENUMS_LAST; i++) + g_object_notify_by_pspec(G_OBJECT(self), obj_properties[i]); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char * iface = nm_device_get_iface(device); + NMPlatformLnkVxlan props = {}; + NMSettingVxlan * s_vxlan; + const char * str; + int r; + + s_vxlan = nm_connection_get_setting_vxlan(connection); + g_return_val_if_fail(s_vxlan, FALSE); + + if (parent) + props.parent_ifindex = nm_device_get_ifindex(parent); + + props.id = nm_setting_vxlan_get_id(s_vxlan); + + str = nm_setting_vxlan_get_local(s_vxlan); + if (str) { + if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.local) + && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.local6)) + return FALSE; + } + + str = nm_setting_vxlan_get_remote(s_vxlan); + if (str) { + if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.group) + && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.group6)) + return FALSE; + } + + props.tos = nm_setting_vxlan_get_tos(s_vxlan); + props.ttl = nm_setting_vxlan_get_ttl(s_vxlan); + props.learning = nm_setting_vxlan_get_learning(s_vxlan); + props.ageing = nm_setting_vxlan_get_ageing(s_vxlan); + props.limit = nm_setting_vxlan_get_limit(s_vxlan); + props.src_port_min = nm_setting_vxlan_get_source_port_min(s_vxlan); + props.src_port_max = nm_setting_vxlan_get_source_port_max(s_vxlan); + props.dst_port = nm_setting_vxlan_get_destination_port(s_vxlan); + props.proxy = nm_setting_vxlan_get_proxy(s_vxlan); + props.rsc = nm_setting_vxlan_get_rsc(s_vxlan); + props.l2miss = nm_setting_vxlan_get_l2_miss(s_vxlan); + props.l3miss = nm_setting_vxlan_get_l3_miss(s_vxlan); + + r = nm_platform_link_vxlan_add(nm_device_get_platform(device), iface, &props, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create VXLAN interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +static gboolean +address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6) +{ + NMIPAddr candidate_addr; + int addr_family; + + if (!candidate) + return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6); + + if (!nm_utils_parse_inaddr_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr)) + return FALSE; + + if (!nm_ip_addr_equal(addr_family, + &candidate_addr, + NM_IS_IPv4(addr_family) ? (gpointer) &addr4 : addr6)) + return FALSE; + + if (NM_IS_IPv4(addr_family)) + return IN6_IS_ADDR_UNSPECIFIED(addr6); + else + return addr4 == 0u; +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device); + NMSettingVxlan * s_vxlan; + const char * parent; + + if (!NM_DEVICE_CLASS(nm_device_vxlan_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (nm_device_is_real(device)) { + s_vxlan = nm_connection_get_setting_vxlan(connection); + + parent = nm_setting_vxlan_get_parent(s_vxlan); + if (parent && !nm_device_match_parent(device, parent)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan parent mismatches"); + return FALSE; + } + + if (priv->props.id != nm_setting_vxlan_get_id(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan id mismatches"); + return FALSE; + } + + if (!address_matches(nm_setting_vxlan_get_local(s_vxlan), + priv->props.local, + &priv->props.local6)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan local address mismatches"); + return FALSE; + } + + if (!address_matches(nm_setting_vxlan_get_remote(s_vxlan), + priv->props.group, + &priv->props.group6)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan remote address mismatches"); + return FALSE; + } + + if (priv->props.src_port_min != nm_setting_vxlan_get_source_port_min(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan source port min mismatches"); + return FALSE; + } + + if (priv->props.src_port_max != nm_setting_vxlan_get_source_port_max(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan source port max mismatches"); + return FALSE; + } + + if (priv->props.dst_port != nm_setting_vxlan_get_destination_port(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan destination port mismatches"); + return FALSE; + } + + if (priv->props.tos != nm_setting_vxlan_get_tos(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan TOS mismatches"); + return FALSE; + } + + if (priv->props.ttl != nm_setting_vxlan_get_ttl(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan TTL mismatches"); + return FALSE; + } + + if (priv->props.learning != nm_setting_vxlan_get_learning(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan learning mismatches"); + return FALSE; + } + + if (priv->props.ageing != nm_setting_vxlan_get_ageing(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan ageing mismatches"); + return FALSE; + } + + if (priv->props.proxy != nm_setting_vxlan_get_proxy(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan proxy mismatches"); + return FALSE; + } + + if (priv->props.rsc != nm_setting_vxlan_get_rsc(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan rsc mismatches"); + return FALSE; + } + + if (priv->props.l2miss != nm_setting_vxlan_get_l2_miss(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan l2miss mismatches"); + return FALSE; + } + + if (priv->props.l3miss != nm_setting_vxlan_get_l3_miss(s_vxlan)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "vxlan l3miss mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingVxlan *s_vxlan; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_VXLAN_SETTING_NAME, + existing_connections, + NULL, + _("VXLAN connection"), + NULL, + NULL, + TRUE); + + s_vxlan = nm_connection_get_setting_vxlan(connection); + if (!s_vxlan) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'vxlan' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device); + NMSettingVxlan * s_vxlan = nm_connection_get_setting_vxlan(connection); + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + if (!s_vxlan) { + s_vxlan = (NMSettingVxlan *) nm_setting_vxlan_new(); + nm_connection_add_setting(connection, (NMSetting *) s_vxlan); + } + + if (priv->props.id != nm_setting_vxlan_get_id(s_vxlan)) + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_ID, priv->props.id, NULL); + + g_object_set(s_vxlan, + NM_SETTING_VXLAN_PARENT, + nm_device_parent_find_for_connection(device, nm_setting_vxlan_get_parent(s_vxlan)), + NULL); + + if (!address_matches(nm_setting_vxlan_get_remote(s_vxlan), + priv->props.group, + &priv->props.group6)) { + if (priv->props.group) { + g_object_set(s_vxlan, + NM_SETTING_VXLAN_REMOTE, + _nm_utils_inet4_ntop(priv->props.group, sbuf), + NULL); + } else { + g_object_set(s_vxlan, + NM_SETTING_VXLAN_REMOTE, + _nm_utils_inet6_ntop(&priv->props.group6, sbuf), + NULL); + } + } + + if (!address_matches(nm_setting_vxlan_get_local(s_vxlan), + priv->props.local, + &priv->props.local6)) { + if (priv->props.local) { + g_object_set(s_vxlan, + NM_SETTING_VXLAN_LOCAL, + _nm_utils_inet4_ntop(priv->props.local, sbuf), + NULL); + } else if (memcmp(&priv->props.local6, &in6addr_any, sizeof(in6addr_any))) { + g_object_set(s_vxlan, + NM_SETTING_VXLAN_LOCAL, + _nm_utils_inet6_ntop(&priv->props.local6, sbuf), + NULL); + } + } + + if (priv->props.src_port_min != nm_setting_vxlan_get_source_port_min(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), + NM_SETTING_VXLAN_SOURCE_PORT_MIN, + priv->props.src_port_min, + NULL); + } + + if (priv->props.src_port_max != nm_setting_vxlan_get_source_port_max(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), + NM_SETTING_VXLAN_SOURCE_PORT_MAX, + priv->props.src_port_max, + NULL); + } + + if (priv->props.dst_port != nm_setting_vxlan_get_destination_port(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), + NM_SETTING_VXLAN_DESTINATION_PORT, + priv->props.dst_port, + NULL); + } + + if (priv->props.tos != nm_setting_vxlan_get_tos(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_TOS, priv->props.tos, NULL); + } + + if (priv->props.ttl != nm_setting_vxlan_get_ttl(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_TTL, priv->props.ttl, NULL); + } + + if (priv->props.learning != nm_setting_vxlan_get_learning(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_LEARNING, priv->props.learning, NULL); + } + + if (priv->props.ageing != nm_setting_vxlan_get_ageing(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_AGEING, priv->props.ageing, NULL); + } + + if (priv->props.proxy != nm_setting_vxlan_get_proxy(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_PROXY, priv->props.proxy, NULL); + } + + if (priv->props.rsc != nm_setting_vxlan_get_rsc(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_RSC, priv->props.rsc, NULL); + } + + if (priv->props.l2miss != nm_setting_vxlan_get_l2_miss(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_L2_MISS, priv->props.l2miss, NULL); + } + + if (priv->props.l3miss != nm_setting_vxlan_get_l3_miss(s_vxlan)) { + g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_L3_MISS, priv->props.l3miss, NULL); + } +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_ID: + g_value_set_uint(value, priv->props.id); + break; + case PROP_GROUP: + if (priv->props.group) + g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.group)); + else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.group6)) + g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.group6)); + break; + case PROP_LOCAL: + if (priv->props.local) + g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.local)); + else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.local6)) + g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.local6)); + break; + case PROP_TOS: + g_value_set_uchar(value, priv->props.tos); + break; + case PROP_TTL: + g_value_set_uchar(value, priv->props.ttl); + break; + case PROP_LEARNING: + g_value_set_boolean(value, priv->props.learning); + break; + case PROP_AGEING: + g_value_set_uint(value, priv->props.ageing); + break; + case PROP_LIMIT: + g_value_set_uint(value, priv->props.limit); + break; + case PROP_DST_PORT: + g_value_set_uint(value, priv->props.dst_port); + break; + case PROP_SRC_PORT_MIN: + g_value_set_uint(value, priv->props.src_port_min); + break; + case PROP_SRC_PORT_MAX: + g_value_set_uint(value, priv->props.src_port_max); + break; + case PROP_PROXY: + g_value_set_boolean(value, priv->props.proxy); + break; + case PROP_RSC: + g_value_set_boolean(value, priv->props.rsc); + break; + case PROP_L2MISS: + g_value_set_boolean(value, priv->props.l2miss); + break; + case PROP_L3MISS: + g_value_set_boolean(value, priv->props.l3miss); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_vxlan_init(NMDeviceVxlan *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_vxlan = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_VXLAN, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Id", "u", NM_DEVICE_VXLAN_ID), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Group", "s", NM_DEVICE_VXLAN_GROUP), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Local", "s", NM_DEVICE_VXLAN_LOCAL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tos", "y", NM_DEVICE_VXLAN_TOS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ttl", "y", NM_DEVICE_VXLAN_TTL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Learning", + "b", + NM_DEVICE_VXLAN_LEARNING), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ageing", "u", NM_DEVICE_VXLAN_AGEING), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Limit", "u", NM_DEVICE_VXLAN_LIMIT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DstPort", + "q", + NM_DEVICE_VXLAN_DST_PORT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("SrcPortMin", + "q", + NM_DEVICE_VXLAN_SRC_PORT_MIN), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("SrcPortMax", + "q", + NM_DEVICE_VXLAN_SRC_PORT_MAX), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Proxy", "b", NM_DEVICE_VXLAN_PROXY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Rsc", "b", NM_DEVICE_VXLAN_RSC), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("L2miss", "b", NM_DEVICE_VXLAN_L2MISS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("L3miss", + "b", + NM_DEVICE_VXLAN_L3MISS), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_vxlan_class_init(NMDeviceVxlanClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vxlan); + + device_class->connection_type_supported = NM_SETTING_VXLAN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_VXLAN_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VXLAN); + + device_class->link_changed = link_changed; + device_class->unrealize_notify = unrealize_notify; + device_class->create_and_realize = create_and_realize; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->update_connection = update_connection; + device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + + obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_VXLAN_ID, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LOCAL] = g_param_spec_string(NM_DEVICE_VXLAN_LOCAL, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_GROUP] = g_param_spec_string(NM_DEVICE_VXLAN_GROUP, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_VXLAN_TOS, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_VXLAN_TTL, + "", + "", + 0, + 255, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LEARNING] = g_param_spec_boolean(NM_DEVICE_VXLAN_LEARNING, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_AGEING] = g_param_spec_uint(NM_DEVICE_VXLAN_AGEING, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LIMIT] = g_param_spec_uint(NM_DEVICE_VXLAN_LIMIT, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SRC_PORT_MIN] = + g_param_spec_uint(NM_DEVICE_VXLAN_SRC_PORT_MIN, + "", + "", + 0, + 65535, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SRC_PORT_MAX] = + g_param_spec_uint(NM_DEVICE_VXLAN_SRC_PORT_MAX, + "", + "", + 0, + 65535, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_VXLAN_DST_PORT, + "", + "", + 0, + 65535, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_PROXY] = g_param_spec_boolean(NM_DEVICE_VXLAN_PROXY, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_RSC] = g_param_spec_boolean(NM_DEVICE_VXLAN_RSC, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_L2MISS] = g_param_spec_boolean(NM_DEVICE_VXLAN_L2MISS, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_L3MISS] = g_param_spec_boolean(NM_DEVICE_VXLAN_L3MISS, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +#define NM_TYPE_VXLAN_DEVICE_FACTORY (nm_vxlan_device_factory_get_type()) +#define NM_VXLAN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VXLAN_DEVICE_FACTORY, NMVxlanDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_VXLAN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "Vxlan", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_VXLAN, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_VXLAN, + NULL); +} + +static const char * +get_connection_parent(NMDeviceFactory *factory, NMConnection *connection) +{ + NMSettingVxlan *s_vxlan; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VXLAN_SETTING_NAME), NULL); + + s_vxlan = nm_connection_get_setting_vxlan(connection); + g_assert(s_vxlan); + + return nm_setting_vxlan_get_parent(s_vxlan); +} + +static char * +get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface) +{ + const char * ifname; + NMSettingVxlan *s_vxlan; + + g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VXLAN_SETTING_NAME), NULL); + + s_vxlan = nm_connection_get_setting_vxlan(connection); + g_assert(s_vxlan); + + if (nm_setting_vxlan_get_parent(s_vxlan) && !parent_iface) + return NULL; + + ifname = nm_connection_get_interface_name(connection); + return g_strdup(ifname); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + VXLAN, + Vxlan, + vxlan, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VXLAN) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VXLAN_SETTING_NAME), + factory_class->create_device = create_device; + factory_class->get_connection_parent = get_connection_parent; + factory_class->get_connection_iface = get_connection_iface;); diff --git a/src/core/devices/nm-device-vxlan.h b/src/core/devices/nm-device-vxlan.h new file mode 100644 index 00000000..4c4165e5 --- /dev/null +++ b/src/core/devices/nm-device-vxlan.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013, 2014 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_VXLAN_H__ +#define __NETWORKMANAGER_DEVICE_VXLAN_H__ + +#include "nm-device-generic.h" + +#define NM_TYPE_DEVICE_VXLAN (nm_device_vxlan_get_type()) +#define NM_DEVICE_VXLAN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlan)) +#define NM_DEVICE_VXLAN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlanClass)) +#define NM_IS_DEVICE_VXLAN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VXLAN)) +#define NM_IS_DEVICE_VXLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VXLAN)) +#define NM_DEVICE_VXLAN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlanClass)) + +#define NM_DEVICE_VXLAN_ID "id" +#define NM_DEVICE_VXLAN_GROUP "group" +#define NM_DEVICE_VXLAN_LOCAL "local" +#define NM_DEVICE_VXLAN_TOS "tos" +#define NM_DEVICE_VXLAN_TTL "ttl" +#define NM_DEVICE_VXLAN_LEARNING "learning" +#define NM_DEVICE_VXLAN_AGEING "ageing" +#define NM_DEVICE_VXLAN_LIMIT "limit" +#define NM_DEVICE_VXLAN_DST_PORT "dst-port" +#define NM_DEVICE_VXLAN_SRC_PORT_MIN "src-port-min" +#define NM_DEVICE_VXLAN_SRC_PORT_MAX "src-port-max" +#define NM_DEVICE_VXLAN_PROXY "proxy" +#define NM_DEVICE_VXLAN_RSC "rsc" +#define NM_DEVICE_VXLAN_L2MISS "l2miss" +#define NM_DEVICE_VXLAN_L3MISS "l3miss" + +typedef struct _NMDeviceVxlan NMDeviceVxlan; +typedef struct _NMDeviceVxlanClass NMDeviceVxlanClass; + +GType nm_device_vxlan_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_VXLAN_H__ */ diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c new file mode 100644 index 00000000..fd057ded --- /dev/null +++ b/src/core/devices/nm-device-wireguard.c @@ -0,0 +1,2093 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Javier Arteaga <jarteaga@jbeta.is> + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-wireguard.h" + +#include <linux/rtnetlink.h> +#include <linux/fib_rules.h> + +#include "nm-setting-wireguard.h" +#include "nm-core-internal.h" +#include "nm-glib-aux/nm-secret-utils.h" +#include "nm-device-private.h" +#include "platform/nm-platform.h" +#include "platform/nmp-object.h" +#include "platform/nmp-rules-manager.h" +#include "nm-device-factory.h" +#include "nm-active-connection.h" +#include "nm-act-request.h" +#include "dns/nm-dns-manager.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceWireGuard +#include "nm-device-logging.h" + +/*****************************************************************************/ + +/* TODO: activate profile with peer preshared-key-flags=2. On first activation, the secret is + * requested (good). Enter it and connect. Reactivate the profile, now there is no password + * prompt, as the secret is cached (good??). */ + +/* TODO: unlike for other VPNs, we don't inject a direct route to the peers. That means, + * you might get a routing scenario where the peer (VPN server) is reachable via the VPN. + * How we handle adding routes to external gateway for other peers, has severe issues + * as well. We may use policy-routing like wg-quick does. See also discussions at + * https://www.wireguard.com/netns/#improving-the-classic-solutions */ + +/* TODO: honor the TTL of DNS to determine when to retry resolving endpoints. */ + +/* TODO: when we get multiple IP addresses when resolving a peer endpoint. We currently + * just take the first from GAI. We should only accept AAAA/IPv6 if we also have a suitable + * IPv6 address. The problem is, that we have to recheck that when IP addressing on other + * interfaces changes. This makes it almost too cumbersome to implement. */ + +/*****************************************************************************/ + +G_STATIC_ASSERT(NM_WIREGUARD_PUBLIC_KEY_LEN == NMP_WIREGUARD_PUBLIC_KEY_LEN); +G_STATIC_ASSERT(NM_WIREGUARD_SYMMETRIC_KEY_LEN == NMP_WIREGUARD_SYMMETRIC_KEY_LEN); + +/*****************************************************************************/ + +#define LINK_CONFIG_RATE_LIMIT_NSEC (50 * NM_UTILS_NSEC_PER_MSEC) + +/* a special @next_try_at_nsec timestamp indicating that we should try again as soon as possible. */ +#define NEXT_TRY_AT_NSEC_ASAP ((gint64) G_MAXINT64) + +/* a special @next_try_at_nsec timestamp that is + * - positive (indicating resolve-checks are enabled) + * - already in the past (we use the absolute timestamp of 1nsec for that). */ +#define NEXT_TRY_AT_NSEC_PAST ((gint64) 1) + +/* like %NEXT_TRY_AT_NSEC_ASAP, but used for indicating to retry ASAP for a @retry_in_msec value. + * That is a relative time duration, contrary to @next_try_at_nsec which is an absolute + * timestamp. */ +#define RETRY_IN_MSEC_ASAP ((gint64) G_MAXINT64) + +#define RETRY_IN_MSEC_MAX ((gint64)(30 * 60 * 1000)) + +typedef enum { + LINK_CONFIG_MODE_FULL, + LINK_CONFIG_MODE_REAPPLY, + LINK_CONFIG_MODE_ASSUME, + LINK_CONFIG_MODE_ENDPOINTS, +} LinkConfigMode; + +typedef struct { + GCancellable *cancellable; + + NMSockAddrUnion sockaddr; + + /* the timestamp (in nm_utils_get_monotonic_timestamp_nsec() scale) when we want + * to retry resolving the endpoint (again). + * + * It may be set to %NEXT_TRY_AT_NSEC_ASAP to indicate to re-resolve as soon as possible. + * + * A @sockaddr is either fixed or it has + * - @cancellable set to indicate an ongoing request + * - @next_try_at_nsec set to a positive value, indicating when + * we ought to retry. */ + gint64 next_try_at_nsec; + + guint resolv_fail_count; +} PeerEndpointResolveData; + +typedef struct { + NMWireGuardPeer *peer; + + NMDeviceWireGuard *self; + + CList lst_peers; + + PeerEndpointResolveData ep_resolv; + + /* dirty flag used during _peers_update_all(). */ + bool dirty_update_all : 1; +} PeerData; + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWireGuard, PROP_PUBLIC_KEY, PROP_LISTEN_PORT, PROP_FWMARK, ); + +typedef struct { + NMDnsManager *dns_manager; + + NMPlatformLnkWireGuard lnk_curr; + NMActRequestGetSecretsCallId *secrets_call_id; + + CList lst_peers_head; + GHashTable *peers; + + /* counts the numbers of peers that are currently resolving. */ + guint peers_resolving_cnt; + + gint64 resolve_next_try_at; + gint64 link_config_last_at; + + guint resolve_next_try_id; + guint link_config_delayed_id; + + guint32 auto_default_route_fwmark; + + guint32 auto_default_route_priority; + + bool auto_default_route_enabled_4 : 1; + bool auto_default_route_enabled_6 : 1; + bool auto_default_route_initialized : 1; + bool auto_default_route_refresh : 1; + bool auto_default_route_priority_initialized : 1; + +} NMDeviceWireGuardPrivate; + +struct _NMDeviceWireGuard { + NMDevice parent; + NMDeviceWireGuardPrivate _priv; +}; + +struct _NMDeviceWireGuardClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceWireGuard, nm_device_wireguard, NM_TYPE_DEVICE) + +#define NM_DEVICE_WIREGUARD_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceWireGuard, NM_IS_DEVICE_WIREGUARD, NMDevice) + +/*****************************************************************************/ + +static void _peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data); + +static void _peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec); + +static gboolean link_config_delayed_resolver_cb(gpointer user_data); + +static gboolean link_config_delayed_ratelimit_cb(gpointer user_data); + +/*****************************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE(_link_config_mode_to_string, + LinkConfigMode, + NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL), + NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_FULL, "full"), + NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_REAPPLY, "reapply"), + NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ASSUME, "assume"), + NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ENDPOINTS, "endpoints"), ); + +/*****************************************************************************/ + +static void +_auto_default_route_get_enabled(NMSettingWireGuard *s_wg, + NMConnection * connection, + gboolean * out_enabled_v4, + gboolean * out_enabled_v6) +{ + NMTernary enabled_v4; + NMTernary enabled_v6; + + enabled_v4 = nm_setting_wireguard_get_ip4_auto_default_route(s_wg); + enabled_v6 = nm_setting_wireguard_get_ip6_auto_default_route(s_wg); + + if (enabled_v4 == NM_TERNARY_DEFAULT) { + if (nm_setting_ip_config_get_never_default( + nm_connection_get_setting_ip_config(connection, AF_INET))) + enabled_v4 = FALSE; + } + if (enabled_v6 == NM_TERNARY_DEFAULT) { + if (nm_setting_ip_config_get_never_default( + nm_connection_get_setting_ip_config(connection, AF_INET6))) + enabled_v6 = FALSE; + } + + if (enabled_v4 == NM_TERNARY_DEFAULT || enabled_v6 == NM_TERNARY_DEFAULT) { + guint i, n_peers; + + n_peers = nm_setting_wireguard_get_peers_len(s_wg); + for (i = 0; i < n_peers; i++) { + NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i); + guint n_aips; + guint j; + + n_aips = nm_wireguard_peer_get_allowed_ips_len(peer); + for (j = 0; j < n_aips; j++) { + const char *aip; + gboolean valid; + int prefix; + int addr_family; + + aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid); + if (!valid) + continue; + if (!nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix)) + continue; + if (prefix != 0) + continue; + + if (addr_family == AF_INET) { + if (enabled_v4 == NM_TERNARY_DEFAULT) { + enabled_v4 = TRUE; + if (enabled_v6 != NM_TERNARY_DEFAULT) + goto done; + } + } else { + if (enabled_v6 == NM_TERNARY_DEFAULT) { + enabled_v6 = TRUE; + if (enabled_v4 != NM_TERNARY_DEFAULT) + goto done; + } + } + } + } +done:; + } + + *out_enabled_v4 = (enabled_v4 == TRUE); + *out_enabled_v6 = (enabled_v6 == TRUE); +} + +#define AUTO_RANDOM_RANGE 500u + +static guint32 +_auto_default_route_get_auto_fwmark(const char *uuid) +{ + guint64 rnd_seed; + + /* we use the generated number as fwmark but also as routing table for + * the default-route. + * + * We pick a number + * + * - based on the connection's UUID (as stable seed). + * - larger than 51820u (arbitrarily) + * - one out of AUTO_RANDOM_RANGE + */ + + rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0xb9, + 0x39, + 0x8e, + 0xed, + 0x15, + 0xb3, + 0xd1, + 0xc4, + 0x5f, + 0x45, + 0x00, + 0x4f, + 0xec, + 0xc2, + 0x2b, + 0x7e), + (const guint8 *) uuid, + uuid ? strlen(uuid) + 1u : 0u); + + return 51820u + (rnd_seed % AUTO_RANDOM_RANGE); +} + +#define PRIO_WIDTH 2u + +static guint32 +_auto_default_route_get_auto_priority(const char *uuid) +{ + const guint32 RANGE_TOP = 32766u - 1000u; + guint64 rnd_seed; + + /* we pick a priority for the routing rules as follows: + * + * - use the connection's UUID as stable seed for the "random" number. + * - have it smaller than RANGE_TOP (32766u - 1000u), where 32766u is the priority of the default + * rules + * - we add 2 rules (PRIO_WIDTH). Hence only pick even priorities. + * - pick one out of AUTO_RANDOM_RANGE. */ + + rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0x99, + 0x22, + 0x4d, + 0x7c, + 0x37, + 0xda, + 0x8e, + 0x7b, + 0x2f, + 0x55, + 0x16, + 0x7b, + 0x75, + 0xda, + 0x42, + 0xdc), + (const guint8 *) uuid, + uuid ? strlen(uuid) + 1u : 0u); + + return RANGE_TOP - (((rnd_seed % (PRIO_WIDTH * AUTO_RANDOM_RANGE)) / PRIO_WIDTH) * PRIO_WIDTH); +} + +static void +_auto_default_route_init(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + NMConnection * connection; + gboolean enabled_v4 = FALSE; + gboolean enabled_v6 = FALSE; + gboolean refreshing_only; + guint32 new_fwmark = 0; + guint32 old_fwmark; + char sbuf1[100]; + + if (G_LIKELY(priv->auto_default_route_initialized && !priv->auto_default_route_refresh)) + return; + + refreshing_only = priv->auto_default_route_initialized && priv->auto_default_route_refresh; + + old_fwmark = priv->auto_default_route_fwmark; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + if (connection) { + NMSettingWireGuard *s_wg; + + s_wg = _nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD); + + new_fwmark = nm_setting_wireguard_get_fwmark(s_wg); + + _auto_default_route_get_enabled(s_wg, connection, &enabled_v4, &enabled_v6); + } + + if ((enabled_v4 || enabled_v6) && new_fwmark == 0u) { + if (refreshing_only) + new_fwmark = old_fwmark; + else + new_fwmark = _auto_default_route_get_auto_fwmark(nm_connection_get_uuid(connection)); + } + + priv->auto_default_route_refresh = FALSE; + priv->auto_default_route_fwmark = new_fwmark; + priv->auto_default_route_enabled_4 = enabled_v4; + priv->auto_default_route_enabled_6 = enabled_v6; + priv->auto_default_route_initialized = TRUE; + + if (connection) { + _LOGT(LOGD_DEVICE, + "auto-default-route is %s for IPv4 and %s for IPv6%s", + priv->auto_default_route_enabled_4 ? "enabled" : "disabled", + priv->auto_default_route_enabled_6 ? "enabled" : "disabled", + priv->auto_default_route_enabled_4 || priv->auto_default_route_enabled_6 + ? nm_sprintf_buf(sbuf1, " (fwmark 0x%x)", priv->auto_default_route_fwmark) + : ""); + } +} + +static GPtrArray * +get_extra_rules(NMDevice *device) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(device); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *extra_rules = NULL; + guint32 priority = 0; + int is_ipv4; + NMConnection * connection; + + _auto_default_route_init(self); + + connection = nm_device_get_applied_connection(device); + if (!connection) + return NULL; + + for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) { + NMSettingIPConfig *s_ip; + int addr_family = is_ipv4 ? AF_INET : AF_INET6; + guint32 table_main; + guint32 fwmark; + + if (is_ipv4) { + if (!priv->auto_default_route_enabled_4) + continue; + } else { + if (!priv->auto_default_route_enabled_6) + continue; + } + + if (!extra_rules) { + if (priv->auto_default_route_priority_initialized) + priority = priv->auto_default_route_priority; + else { + priority = + _auto_default_route_get_auto_priority(nm_connection_get_uuid(connection)); + priv->auto_default_route_priority = priority; + priv->auto_default_route_priority_initialized = TRUE; + } + extra_rules = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref); + } + + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + table_main = nm_setting_ip_config_get_route_table(s_ip); + if (table_main == 0) + table_main = RT_TABLE_MAIN; + + fwmark = priv->auto_default_route_fwmark; + + G_STATIC_ASSERT_EXPR(PRIO_WIDTH == 2); + + g_ptr_array_add(extra_rules, + nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE, + &((const NMPlatformRoutingRule){ + .priority = priority, + .addr_family = addr_family, + .action = FR_ACT_TO_TBL, + .table = table_main, + .suppress_prefixlen_inverse = ~((guint32) 0u), + }))); + + g_ptr_array_add(extra_rules, + nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE, + &((const NMPlatformRoutingRule){ + .priority = priority + 1u, + .addr_family = addr_family, + .action = FR_ACT_TO_TBL, + .table = fwmark, + .flags = FIB_RULE_INVERT, + .fwmark = fwmark, + .fwmask = 0xFFFFFFFFu, + }))); + } + + return g_steal_pointer(&extra_rules); +} + +static guint32 +coerce_route_table(NMDevice *device, int addr_family, guint32 route_table, gboolean is_user_config) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(device); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gboolean auto_default_route_enabled; + + if (route_table != 0u) + return route_table; + + _auto_default_route_init(self); + + auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4 + : priv->auto_default_route_enabled_6; + + if (auto_default_route_enabled) { + /* we need to enable full-sync mode of all routing tables. */ + _LOGT(LOGD_DEVICE, + "coerce ipv%c.route-table setting to \"main\" (table 254) as we enable " + "auto-default-route handling", + nm_utils_addr_family_to_char(addr_family)); + return RT_TABLE_MAIN; + } + + return 0; +} + +/*****************************************************************************/ + +static gboolean +_peer_data_equal(gconstpointer ptr_a, gconstpointer ptr_b) +{ + const PeerData *peer_data_a = ptr_a; + const PeerData *peer_data_b = ptr_b; + + return nm_streq(nm_wireguard_peer_get_public_key(peer_data_a->peer), + nm_wireguard_peer_get_public_key(peer_data_b->peer)); +} + +static guint +_peer_data_hash(gconstpointer ptr) +{ + const PeerData *peer_data = ptr; + + return nm_hash_str(nm_wireguard_peer_get_public_key(peer_data->peer)); +} + +static PeerData * +_peers_find(NMDeviceWireGuardPrivate *priv, NMWireGuardPeer *peer) +{ + nm_assert(peer); + + G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(PeerData, peer) == 0); + + return g_hash_table_lookup(priv->peers, &peer); +} + +static guint +_peers_resolving_cnt(NMDeviceWireGuardPrivate *priv) +{ + nm_assert(priv); +#if NM_MORE_ASSERTS > 3 + { + PeerData *peer_data; + guint cnt = 0; + + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + if (peer_data->ep_resolv.cancellable) + cnt++; + } + nm_assert(cnt == priv->peers_resolving_cnt); + } +#endif + + return priv->peers_resolving_cnt; +} + +static void +_peers_resolving_cnt_decrement(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + nm_assert(priv); + nm_assert(priv->peers_resolving_cnt > 0); + + priv->peers_resolving_cnt--; + + nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt); + + if (priv->peers_resolving_cnt == 0) { + if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) { + _LOGT(LOGD_DEVICE, + "activation delayed to resolve DNS names of peers: completed, proceed now"); + nm_device_activate_schedule_stage2_device_config(NM_DEVICE(self), FALSE); + } + } +} + +static void +_peers_remove(NMDeviceWireGuard *self, PeerData *peer_data) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + nm_assert(peer_data); + nm_assert(g_hash_table_lookup(priv->peers, peer_data) == peer_data); + + if (!g_hash_table_remove(priv->peers, peer_data)) + nm_assert_not_reached(); + + c_list_unlink_stale(&peer_data->lst_peers); + nm_wireguard_peer_unref(peer_data->peer); + if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable)) + _peers_resolving_cnt_decrement(self); + g_slice_free(PeerData, peer_data); + + if (c_list_is_empty(&priv->lst_peers_head)) { + nm_clear_g_source(&priv->resolve_next_try_id); + nm_clear_g_source(&priv->link_config_delayed_id); + } + + nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt); +} + +static PeerData * +_peers_add(NMDeviceWireGuard *self, NMWireGuardPeer *peer) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + PeerData * peer_data; + + nm_assert(peer); + nm_assert(nm_wireguard_peer_is_sealed(peer)); + nm_assert(!_peers_find(priv, peer)); + + peer_data = g_slice_new(PeerData); + *peer_data = (PeerData){ + .self = self, + .peer = nm_wireguard_peer_ref(peer), + .ep_resolv = + { + .sockaddr = NM_SOCK_ADDR_UNION_INIT_UNSPEC, + }, + }; + + c_list_link_tail(&priv->lst_peers_head, &peer_data->lst_peers); + if (!nm_g_hash_table_add(priv->peers, peer_data)) + nm_assert_not_reached(); + return peer_data; +} + +static gboolean +_peers_resolve_retry_timeout(gpointer user_data) +{ + NMDeviceWireGuard * self = user_data; + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + PeerData * peer_data; + gint64 now; + gint64 next; + + priv->resolve_next_try_id = 0; + + _LOGT(LOGD_DEVICE, "wireguard-peers: rechecking peer endpoints..."); + + now = nm_utils_get_monotonic_timestamp_nsec(); + next = G_MAXINT64; + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + if (peer_data->ep_resolv.next_try_at_nsec <= 0) + continue; + + if (peer_data->ep_resolv.cancellable) { + /* we are currently resolving a name. We don't need the global + * watchdog to guard this peer. No need to adjust @next for + * this one, when the currently ongoing resolving completes, we + * may reschedule. Skip. */ + continue; + } + + if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP + || now >= peer_data->ep_resolv.next_try_at_nsec) { + _peers_resolve_start(self, peer_data); + /* same here. Now we are resolving. We don't need the global + * watchdog. Skip w.r.t. finding @next. */ + continue; + } + + if (next > peer_data->ep_resolv.next_try_at_nsec) + next = peer_data->ep_resolv.next_try_at_nsec; + } + if (next < G_MAXINT64) + _peers_resolve_retry_reschedule(self, next); + + return G_SOURCE_REMOVE; +} + +static void +_peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + guint32 interval_ms; + gint64 now; + + nm_assert(new_next_try_at_nsec > 0); + nm_assert(new_next_try_at_nsec != NEXT_TRY_AT_NSEC_ASAP); + + if (priv->resolve_next_try_id && priv->resolve_next_try_at <= new_next_try_at_nsec) { + /* we already have an earlier timeout scheduled (possibly for + * another peer that expires sooner). Don't reschedule now. + * Even if the scheduled timeout expires too early, we will + * compute the right next-timeout and reschedule then. */ + return; + } + + now = nm_utils_get_monotonic_timestamp_nsec(); + + /* schedule at most one day ahead. No problem if we expire earlier + * than expected. Also, rate-limit to 500 msec. */ + interval_ms = NM_CLAMP((new_next_try_at_nsec - now) / NM_UTILS_NSEC_PER_MSEC, + (gint64) 500, + (gint64)(24 * 60 * 60 * 1000)); + + _LOGT(LOGD_DEVICE, + "wireguard-peers: schedule rechecking peer endpoints in %u msec", + interval_ms); + + nm_clear_g_source(&priv->resolve_next_try_id); + priv->resolve_next_try_at = new_next_try_at_nsec; + priv->resolve_next_try_id = g_timeout_add(interval_ms, _peers_resolve_retry_timeout, self); +} + +static void +_peers_resolve_retry_reschedule_for_peer(NMDeviceWireGuard *self, + PeerData * peer_data, + gint64 retry_in_msec) +{ + nm_assert(retry_in_msec >= 0); + + if (retry_in_msec == RETRY_IN_MSEC_ASAP) { + _peers_resolve_start(self, peer_data); + return; + } + + peer_data->ep_resolv.next_try_at_nsec = + nm_utils_get_monotonic_timestamp_nsec() + (retry_in_msec * NM_UTILS_NSEC_PER_MSEC); + _peers_resolve_retry_reschedule(self, peer_data->ep_resolv.next_try_at_nsec); +} + +static gint64 +_peers_retry_in_msec(PeerData *peer_data, gboolean after_failure) +{ + if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP) { + peer_data->ep_resolv.resolv_fail_count = 0; + return RETRY_IN_MSEC_ASAP; + } + + if (after_failure) { + if (peer_data->ep_resolv.resolv_fail_count < G_MAXUINT) + peer_data->ep_resolv.resolv_fail_count++; + } else + peer_data->ep_resolv.resolv_fail_count = 0; + + if (!after_failure) + return RETRY_IN_MSEC_MAX; + + if (peer_data->ep_resolv.resolv_fail_count > 20) + return RETRY_IN_MSEC_MAX; + + /* double the retry-time, starting with one second. */ + return NM_MIN(RETRY_IN_MSEC_MAX, (1u << peer_data->ep_resolv.resolv_fail_count) * 500); +} + +static void +_peers_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + NMDeviceWireGuard * self; + NMDeviceWireGuardPrivate *priv; + PeerData * peer_data; + gs_free_error GError *resolv_error = NULL; + GList * list; + gboolean changed = FALSE; + NMSockAddrUnion sockaddr; + gint64 retry_in_msec; + char s_sockaddr[100]; + char s_retry[100]; + + list = g_resolver_lookup_by_name_finish(G_RESOLVER(source_object), res, &resolv_error); + + if (nm_utils_error_is_cancelled(resolv_error)) + return; + + peer_data = user_data; + self = peer_data->self; + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + if (nm_clear_g_object(&peer_data->ep_resolv.cancellable)) + _peers_resolving_cnt_decrement(self); + + nm_assert((!resolv_error) != (!list)); + nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt); + +#define _retry_in_msec_to_string(retry_in_msec, s_retry) \ + ({ \ + gint64 _retry_in_msec = (retry_in_msec); \ + \ + _retry_in_msec == RETRY_IN_MSEC_ASAP \ + ? "right away" \ + : nm_sprintf_buf(s_retry, "in %" G_GINT64_FORMAT " msec", _retry_in_msec); \ + }) + + if (resolv_error + && !g_error_matches(resolv_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND)) { + retry_in_msec = _peers_retry_in_msec(peer_data, TRUE); + + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: failure to resolve endpoint \"%s\": %s (retry %s)", + nm_wireguard_peer_get_public_key(peer_data->peer), + nm_wireguard_peer_get_endpoint(peer_data->peer), + resolv_error->message, + _retry_in_msec_to_string(retry_in_msec, s_retry)); + + _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec); + return; + } + + sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC; + + if (!resolv_error) { + GList *iter; + + for (iter = list; iter; iter = iter->next) { + GInetAddress *a = iter->data; + GSocketFamily f = g_inet_address_get_family(a); + + if (f == G_SOCKET_FAMILY_IPV4) { + nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in_addr)); + sockaddr.in = (struct sockaddr_in){ + .sin_family = AF_INET, + .sin_port = htons(nm_sock_addr_endpoint_get_port( + _nm_wireguard_peer_get_endpoint(peer_data->peer))), + }; + memcpy(&sockaddr.in.sin_addr, g_inet_address_to_bytes(a), sizeof(struct in_addr)); + break; + } + if (f == G_SOCKET_FAMILY_IPV6) { + nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in6_addr)); + sockaddr.in6 = (struct sockaddr_in6){ + .sin6_family = AF_INET6, + .sin6_port = htons(nm_sock_addr_endpoint_get_port( + _nm_wireguard_peer_get_endpoint(peer_data->peer))), + .sin6_scope_id = 0, + .sin6_flowinfo = 0, + }; + memcpy(&sockaddr.in6.sin6_addr, + g_inet_address_to_bytes(a), + sizeof(struct in6_addr)); + break; + } + } + + g_list_free_full(list, g_object_unref); + } + + if (sockaddr.sa.sa_family == AF_UNSPEC) { + /* we failed to resolve the name. There is no need to reset the previous + * sockaddr. Either it was already AF_UNSPEC, or we had a good name + * from resolving before. In that case, we don't want to throw away + * a possibly good IP address, since WireGuard supports automatic roaming + * anyway. Either the IP address is still good (and we would wrongly + * reject it), or it isn't -- in which case it does not hurt much. */ + } else { + if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) != 0) + changed = TRUE; + peer_data->ep_resolv.sockaddr = sockaddr; + } + + if (resolv_error || peer_data->ep_resolv.sockaddr.sa.sa_family == AF_UNSPEC) { + /* while it technically did not fail, something is probably odd. Retry frequently to + * resolve the name, like we would do for normal failures. */ + retry_in_msec = _peers_retry_in_msec(peer_data, TRUE); + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: no %sresults for endpoint \"%s\" (retry %s)", + nm_wireguard_peer_get_public_key(peer_data->peer), + resolv_error ? "" : "suitable ", + nm_wireguard_peer_get_endpoint(peer_data->peer), + _retry_in_msec_to_string(retry_in_msec, s_retry)); + } else { + retry_in_msec = _peers_retry_in_msec(peer_data, FALSE); + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: endpoint \"%s\" resolved to %s (retry %s)", + nm_wireguard_peer_get_public_key(peer_data->peer), + nm_wireguard_peer_get_endpoint(peer_data->peer), + nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr, + s_sockaddr, + sizeof(s_sockaddr)), + _retry_in_msec_to_string(retry_in_msec, s_retry)); + } + + _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec); + + if (changed) { + /* schedule the job in the background, to give multiple resolve events time + * to complete. */ + nm_clear_g_source(&priv->link_config_delayed_id); + priv->link_config_delayed_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1, + link_config_delayed_resolver_cb, + self, + NULL); + } +} + +static void +_peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gs_unref_object GResolver *resolver = NULL; + const char * host; + + resolver = g_resolver_get_default(); + + nm_assert(!peer_data->ep_resolv.cancellable); + + peer_data->ep_resolv.cancellable = g_cancellable_new(); + priv->peers_resolving_cnt++; + + /* set a special next-try timestamp. It is positive, and indicates + * that we are in the process of trying. + * This timestamp however already lies in the past, but that is correct, + * because we are currently in the process of trying. We will determine + * a next-try timestamp once the try completes. */ + peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_PAST; + + host = nm_sock_addr_endpoint_get_host(_nm_wireguard_peer_get_endpoint(peer_data->peer)); + + g_resolver_lookup_by_name_async(resolver, + host, + peer_data->ep_resolv.cancellable, + _peers_resolve_cb, + peer_data); + + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: resolving name \"%s\" for endpoint \"%s\"...", + nm_wireguard_peer_get_public_key(peer_data->peer), + host, + nm_wireguard_peer_get_endpoint(peer_data->peer)); + + nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt); +} + +static void +_peers_resolve_reresolve_all(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + PeerData * peer_data; + + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + if (peer_data->ep_resolv.cancellable) { + /* remember to retry when the currently ongoing request completes. */ + peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_ASAP; + } else if (peer_data->ep_resolv.next_try_at_nsec <= 0) { + /* this peer does not require resolving the name. Skip it. */ + } else { + /* we have a next-try scheduled. Restart right away. */ + peer_data->ep_resolv.resolv_fail_count = 0; + _peers_resolve_start(self, peer_data); + } + } +} + +static gboolean +_peers_update(NMDeviceWireGuard *self, + PeerData * peer_data, + NMWireGuardPeer * peer, + gboolean force_update) +{ + nm_auto_unref_wgpeer NMWireGuardPeer *old_peer = NULL; + NMSockAddrEndpoint * old_endpoint; + NMSockAddrEndpoint * endpoint; + gboolean endpoint_changed = FALSE; + gboolean changed; + NMSockAddrUnion sockaddr; + gboolean sockaddr_fixed; + char sockaddr_sbuf[100]; + + nm_assert(peer); + nm_assert(nm_wireguard_peer_is_sealed(peer)); + + if (peer == peer_data->peer && !force_update) + return FALSE; + + changed = (nm_wireguard_peer_cmp(peer, peer_data->peer, NM_SETTING_COMPARE_FLAG_EXACT) != 0); + + old_peer = peer_data->peer; + peer_data->peer = nm_wireguard_peer_ref(peer); + + old_endpoint = old_peer ? _nm_wireguard_peer_get_endpoint(old_peer) : NULL; + endpoint = peer ? _nm_wireguard_peer_get_endpoint(peer) : NULL; + + endpoint_changed = (endpoint != old_endpoint + && (!old_endpoint || !endpoint + || !nm_streq(nm_sock_addr_endpoint_get_endpoint(old_endpoint), + nm_sock_addr_endpoint_get_endpoint(endpoint)))); + + if (!force_update && !endpoint_changed) { + /* nothing to do. */ + return changed; + } + + sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC; + sockaddr_fixed = TRUE; + if (endpoint && nm_sock_addr_endpoint_get_host(endpoint)) { + if (!nm_sock_addr_endpoint_get_fixed_sockaddr(endpoint, &sockaddr)) { + /* we have an endpoint, but it's not a static IP address. We need to resolve + * the names. */ + sockaddr_fixed = FALSE; + } + } + + if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) != 0) + changed = TRUE; + + if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable)) + _peers_resolving_cnt_decrement(self); + + peer_data->ep_resolv = (PeerEndpointResolveData){ + .sockaddr = sockaddr, + .resolv_fail_count = 0, + .cancellable = NULL, + .next_try_at_nsec = 0, + }; + + if (!endpoint) { + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: no endpoint configured", + nm_wireguard_peer_get_public_key(peer_data->peer)); + } else if (!nm_sock_addr_endpoint_get_host(endpoint)) { + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: invalid endpoint \"%s\"", + nm_wireguard_peer_get_public_key(peer_data->peer), + nm_sock_addr_endpoint_get_endpoint(endpoint)); + } else if (sockaddr_fixed) { + _LOGT(LOGD_DEVICE, + "wireguard-peer[%s]: fixed endpoint \"%s\" (%s)", + nm_wireguard_peer_get_public_key(peer_data->peer), + nm_sock_addr_endpoint_get_endpoint(endpoint), + nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr, + sockaddr_sbuf, + sizeof(sockaddr_sbuf))); + } else + _peers_resolve_start(self, peer_data); + + return changed; +} + +static void +_peers_remove_all(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + PeerData * peer_data; + + while ((peer_data = c_list_first_entry(&priv->lst_peers_head, PeerData, lst_peers))) + _peers_remove(self, peer_data); +} + +static void +_peers_update_all(NMDeviceWireGuard *self, NMSettingWireGuard *s_wg, gboolean *out_peers_removed) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + PeerData * peer_data_safe; + PeerData * peer_data; + guint i, n; + gboolean peers_removed = FALSE; + + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) + peer_data->dirty_update_all = TRUE; + + n = nm_setting_wireguard_get_peers_len(s_wg); + for (i = 0; i < n; i++) { + NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i); + gboolean added = FALSE; + + peer_data = _peers_find(priv, peer); + if (!peer_data) { + peer_data = _peers_add(self, peer); + added = TRUE; + } + _peers_update(self, peer_data, peer, added); + peer_data->dirty_update_all = FALSE; + } + + c_list_for_each_entry_safe (peer_data, peer_data_safe, &priv->lst_peers_head, lst_peers) { + if (peer_data->dirty_update_all) { + _peers_remove(self, peer_data); + peers_removed = TRUE; + } + } + + NM_SET_OUT(out_peers_removed, peers_removed); +} + +static void +_peers_get_platform_list(NMDeviceWireGuardPrivate * priv, + LinkConfigMode config_mode, + NMPWireGuardPeer ** out_peers, + NMPlatformWireGuardChangePeerFlags **out_peer_flags, + guint * out_len, + GArray ** out_allowed_ips_data) +{ + gs_free NMPWireGuardPeer *plpeers = NULL; + gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL; + gs_unref_array GArray *allowed_ips = NULL; + PeerData * peer_data; + guint i_good; + guint n_aip; + guint i_aip; + guint len; + guint i; + + nm_assert(out_peers && !*out_peers); + nm_assert(out_peer_flags && !*out_peer_flags); + nm_assert(out_len && *out_len == 0); + nm_assert(out_allowed_ips_data && !*out_allowed_ips_data); + + len = g_hash_table_size(priv->peers); + + nm_assert(len == c_list_length(&priv->lst_peers_head)); + + if (len == 0) + return; + + plpeers = g_new0(NMPWireGuardPeer, len); + plpeer_flags = g_new0(NMPlatformWireGuardChangePeerFlags, len); + + i_good = 0; + c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) { + NMPlatformWireGuardChangePeerFlags *plf = &plpeer_flags[i_good]; + NMPWireGuardPeer * plp = &plpeers[i_good]; + NMSettingSecretFlags psk_secret_flags; + + if (!nm_utils_base64secret_decode(nm_wireguard_peer_get_public_key(peer_data->peer), + sizeof(plp->public_key), + plp->public_key)) + continue; + + *plf = NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_NONE; + + plp->persistent_keepalive_interval = + nm_wireguard_peer_get_persistent_keepalive(peer_data->peer); + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_KEEPALIVE_INTERVAL; + + /* if the peer has an endpoint but it is not yet resolved (not ready), + * we still configure it and leave the endpoint unspecified. Later, + * when we can resolve the endpoint, we will update. */ + plp->endpoint = peer_data->ep_resolv.sockaddr; + if (plp->endpoint.sa.sa_family == AF_UNSPEC) { + /* we don't actually ever clear endpoints, if we don't have better information. */ + } else + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ENDPOINT; + + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) { + psk_secret_flags = nm_wireguard_peer_get_preshared_key_flags(peer_data->peer); + if (!NM_FLAGS_HAS(psk_secret_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) { + if (!nm_utils_base64secret_decode( + nm_wireguard_peer_get_preshared_key(peer_data->peer), + sizeof(plp->preshared_key), + plp->preshared_key) + && config_mode == LINK_CONFIG_MODE_FULL) + goto skip; + } + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_PRESHARED_KEY; + } + + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY) + && ((n_aip = nm_wireguard_peer_get_allowed_ips_len(peer_data->peer)) > 0)) { + if (!allowed_ips) + allowed_ips = g_array_new(FALSE, FALSE, sizeof(NMPWireGuardAllowedIP)); + + *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ALLOWEDIPS + | NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_REPLACE_ALLOWEDIPS; + + plp->_construct_idx_start = allowed_ips->len; + for (i_aip = 0; i_aip < n_aip; i_aip++) { + const char *aip; + NMIPAddr addrbin = {}; + int addr_family; + gboolean valid; + int prefix; + + aip = nm_wireguard_peer_get_allowed_ip(peer_data->peer, i_aip, &valid); + if (!valid + || !nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC, + aip, + &addr_family, + &addrbin, + &prefix)) { + /* the address is really not expected to be invalid, because then + * the connection would not verify. Anyway, silently skip it. */ + continue; + } + + if (prefix == -1) + prefix = addr_family == AF_INET ? 32 : 128; + + g_array_append_val(allowed_ips, + ((NMPWireGuardAllowedIP){ + .family = addr_family, + .mask = prefix, + .addr = addrbin, + })); + } + plp->_construct_idx_end = allowed_ips->len; + } + + i_good++; + continue; + +skip: + memset(plp, 0, sizeof(*plp)); + } + + if (i_good == 0) + return; + + for (i = 0; i < i_good; i++) { + NMPWireGuardPeer *plp = &plpeers[i]; + guint l; + + if (plp->_construct_idx_end == 0) { + nm_assert(plp->_construct_idx_start == 0); + plp->allowed_ips = NULL; + plp->allowed_ips_len = 0; + } else { + nm_assert(plp->_construct_idx_start < plp->_construct_idx_end); + l = plp->_construct_idx_end - plp->_construct_idx_start; + plp->allowed_ips = + &g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start); + plp->allowed_ips_len = l; + } + } + *out_peers = g_steal_pointer(&plpeers); + *out_peer_flags = g_steal_pointer(&plpeer_flags); + *out_len = i_good; + *out_allowed_ips_data = g_steal_pointer(&allowed_ips); +} + +/*****************************************************************************/ + +static void +update_properties(NMDevice *device) +{ + NMDeviceWireGuard * self; + NMDeviceWireGuardPrivate * priv; + const NMPlatformLink * plink; + const NMPlatformLnkWireGuard *props = NULL; + int ifindex; + + g_return_if_fail(NM_IS_DEVICE_WIREGUARD(device)); + self = NM_DEVICE_WIREGUARD(device); + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + ifindex = nm_device_get_ifindex(device); + props = nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device), ifindex, &plink); + if (!props) { + _LOGW(LOGD_PLATFORM, "could not get wireguard properties"); + return; + } + + g_object_freeze_notify(G_OBJECT(device)); + +#define CHECK_PROPERTY_CHANGED(field, prop) \ + G_STMT_START \ + { \ + if (priv->lnk_curr.field != props->field) { \ + priv->lnk_curr.field = props->field; \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + +#define CHECK_PROPERTY_CHANGED_ARRAY(field, prop) \ + G_STMT_START \ + { \ + if (memcmp(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field)) != 0) { \ + memcpy(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field)); \ + _notify(self, prop); \ + } \ + } \ + G_STMT_END + + CHECK_PROPERTY_CHANGED_ARRAY(public_key, PROP_PUBLIC_KEY); + CHECK_PROPERTY_CHANGED(listen_port, PROP_LISTEN_PORT); + CHECK_PROPERTY_CHANGED(fwmark, PROP_FWMARK); + + g_object_thaw_notify(G_OBJECT(device)); +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->link_changed(device, pllink); + update_properties(device); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *dev) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +/*****************************************************************************/ + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char *iface = nm_device_get_iface(device); + int r; + + g_return_val_if_fail(iface, FALSE); + + r = nm_platform_link_wireguard_add(nm_device_get_platform(device), iface, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create WireGuard interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +_secrets_cancel(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + if (priv->secrets_call_id) + nm_act_request_cancel_secrets(NULL, priv->secrets_call_id); + nm_assert(!priv->secrets_call_id); +} + +static void +_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * connection, + GError * error, + gpointer user_data) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(user_data); + NMDevice * device = NM_DEVICE(self); + NMDeviceWireGuardPrivate *priv; + + g_return_if_fail(NM_IS_DEVICE_WIREGUARD(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + g_return_if_fail(priv->secrets_call_id == call_id); + + priv->secrets_call_id = NULL; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + g_return_if_fail(nm_act_request_get_settings_connection(req) == connection); + + if (error) { + _LOGW(LOGD_ETHER, "%s", error->message); + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +_secrets_get_secrets(NMDeviceWireGuard * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags, + const char *const * hints) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + NMActRequest * req; + + _secrets_cancel(self); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv->secrets_call_id = + nm_act_request_get_secrets(req, TRUE, setting_name, flags, hints, _secrets_cb, self); + g_return_if_fail(priv->secrets_call_id); +} + +static NMActStageReturn +_secrets_handle_auth_or_fail(NMDeviceWireGuard *self, NMActRequest *req, gboolean new_secrets) +{ + NMConnection * applied_connection; + const char * setting_name; + gs_unref_ptrarray GPtrArray *hints = NULL; + + if (!nm_device_auth_retries_try_next(NM_DEVICE(self))) + return NM_ACT_STAGE_RETURN_FAILURE; + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_NONE); + + nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req)); + + applied_connection = nm_act_request_get_applied_connection(req); + setting_name = nm_connection_need_secrets(applied_connection, &hints); + if (!setting_name) { + _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets."); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (hints) + g_ptr_array_add(hints, NULL); + + _secrets_get_secrets(self, + setting_name, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0), + (hints ? (const char *const *) hints->pdata : NULL)); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static void +_dns_config_changed(NMDnsManager *dns_manager, NMDeviceWireGuard *self) +{ + /* when the DNS configuration changes, we re-resolve the peer addresses. + * + * Possibly, we should also do that when the default-route changes, but it's + * hard to figure out when that happens. */ + _peers_resolve_reresolve_all(self); +} + +/*****************************************************************************/ + +static NMActStageReturn +link_config(NMDeviceWireGuard * self, + const char * reason, + LinkConfigMode config_mode, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWireGuardPrivate * priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + nm_auto_bzero_secret_ptr NMSecretPtr wg_lnk_clear_private_key = NM_SECRET_PTR_INIT(); + NMSettingWireGuard * s_wg; + NMConnection * connection; + NMActStageReturn ret; + gs_unref_array GArray *allowed_ips_data = NULL; + NMPlatformLnkWireGuard wg_lnk; + gs_free NMPWireGuardPeer *plpeers = NULL; + gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL; + guint plpeers_len = 0; + const char * setting_name; + gboolean peers_removed; + NMPlatformWireGuardChangeFlags wg_change_flags; + int ifindex; + int r; + + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD)); + g_return_val_if_fail(s_wg, NM_ACT_STAGE_RETURN_FAILURE); + + priv->link_config_last_at = nm_utils_get_monotonic_timestamp_nsec(); + + _LOGT(LOGD_DEVICE, + "wireguard link config (%s, %s)...", + reason, + _link_config_mode_to_string(config_mode)); + + _auto_default_route_init(self); + + if (!priv->dns_manager) { + priv->dns_manager = g_object_ref(nm_dns_manager_get()); + g_signal_connect(priv->dns_manager, + NM_DNS_MANAGER_CONFIG_CHANGED, + G_CALLBACK(_dns_config_changed), + self); + } + + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL) + && (setting_name = nm_connection_need_secrets(connection, NULL))) { + NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self)); + + _LOGD(LOGD_DEVICE, + "Activation: connection '%s' has security, but secrets are required.", + nm_connection_get_id(connection)); + + ret = _secrets_handle_auth_or_fail(self, req, FALSE); + if (ret != NM_ACT_STAGE_RETURN_SUCCESS) { + if (ret != NM_ACT_STAGE_RETURN_POSTPONE) { + nm_assert(ret == NM_ACT_STAGE_RETURN_FAILURE); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + } + return ret; + } + } + + ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self)); + if (ifindex <= 0) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + _peers_update_all(self, s_wg, &peers_removed); + + wg_lnk = (NMPlatformLnkWireGuard){}; + + wg_change_flags = NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE; + + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL) + || (NM_IN_SET(config_mode, LINK_CONFIG_MODE_REAPPLY) && peers_removed)) + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS; + + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) { + wg_lnk.listen_port = nm_setting_wireguard_get_listen_port(s_wg); + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_LISTEN_PORT; + + wg_lnk.fwmark = priv->auto_default_route_fwmark; + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_FWMARK; + + if (nm_utils_base64secret_decode(nm_setting_wireguard_get_private_key(s_wg), + sizeof(wg_lnk.private_key), + wg_lnk.private_key)) { + wg_lnk_clear_private_key = NM_SECRET_PTR_ARRAY(wg_lnk.private_key); + wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY; + } else { + if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)) { + _LOGD(LOGD_DEVICE, "the provided private-key is invalid"); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + } + + _peers_get_platform_list(priv, + config_mode, + &plpeers, + &plpeer_flags, + &plpeers_len, + &allowed_ips_data); + + r = nm_platform_link_wireguard_change(nm_device_get_platform(NM_DEVICE(self)), + ifindex, + &wg_lnk, + plpeers, + plpeer_flags, + plpeers_len, + wg_change_flags); + + nm_explicit_bzero(plpeers, sizeof(plpeers[0]) * plpeers_len); + + if (r < 0) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +link_config_delayed(NMDeviceWireGuard *self, const char *reason) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gint64 now; + + priv->link_config_delayed_id = 0; + + if (priv->link_config_last_at != 0) { + now = nm_utils_get_monotonic_timestamp_nsec(); + if (now < priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC) { + /* we ratelimit calls to link_config(), because we call this whenever a resolver + * completes. */ + _LOGT(LOGD_DEVICE, "wireguard link config (%s) (postponed)", reason); + priv->link_config_delayed_id = + g_timeout_add(NM_MAX((priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC - now) + / NM_UTILS_NSEC_PER_MSEC, + (gint64) 1), + link_config_delayed_ratelimit_cb, + self); + return; + } + } + + link_config(self, reason, LINK_CONFIG_MODE_ENDPOINTS, NULL); +} + +static gboolean +link_config_delayed_ratelimit_cb(gpointer user_data) +{ + link_config_delayed(user_data, "after-ratelimiting"); + return G_SOURCE_REMOVE; +} + +static gboolean +link_config_delayed_resolver_cb(gpointer user_data) +{ + link_config_delayed(user_data, "resolver-update"); + return G_SOURCE_REMOVE; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(device); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + NMDeviceSysIfaceState sys_iface_state; + NMDeviceStateReason failure_reason; + NMActStageReturn ret; + + sys_iface_state = nm_device_sys_iface_state_get(device); + + if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + ret = + link_config(NM_DEVICE_WIREGUARD(device), + "configure", + (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) ? LINK_CONFIG_MODE_ASSUME + : LINK_CONFIG_MODE_FULL, + &failure_reason); + + if (ret == NM_ACT_STAGE_RETURN_FAILURE) { + if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) { + /* this never fails. */ + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, failure_reason); + NM_SET_OUT(out_failure_reason, failure_reason); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + nm_assert(NM_IN_SET(ret, NM_ACT_STAGE_RETURN_SUCCESS, NM_ACT_STAGE_RETURN_POSTPONE)); + + if (ret == NM_ACT_STAGE_RETURN_SUCCESS && _peers_resolving_cnt(priv) > 0u) { + _LOGT(LOGD_DEVICE, + "activation delayed to resolve DNS names of peers: resolving and waiting..."); + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + return ret; +} + +static NMIPConfig * +_get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gs_unref_object NMIPConfig *ip_config = NULL; + NMConnection * connection; + NMSettingWireGuard * s_wg; + guint n_peers; + guint i; + int ip_ifindex; + guint32 route_metric; + guint32 route_table_coerced; + gboolean auto_default_route_enabled; + + _auto_default_route_init(self); + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD)); + + /* Differences to `wg-quick`. + * + * `wg-quick` supports the "Table" setting with 3 modes: + * + * a1) "off": this is what we do with "peer-routes" disabled. + * + * a2) an explicit routing table. This is our behavior with "peer-routes" on. In this case + * we honor the "ipv4.route-table" and "ipv6.route-table" settings. One difference is that + * `wg-quick` would resolve table names from /etc/iproute2/rt_tables. Our connection profiles + * only contain table numbers, so that conversion from name to table must have happened + * before already. + * + * a3) "auto" (the default). In this case, `wg-quick` would only add the route to the + * main table, if the AllowedIP range is not yet reachable on the link. With "peer-routes" + * enabled, we don't check for that and always add the routes to the main-table + * (with 'ipv4.route-table' and 'ipv6.route-table' set to zero or RT_TABLE_MAIN (254)). + * + * Also, in "auto" mode, `wg-quick` would add special handling for /0 routes and pick + * an empty table to configure policy routing to avoid routing loops. This handling + * of routing-loops via policy routing is not yet done, and requires a separate solution + * from constructing the peer-routes here. + */ + if (!nm_setting_wireguard_get_peer_routes(s_wg)) + return NULL; + + ip_ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self)); + + if (ip_ifindex <= 0) + return NULL; + + route_metric = nm_device_get_route_metric(NM_DEVICE(self), addr_family); + + route_table_coerced = + nm_platform_route_table_coerce(nm_device_get_route_table(NM_DEVICE(self), addr_family)); + + auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4 + : priv->auto_default_route_enabled_6; + + n_peers = nm_setting_wireguard_get_peers_len(s_wg); + for (i = 0; i < n_peers; i++) { + NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i); + guint n_aips; + guint j; + + n_aips = nm_wireguard_peer_get_allowed_ips_len(peer); + for (j = 0; j < n_aips; j++) { + NMPlatformIPXRoute rt; + NMIPAddr addrbin; + const char * aip; + gboolean valid; + int prefix; + guint32 rtable_coerced; + + aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid); + + if (!valid + || !nm_utils_parse_inaddr_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix)) + continue; + + if (prefix < 0) + prefix = (addr_family == AF_INET) ? 32 : 128; + + if (prefix == 0) { + NMSettingIPConfig *s_ip; + + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + if (nm_setting_ip_config_get_never_default(s_ip)) + continue; + } + + if (!ip_config) { + ip_config = nm_device_ip_config_new(NM_DEVICE(self), addr_family); + nm_ip_config_set_config_flags(ip_config, + NM_IP_CONFIG_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES, + 0); + } + + nm_utils_ipx_address_clear_host_address(addr_family, &addrbin, NULL, prefix); + + rtable_coerced = route_table_coerced; + + if (prefix == 0 && auto_default_route_enabled) { + /* In auto-default-route mode, we place the default route in a table that + * has the same number as the fwmark. wg-quick does that too. If you don't + * like that, configure the rules and the default-route explicitly in the + * connection profile. */ + rtable_coerced = nm_platform_route_table_coerce(priv->auto_default_route_fwmark); + } + + if (addr_family == AF_INET) { + rt.r4 = (NMPlatformIP4Route){ + .network = addrbin.addr4, + .plen = prefix, + .ifindex = ip_ifindex, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .table_coerced = rtable_coerced, + .metric = route_metric, + }; + } else { + rt.r6 = (NMPlatformIP6Route){ + .network = addrbin.addr6, + .plen = prefix, + .ifindex = ip_ifindex, + .rt_source = NM_IP_CONFIG_SOURCE_USER, + .table_coerced = rtable_coerced, + .metric = route_metric, + }; + } + + nm_ip_config_add_route(ip_config, &rt.rx, NULL); + } + } + + return g_steal_pointer(&ip_config); +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + gs_unref_object NMIPConfig *ip_config = NULL; + + ip_config = _get_dev2_ip_config(NM_DEVICE_WIREGUARD(device), addr_family); + + nm_device_set_dev2_ip_config(device, addr_family, ip_config); + + return NM_DEVICE_CLASS(nm_device_wireguard_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + /* When "MTU" for `wg-quick up` is unset, it calls `ip route get` for + * each configured endpoint, to determine the suitable MTU how to reach + * each endpoint. + * For `wg-quick` this works very well, because whenever the script runs it + * determines the best setting at that point in time. It's simply not concerned + * with what happens later (and it's not around anyway). + * + * NetworkManager sticks around, so the right MTU would need to be re-determined + * whenever anything relevant changes. Which basically means, to re-evaluate whenever + * something related to addresses or routing changes (which happens all the time). + * + * The correct MTU indeed depends on the MTU setting of other interfaces (or routes). + * But it's still odd, that activating/deactivating a seemingly unrelated interface + * would trigger an MTU change. It's odd to explain/document and odd to implemented + * -- despite this being the reality. + * + * For now, only support configuring an explicit MTU, or leave the setting untouched. + * The same limitation also applies to other "ip-tunnel" types, where we could use + * similar smarts for autodetecting the MTU. + */ + return nm_device_get_configured_mtu_from_connection(device, + NM_TYPE_SETTING_WIREGUARD, + out_source); +} + +static void +_device_cleanup(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + _peers_remove_all(self); + + _secrets_cancel(self); + + priv->auto_default_route_initialized = FALSE; + priv->auto_default_route_priority_initialized = FALSE; +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + if (new_state <= NM_DEVICE_STATE_ACTIVATED) + return; + + _device_cleanup(NM_DEVICE_WIREGUARD(device)); +} + +/*****************************************************************************/ + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + if (nm_streq(setting_name, NM_SETTING_WIREGUARD_SETTING_NAME)) { + /* Most, but not all WireGuard settings can be reapplied. Whitelist. + * + * MTU cannot be reapplied. */ + return nm_device_hash_check_invalid_keys(diffs, + NM_SETTING_WIREGUARD_SETTING_NAME, + error, + NM_SETTING_WIREGUARD_FWMARK, + NM_SETTING_WIREGUARD_IP4_AUTO_DEFAULT_ROUTE, + NM_SETTING_WIREGUARD_IP6_AUTO_DEFAULT_ROUTE, + NM_SETTING_WIREGUARD_LISTEN_PORT, + NM_SETTING_WIREGUARD_PEERS, + NM_SETTING_WIREGUARD_PEER_ROUTES, + NM_SETTING_WIREGUARD_PRIVATE_KEY, + NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS); + } + + return NM_DEVICE_CLASS(nm_device_wireguard_parent_class) + ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static void +reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(device); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + gs_unref_object NMIPConfig *ip4_config = NULL; + gs_unref_object NMIPConfig *ip6_config = NULL; + NMDeviceState state = nm_device_get_state(device); + + NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->reapply_connection(device, con_old, con_new); + + if (state >= NM_DEVICE_STATE_CONFIG) { + priv->auto_default_route_refresh = TRUE; + link_config(NM_DEVICE_WIREGUARD(device), "reapply", LINK_CONFIG_MODE_REAPPLY, NULL); + } + + if (state >= NM_DEVICE_STATE_IP_CONFIG) { + ip4_config = _get_dev2_ip_config(self, AF_INET); + ip6_config = _get_dev2_ip_config(self, AF_INET6); + + nm_device_set_dev2_ip_config(device, AF_INET, ip4_config); + nm_device_set_dev2_ip_config(device, AF_INET6, ip6_config); + } +} + +/*****************************************************************************/ + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(device); + NMSettingWireGuard * s_wg = + NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD)); + const NMPObject * obj_wg; + const NMPObjectLnkWireGuard *olnk_wg; + guint i; + + if (!s_wg) { + s_wg = NM_SETTING_WIREGUARD(nm_setting_wireguard_new()); + nm_connection_add_setting(connection, NM_SETTING(s_wg)); + } + + g_object_set(s_wg, + NM_SETTING_WIREGUARD_FWMARK, + (guint) priv->lnk_curr.fwmark, + NM_SETTING_WIREGUARD_LISTEN_PORT, + (guint) priv->lnk_curr.listen_port, + NULL); + + obj_wg = NMP_OBJECT_UP_CAST(nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + NULL)); + if (!obj_wg) + return; + + olnk_wg = &obj_wg->_lnk_wireguard; + + for (i = 0; i < olnk_wg->peers_len; i++) { + nm_auto_unref_wgpeer NMWireGuardPeer *peer = NULL; + const NMPWireGuardPeer * ppeer = &olnk_wg->peers[i]; + + peer = nm_wireguard_peer_new(); + + _nm_wireguard_peer_set_public_key_bin(peer, ppeer->public_key); + + nm_setting_wireguard_append_peer(s_wg, peer); + } +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(object); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_PUBLIC_KEY: + g_value_take_variant(value, + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, + priv->lnk_curr.public_key, + sizeof(priv->lnk_curr.public_key), + 1)); + break; + case PROP_LISTEN_PORT: + g_value_set_uint(value, priv->lnk_curr.listen_port); + break; + case PROP_FWMARK: + g_value_set_uint(value, priv->lnk_curr.fwmark); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_wireguard_init(NMDeviceWireGuard *self) +{ + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + c_list_init(&priv->lst_peers_head); + priv->peers = g_hash_table_new(_peer_data_hash, _peer_data_equal); +} + +static void +dispose(GObject *object) +{ + NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD(object); + + _device_cleanup(self); + + G_OBJECT_CLASS(nm_device_wireguard_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMDeviceWireGuard * self = NM_DEVICE_WIREGUARD(object); + NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self); + + nm_explicit_bzero(priv->lnk_curr.private_key, sizeof(priv->lnk_curr.private_key)); + + if (priv->dns_manager) { + g_signal_handlers_disconnect_by_func(priv->dns_manager, _dns_config_changed, self); + g_object_unref(priv->dns_manager); + } + + g_hash_table_destroy(priv->peers); + + G_OBJECT_CLASS(nm_device_wireguard_parent_class)->finalize(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_wireguard = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_WIREGUARD, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("PublicKey", + "ay", + NM_DEVICE_WIREGUARD_PUBLIC_KEY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ListenPort", + "q", + NM_DEVICE_WIREGUARD_LISTEN_PORT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FwMark", + "u", + NM_DEVICE_WIREGUARD_FWMARK), ), ), +}; + +static void +nm_device_wireguard_class_init(NMDeviceWireGuardClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wireguard); + + device_class->connection_type_supported = NM_SETTING_WIREGUARD_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WIREGUARD_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD); + + device_class->state_changed = device_state_changed; + device_class->create_and_realize = create_and_realize; + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage2_config_also_for_external_or_assume = TRUE; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->link_changed = link_changed; + device_class->update_connection = update_connection; + device_class->can_reapply_change = can_reapply_change; + device_class->reapply_connection = reapply_connection; + device_class->get_configured_mtu = get_configured_mtu; + device_class->get_extra_rules = get_extra_rules; + device_class->coerce_route_table = coerce_route_table; + + obj_properties[PROP_PUBLIC_KEY] = + g_param_spec_variant(NM_DEVICE_WIREGUARD_PUBLIC_KEY, + "", + "", + G_VARIANT_TYPE("ay"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LISTEN_PORT] = g_param_spec_uint(NM_DEVICE_WIREGUARD_LISTEN_PORT, + "", + "", + 0, + G_MAXUINT16, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_FWMARK] = g_param_spec_uint(NM_DEVICE_WIREGUARD_FWMARK, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*************************************************************/ + +#define NM_TYPE_WIREGUARD_DEVICE_FACTORY (nm_wireguard_device_factory_get_type()) +#define NM_WIREGUARD_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIREGUARD_DEVICE_FACTORY, NMWireGuardDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_WIREGUARD, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "WireGuard", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_WIREGUARD, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_WIREGUARD, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + WIREGUARD, + WireGuard, + wireguard, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIREGUARD_SETTING_NAME), + factory_class->create_device = create_device;) diff --git a/src/core/devices/nm-device-wireguard.h b/src/core/devices/nm-device-wireguard.h new file mode 100644 index 00000000..7e18bdba --- /dev/null +++ b/src/core/devices/nm-device-wireguard.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Javier Arteaga <jarteaga@jbeta.is> + */ + +#ifndef __NM_DEVICE_WIREGUARD_H__ +#define __NM_DEVICE_WIREGUARD_H__ + +#include "nm-device.h" + +#define NM_TYPE_DEVICE_WIREGUARD (nm_device_wireguard_get_type()) +#define NM_DEVICE_WIREGUARD(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuard)) +#define NM_DEVICE_WIREGUARD_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuardClass)) +#define NM_IS_DEVICE_WIREGUARD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIREGUARD)) +#define NM_IS_DEVICE_WIREGUARD_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIREGUARD)) +#define NM_DEVICE_WIREGUARD_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuardClass)) + +#define NM_DEVICE_WIREGUARD_PUBLIC_KEY "public-key" +#define NM_DEVICE_WIREGUARD_LISTEN_PORT "listen-port" +#define NM_DEVICE_WIREGUARD_FWMARK "fwmark" + +typedef struct _NMDeviceWireGuard NMDeviceWireGuard; +typedef struct _NMDeviceWireGuardClass NMDeviceWireGuardClass; + +GType nm_device_wireguard_get_type(void); + +#endif /* __NM_DEVICE_WIREGUARD_H__ */ diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c new file mode 100644 index 00000000..2f3b16ff --- /dev/null +++ b/src/core/devices/nm-device-wpan.c @@ -0,0 +1,257 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk> + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-manager.h" +#include "nm-device-wpan.h" + +#include <stdlib.h> +#include <sys/types.h> +#include <linux/if.h> + +#include "nm-act-request.h" +#include "nm-device-private.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" +#include "nm-device-factory.h" +#include "nm-setting-wpan.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceWpan +#include "nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceWpan { + NMDevice parent; +}; + +struct _NMDeviceWpanClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceWpan, nm_device_wpan, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingWpan *s_wpan; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_WPAN_SETTING_NAME, + existing_connections, + NULL, + _("WPAN connection"), + NULL, + NULL, + TRUE); + + s_wpan = NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN)); + if (!s_wpan) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'wpan' setting is required."); + return FALSE; + } + + return TRUE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMSettingWpan *s_wpan = + NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN)); + + if (!s_wpan) { + s_wpan = (NMSettingWpan *) nm_setting_wpan_new(); + nm_connection_add_setting(connection, (NMSetting *) s_wpan); + } +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingWpan *s_wpan; + const char * mac, *hw_addr; + + if (!NM_DEVICE_CLASS(nm_device_wpan_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_wpan = NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN)); + + mac = nm_setting_wpan_get_mac_address(s_wpan); + if (mac) { + hw_addr = nm_device_get_hw_address(device); + if (!nm_utils_hwaddr_matches(mac, -1, hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "MAC address mismatches"); + return FALSE; + } + } + + return TRUE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWpan * self = NM_DEVICE_WPAN(device); + NMSettingWpan * s_wpan; + NMPlatform * platform; + guint16 pan_id; + guint16 short_address; + gint16 page, channel; + int ifindex; + const guint8 * hwaddr; + gsize hwaddr_len = 0; + const NMPlatformLink *lowpan_plink; + NMDevice * lowpan_device = NULL; + NMActStageReturn ret = NM_ACT_STAGE_RETURN_FAILURE; + + platform = nm_device_get_platform(device); + nm_assert(NM_IS_PLATFORM(platform)); + + ifindex = nm_device_get_ifindex(device); + + g_return_val_if_fail(ifindex > 0, NM_ACT_STAGE_RETURN_FAILURE); + + s_wpan = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WPAN); + + g_return_val_if_fail(s_wpan, NM_ACT_STAGE_RETURN_FAILURE); + + hwaddr = nm_platform_link_get_address(platform, ifindex, &hwaddr_len); + + if (!hwaddr) { + *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + /* As of kernel 4.16, the 6LoWPAN devices layered on top of WPANs + * need to be DOWN as well as the WPAN device itself in order to + * modify the WPAN properties. */ + lowpan_plink = + nm_platform_link_get_by_address(platform, NM_LINK_TYPE_6LOWPAN, hwaddr, hwaddr_len); + if (lowpan_plink && NM_FLAGS_HAS(lowpan_plink->n_ifi_flags, IFF_UP)) { + lowpan_device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, lowpan_plink->ifindex); + } + + if (lowpan_device) + nm_device_take_down(lowpan_device, TRUE); + + nm_device_take_down(device, TRUE); + + pan_id = nm_setting_wpan_get_pan_id(s_wpan); + if (pan_id != G_MAXUINT16) { + if (!nm_platform_wpan_set_pan_id(platform, ifindex, pan_id)) { + _LOGW(LOGD_DEVICE, "unable to set the PAN ID"); + goto out; + } + } + + short_address = nm_setting_wpan_get_short_address(s_wpan); + if (short_address != G_MAXUINT16) { + if (!nm_platform_wpan_set_short_addr(platform, ifindex, short_address)) { + _LOGW(LOGD_DEVICE, "unable to set the short address"); + goto out; + } + } + + channel = nm_setting_wpan_get_channel(s_wpan); + if (channel != NM_SETTING_WPAN_CHANNEL_DEFAULT) { + page = nm_setting_wpan_get_page(s_wpan); + if (!nm_platform_wpan_set_channel(platform, ifindex, page, channel)) { + _LOGW(LOGD_DEVICE, "unable to set the channel"); + goto out; + } + } + + ret = NM_ACT_STAGE_RETURN_SUCCESS; + +out: + nm_device_bring_up(device, TRUE, NULL); + + if (lowpan_device) + nm_device_bring_up(lowpan_device, TRUE, NULL); + + return ret; +} + +/*****************************************************************************/ + +static void +nm_device_wpan_init(NMDeviceWpan *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_wpan = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_WPAN, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), ), ), +}; + +static void +nm_device_wpan_class_init(NMDeviceWpanClass *klass) +{ + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wpan); + + device_class->connection_type_supported = NM_SETTING_WPAN_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WPAN_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WPAN); + + device_class->complete_connection = complete_connection; + device_class->check_connection_compatible = check_connection_compatible; + device_class->update_connection = update_connection; + device_class->act_stage1_prepare = act_stage1_prepare; +} + +/*****************************************************************************/ + +#define NM_TYPE_WPAN_DEVICE_FACTORY (nm_wpan_device_factory_get_type()) +#define NM_WPAN_DEVICE_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_DEVICE_FACTORY, NMWpanDeviceFactory)) + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return g_object_new(NM_TYPE_DEVICE_WPAN, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "WPAN", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_WPAN, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_WPAN, + NULL); +} + +NM_DEVICE_FACTORY_DEFINE_INTERNAL( + WPAN, + Wpan, + wpan, + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WPAN) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WPAN_SETTING_NAME), + factory_class->create_device = create_device;); diff --git a/src/core/devices/nm-device-wpan.h b/src/core/devices/nm-device-wpan.h new file mode 100644 index 00000000..969929ae --- /dev/null +++ b/src/core/devices/nm-device-wpan.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk> + */ + +#ifndef __NETWORKMANAGER_DEVICE_WPAN_H__ +#define __NETWORKMANAGER_DEVICE_WPAN_H__ + +#define NM_TYPE_DEVICE_WPAN (nm_device_wpan_get_type()) +#define NM_DEVICE_WPAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpan)) +#define NM_DEVICE_WPAN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WPAN, NMDeviceWpanClass)) +#define NM_IS_DEVICE_WPAN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WPAN)) +#define NM_IS_DEVICE_WPAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WPAN)) +#define NM_DEVICE_WPAN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpanClass)) + +typedef struct _NMDeviceWpan NMDeviceWpan; +typedef struct _NMDeviceWpanClass NMDeviceWpanClass; + +GType nm_device_wpan_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_WPAN_H__ */ diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c new file mode 100644 index 00000000..040dd0b4 --- /dev/null +++ b/src/core/devices/nm-device.c @@ -0,0 +1,19022 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2018 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device.h" + +#include <unistd.h> +#include <sys/ioctl.h> +#include <signal.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <arpa/inet.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/if_ether.h> +#include <linux/if.h> +#include <linux/if_addr.h> +#include <linux/rtnetlink.h> +#include <linux/if_ether.h> +#include <linux/if_infiniband.h> + +#include "nm-std-aux/unaligned.h" +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-glib-aux/nm-random-utils.h" +#include "systemd/nm-sd-utils-shared.h" + +#include "nm-base/nm-ethtool-base.h" +#include "nm-libnm-core-intern/nm-common-macros.h" +#include "nm-device-private.h" +#include "nm-l3cfg.h" +#include "nm-l3-config-data.h" +#include "NetworkManagerUtils.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" +#include "nm-platform/nm-platform-utils.h" +#include "platform/nmp-object.h" +#include "platform/nmp-rules-manager.h" +#include "ndisc/nm-ndisc.h" +#include "ndisc/nm-lndp-ndisc.h" +#include "dhcp/nm-dhcp-manager.h" +#include "dhcp/nm-dhcp-utils.h" +#include "nm-act-request.h" +#include "nm-proxy-config.h" +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" +#include "nm-pacrunner-manager.h" +#include "dnsmasq/nm-dnsmasq-manager.h" +#include "nm-dhcp-config.h" +#include "nm-rfkill-manager.h" +#include "nm-firewall-manager.h" +#include "settings/nm-settings-connection.h" +#include "settings/nm-settings.h" +#include "nm-setting-ethtool.h" +#include "nm-setting-ovs-external-ids.h" +#include "nm-setting-user.h" +#include "nm-auth-utils.h" +#include "nm-keep-alive.h" +#include "nm-netns.h" +#include "nm-dispatcher.h" +#include "nm-config.h" +#include "c-list/src/c-list.h" +#include "dns/nm-dns-manager.h" +#include "nm-acd-manager.h" +#include "nm-core-internal.h" +#include "systemd/nm-sd.h" +#include "nm-lldp-listener.h" +#include "nm-audit-manager.h" +#include "nm-connectivity.h" +#include "nm-dbus-interface.h" + +#include "nm-device-generic.h" +#include "nm-device-vlan.h" +#include "nm-device-vrf.h" +#include "nm-device-wireguard.h" + +#include "nm-device-logging.h" + +/*****************************************************************************/ + +#define DEFAULT_AUTOCONNECT TRUE + +static guint32 +dhcp_grace_period_from_timeout(guint32 timeout) +{ +#define DHCP_GRACE_PERIOD_MULTIPLIER 2U + + nm_assert(timeout > 0); + nm_assert(timeout < G_MAXINT32); + + if (timeout < G_MAXUINT32 / DHCP_GRACE_PERIOD_MULTIPLIER) + return timeout * DHCP_GRACE_PERIOD_MULTIPLIER; + + return G_MAXUINT32; +} + +#define CARRIER_WAIT_TIME_MS 6000 +#define CARRIER_WAIT_TIME_AFTER_MTU_MS 10000 + +#define NM_DEVICE_AUTH_RETRIES_UNSET -1 +#define NM_DEVICE_AUTH_RETRIES_INFINITY -2 +#define NM_DEVICE_AUTH_RETRIES_DEFAULT 3 + +/*****************************************************************************/ + +typedef void (*ActivationHandleFunc)(NMDevice *self); + +typedef enum { + CLEANUP_TYPE_KEEP, + CLEANUP_TYPE_REMOVED, + CLEANUP_TYPE_DECONFIGURE, +} CleanupType; + +typedef struct { + CList lst_slave; + NMDevice *slave; + gulong watch_id; + bool slave_is_enslaved; + bool configure; +} SlaveInfo; + +typedef struct { + NMDevice *device; + guint idle_add_id; + int ifindex; +} DeleteOnDeactivateData; + +typedef struct { + NMDevice * device; + GCancellable * cancellable; + NMPlatformAsyncCallback callback; + gpointer callback_data; + guint num_vfs; + NMOptionBool autoprobe; +} SriovOp; + +typedef void (*AcdCallback)(NMDevice *, NMIP4Config **, gboolean); + +typedef enum { + /* The various NML3ConfigData types that we track explicitly. Note that + * their relative order matters: higher numbers in this enum means more + * important (and during merge overwrites other settings). */ + L3_CONFIG_DATA_TYPE_LL_4, + L3_CONFIG_DATA_TYPE_AC_6, + L3_CONFIG_DATA_TYPE_DHCP_4, + L3_CONFIG_DATA_TYPE_DHCP_6, + L3_CONFIG_DATA_TYPE_DEV_4, + L3_CONFIG_DATA_TYPE_DEV_6, + L3_CONFIG_DATA_TYPE_SETTING, + _L3_CONFIG_DATA_TYPE_NUM, + _L3_CONFIG_DATA_TYPE_NONE, +} L3ConfigDataType; + +typedef struct { + AcdCallback callback; + NMDevice * device; + NMIP4Config **configs; +} AcdData; + +typedef enum { + HW_ADDR_TYPE_UNSET = 0, + HW_ADDR_TYPE_PERMANENT, + HW_ADDR_TYPE_EXPLICIT, + HW_ADDR_TYPE_GENERATED, +} HwAddrType; + +typedef enum { + FIREWALL_STATE_UNMANAGED = 0, + FIREWALL_STATE_INITIALIZED, + FIREWALL_STATE_WAIT_STAGE_3, + FIREWALL_STATE_WAIT_IP_CONFIG, +} FirewallState; + +typedef struct { + NMIPConfig *orig; /* the original configuration applied to the device */ + NMIPConfig *current; /* configuration after external changes. NULL means + * that the original configuration didn't change. */ +} AppliedConfig; + +typedef struct { + NMDhcpClient *client; + NMDhcpConfig *config; + gulong state_sigid; + guint grace_id; + bool grace_pending : 1; + bool was_active : 1; +} DhcpData; + +struct _NMDeviceConnectivityHandle { + CList concheck_lst; + NMDevice * self; + NMDeviceConnectivityCallback callback; + gpointer user_data; + NMConnectivityCheckHandle * c_handle; + guint64 seq; + bool is_periodic : 1; + bool is_periodic_bump : 1; + bool is_periodic_bump_on_complete : 1; + int addr_family; +}; + +typedef struct { + int ifindex; + NMEthtoolFeatureStates *features; + NMOptionBool requested[_NM_ETHTOOL_ID_FEATURE_NUM]; + NMEthtoolCoalesceState *coalesce; + NMEthtoolRingState * ring; +} EthtoolState; + +typedef enum { + RESOLVER_WAIT_ADDRESS = 0, + RESOLVER_IN_PROGRESS, + RESOLVER_DONE, +} ResolverState; + +typedef struct { + ResolverState state; + GResolver * resolver; + GInetAddress *address; + GCancellable *cancellable; + char * hostname; + NMDevice * device; + guint timeout_id; /* Used when waiting for the address */ + int addr_family; +} HostnameResolver; + +/*****************************************************************************/ + +enum { + STATE_CHANGED, + AUTOCONNECT_ALLOWED, + IP4_CONFIG_CHANGED, + IP6_CONFIG_CHANGED, + IP6_PREFIX_DELEGATED, + IP6_SUBNET_NEEDED, + REMOVED, + RECHECK_AUTO_ACTIVATE, + RECHECK_ASSUME, + DNS_LOOKUP_DONE, + LAST_SIGNAL, +}; +static guint signals[LAST_SIGNAL] = {0}; + +NM_GOBJECT_PROPERTIES_DEFINE(NMDevice, + PROP_UDI, + PROP_PATH, + PROP_IFACE, + PROP_IP_IFACE, + PROP_DRIVER, + PROP_DRIVER_VERSION, + PROP_FIRMWARE_VERSION, + PROP_CAPABILITIES, + PROP_CARRIER, + PROP_MTU, + PROP_IP4_ADDRESS, + PROP_IP4_CONFIG, + PROP_DHCP4_CONFIG, + PROP_IP6_CONFIG, + PROP_DHCP6_CONFIG, + PROP_STATE, + PROP_STATE_REASON, + PROP_ACTIVE_CONNECTION, + PROP_DEVICE_TYPE, + PROP_LINK_TYPE, + PROP_MANAGED, + PROP_AUTOCONNECT, + PROP_FIRMWARE_MISSING, + PROP_NM_PLUGIN_MISSING, + PROP_TYPE_DESC, + PROP_RFKILL_TYPE, + PROP_IFINDEX, + PROP_AVAILABLE_CONNECTIONS, + PROP_PHYSICAL_PORT_ID, + PROP_MASTER, + PROP_PARENT, + PROP_HW_ADDRESS, + PROP_PERM_HW_ADDRESS, + PROP_HAS_PENDING_ACTION, + PROP_METERED, + PROP_LLDP_NEIGHBORS, + PROP_REAL, + PROP_SLAVES, + PROP_STATISTICS_REFRESH_RATE_MS, + PROP_STATISTICS_TX_BYTES, + PROP_STATISTICS_RX_BYTES, + PROP_IP4_CONNECTIVITY, + PROP_IP6_CONNECTIVITY, + PROP_INTERFACE_FLAGS, ); + +typedef struct _NMDevicePrivate { + bool in_state_changed; + + guint device_link_changed_id; + guint device_ip_link_changed_id; + + NMDeviceState state; + NMDeviceStateReason state_reason; + struct { + guint id; + + /* The @state/@reason is only valid, when @id is set. */ + NMDeviceState state; + NMDeviceStateReason reason; + } queued_state; + + union { + struct { + guint queued_ip_config_id_6; + guint queued_ip_config_id_4; + }; + guint queued_ip_config_id_x[2]; + }; + + GSList *pending_actions; + GSList *dad6_failed_addrs; + + NMDBusTrackObjPath parent_device; + + char *udi; + char *path; + + union { + const char *const iface; + char * iface_; + }; + union { + const char *const ip_iface; + char * ip_iface_; + }; + + union { + const int ifindex; + int ifindex_; + }; + union { + const int ip_ifindex; + int ip_ifindex_; + }; + + NMNetnsSharedIPHandle *shared_ip_handle; + + int parent_ifindex; + + int auth_retries; + + union { + struct { + HostnameResolver *hostname_resolver_6; + HostnameResolver *hostname_resolver_4; + }; + HostnameResolver *hostname_resolver_x[2]; + }; + + union { + const guint8 hw_addr_len; /* read-only */ + guint8 hw_addr_len_; + }; + + HwAddrType hw_addr_type : 5; + + bool real : 1; + + bool update_ip_config_completed_v4 : 1; + bool update_ip_config_completed_v6 : 1; + + NMDeviceType type; + char * type_desc; + NMLinkType link_type; + NMDeviceCapabilities capabilities; + char * driver; + char * driver_version; + char * firmware_version; + RfKillType rfkill_type; + bool firmware_missing : 1; + bool nm_plugin_missing : 1; + bool + hw_addr_perm_fake : 1; /* whether the permanent HW address could not be read and is a fake */ + + NMUtilsStableType current_stable_id_type : 3; + + bool nm_owned : 1; /* whether the device is a device owned and created by NM */ + + bool assume_state_guess_assume : 1; + char *assume_state_connection_uuid; + + guint64 udi_id; + + GHashTable *available_connections; + char * hw_addr; + char * hw_addr_perm; + char * hw_addr_initial; + char * physical_port_id; + guint dev_id; + + NMUnmanagedFlags unmanaged_mask; + NMUnmanagedFlags unmanaged_flags; + DeleteOnDeactivateData + *delete_on_deactivate_data; /* data for scheduled cleanup when deleting link (g_idle_add) */ + + GCancellable *deactivating_cancellable; + + NMActRequest * queued_act_request; + bool queued_act_request_is_waiting_for_carrier : 1; + NMDBusTrackObjPath act_request; + + union { + struct { + guint activation_source_id_6; + guint activation_source_id_4; /* for layer2 and IPv4. */ + }; + guint activation_source_id_x[2]; + }; + + union { + struct { + ActivationHandleFunc activation_source_func_6; + ActivationHandleFunc activation_source_func_4; /* for layer2 and IPv4. */ + }; + ActivationHandleFunc activation_source_func_x[2]; + }; + + guint recheck_assume_id; + + struct { + guint call_id; + NMDeviceStateReason available_reason; + NMDeviceStateReason unavailable_reason; + } recheck_available; + + struct { + NMDispatcherCallId *call_id; + NMDeviceState post_state; + NMDeviceStateReason post_state_reason; + } dispatcher; + + /* Link stuff */ + guint link_connected_id; + guint link_disconnected_id; + guint carrier_defer_id; + guint carrier_wait_id; + gulong config_changed_id; + gulong ifindex_changed_id; + guint32 mtu; + guint32 ip6_mtu; + guint32 mtu_initial; + guint32 ip6_mtu_initial; + NMDeviceMtuSource mtu_source; + + guint32 v4_route_table; + guint32 v6_route_table; + + /* when carrier goes away, we give a grace period of _get_carrier_wait_ms() + * until taking action. + * + * When changing MTU, the device might take longer then that. So, whenever + * NM changes the MTU it sets @carrier_wait_until_ms to CARRIER_WAIT_TIME_AFTER_MTU_MS + * in the future. This is used to extend the grace period in this particular case. */ + gint64 carrier_wait_until_ms; + + union { + const NMDeviceSysIfaceState sys_iface_state; + NMDeviceSysIfaceState sys_iface_state_; + }; + + bool carrier : 1; + bool ignore_carrier : 1; + + bool up : 1; /* IFF_UP */ + + bool v4_commit_first_time : 1; + bool v6_commit_first_time : 1; + + bool default_route_metric_penalty_ip4_has : 1; + bool default_route_metric_penalty_ip6_has : 1; + + bool v4_route_table_initialized : 1; + bool v6_route_table_initialized : 1; + + bool l3config_merge_flags_has : 1; + + bool v4_route_table_all_sync_before : 1; + bool v6_route_table_all_sync_before : 1; + + NMDeviceAutoconnectBlockedFlags autoconnect_blocked_flags : 5; + + bool is_enslaved : 1; + + bool ipv6ll_handle : 1; /* TRUE if NM handles the device's IPv6LL address */ + bool ipv6ll_has : 1; + bool ndisc_started : 1; + bool device_link_changed_down : 1; + + bool concheck_rp_filter_checked : 1; + + NMDeviceStageState stage1_sriov_state : 3; + + /* Generic DHCP stuff */ + char *dhcp_anycast_address; + + char *current_stable_id; + + /* Proxy Configuration */ + NMProxyConfig * proxy_config; + NMPacrunnerConfId *pacrunner_conf_id; + + /* IP configuration info. Combined config from VPN, settings, and device */ + union { + struct { + NMIP6Config *ip_config_6; + NMIP4Config *ip_config_4; + }; + NMIPConfig *ip_config_x[2]; + }; + + /* Config from DHCP, PPP, LLv4, etc */ + AppliedConfig dev_ip_config_4; + + /* config from the setting */ + union { + struct { + NMIP6Config *con_ip_config_6; + NMIP4Config *con_ip_config_4; + }; + NMIPConfig *con_ip_config_x[2]; + }; + + /* Stuff added outside NM */ + union { + struct { + NMIP6Config *ext_ip_config_6; + NMIP4Config *ext_ip_config_4; + }; + NMIPConfig *ext_ip_config_x[2]; + }; + + /* VPNs which use this device */ + union { + struct { + GSList *vpn_configs_6; + GSList *vpn_configs_4; + }; + GSList *vpn_configs_x[2]; + }; + + /* Extra device configuration, injected by the subclass of NMDevice. + * This is used for example by NMDeviceModem for WWAN configuration. */ + union { + struct { + AppliedConfig dev2_ip_config_6; + AppliedConfig dev2_ip_config_4; + }; + AppliedConfig dev2_ip_config_x[2]; + }; + + /* DHCPv4 tracking */ + struct { + char *pac_url; + } dhcp4; + + struct { + /* IP6 config from DHCP */ + AppliedConfig ip6_config; + /* Event ID of the current IP6 config from DHCP */ + char * event_id; + gulong prefix_sigid; + NMNDiscDHCPLevel mode; + guint needed_prefixes; + } dhcp6; + + union { + struct { + DhcpData dhcp_data_6; + DhcpData dhcp_data_4; + }; + DhcpData dhcp_data_x[2]; + }; + + struct { + NMLogDomain log_domain; + guint timeout; + guint watch; + GPid pid; + char * binary; + char * address; + guint deadline; + } gw_ping; + + /* dnsmasq stuff for shared connections */ + NMDnsMasqManager *dnsmasq_manager; + gulong dnsmasq_state_id; + + /* Firewall */ + FirewallState fw_state : 4; + NMFirewallManager * fw_mgr; + NMFirewallManagerCallId *fw_call; + + /* IPv4LL stuff */ + sd_ipv4ll *ipv4ll; + guint ipv4ll_timeout; + guint rt6_temporary_not_available_id; + + /* IPv4 DAD stuff */ + struct { + GSList * dad_list; + NMAcdManager *announcing; + } acd; + + union { + struct { + const NMDeviceIPState ip_state_6; + const NMDeviceIPState ip_state_4; + }; + union { + const NMDeviceIPState ip_state_x[2]; + NMDeviceIPState ip_state_x_[2]; + }; + }; + + AppliedConfig ac_ip6_config; /* config from IPv6 autoconfiguration */ + NMIP6Config * ext_ip6_config_captured; /* Configuration captured from platform. */ + NMIP6Config * dad6_ip6_config; + struct in6_addr ipv6ll_addr; + + GHashTable *rt6_temporary_not_available; + + NMNDisc * ndisc; + gulong ndisc_changed_id; + gulong ndisc_timeout_id; + NMSettingIP6ConfigPrivacy ndisc_use_tempaddr; + + guint linklocal6_timeout_id; + guint8 linklocal6_dad_counter; + + GHashTable *ip6_saved_properties; + + EthtoolState *ethtool_state; + + gboolean needs_ip6_subnet; + + /* master interface for bridge/bond/team slave */ + NMDevice *master; + gulong master_ready_id; + int master_ifindex; + + /* slave management */ + CList slaves; /* list of SlaveInfo */ + + NMMetered metered; + + NMSettings *settings; + NMManager * manager; + + NMNetns *netns; + + NMLldpListener *lldp_listener; + + NMConnectivity *concheck_mgr; + CList concheck_lst_head; + struct { + /* if periodic checks are enabled, this is the source id for the next check. */ + guint p_cur_id; + + /* the currently configured max periodic interval. */ + guint p_max_interval; + + /* the current interval. If we are probing, the interval might be lower + * then the configured max interval. */ + guint p_cur_interval; + + /* the timestamp, when we last scheduled the timer p_cur_id with current interval + * p_cur_interval. */ + gint64 p_cur_basetime_ns; + + NMConnectivityState state; + } concheck_x[2]; + + guint check_delete_unrealized_id; + guint32 interface_flags; + + struct { + SriovOp *pending; /* SR-IOV operation currently running */ + SriovOp *next; /* next SR-IOV operation scheduled */ + } sriov; + guint sriov_reset_pending; + + struct { + guint timeout_id; + guint refresh_rate_ms; + guint64 tx_bytes; + guint64 rx_bytes; + } stats; + + bool mtu_force_set_done : 1; +} NMDevicePrivate; + +G_DEFINE_ABSTRACT_TYPE(NMDevice, nm_device, NM_TYPE_DBUS_OBJECT) + +#define NM_DEVICE_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMDevice, NM_IS_DEVICE) + +/*****************************************************************************/ + +static const NMDBusInterfaceInfoExtended interface_info_device; +static const GDBusSignalInfo signal_info_state_changed; + +static void nm_device_set_proxy_config(NMDevice *self, const char *pac_url); + +static gboolean update_ext_ip_config(NMDevice *self, int addr_family, gboolean intersect_configs); + +static gboolean nm_device_set_ip_config(NMDevice * self, + int addr_family, + NMIPConfig *config, + gboolean commit, + GPtrArray * ip4_dev_route_blacklist); + +static gboolean ip_config_merge_and_apply(NMDevice *self, int addr_family, gboolean commit); + +static gboolean nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure); +static void nm_device_slave_notify_enslave(NMDevice *self, gboolean success); +static void nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason); + +static void addrconf6_start_with_link_ready(NMDevice *self); +static gboolean linklocal6_start(NMDevice *self); + +static guint32 default_route_metric_penalty_get(NMDevice *self, int addr_family); + +static guint _prop_get_ipv4_dad_timeout(NMDevice *self); + +static NMIP6Config *dad6_get_pending_addresses(NMDevice *self); + +static void _carrier_wait_check_queued_act_request(NMDevice *self); +static gint64 _get_carrier_wait_ms(NMDevice *self); + +static const char *_activation_func_to_string(ActivationHandleFunc func); + +static void +_set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason, gboolean quitting); +static void queued_state_clear(NMDevice *device); +static gboolean queued_ip4_config_change(gpointer user_data); +static gboolean queued_ip6_config_change(gpointer user_data); +static void ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data); +static gboolean ip_config_valid(NMDeviceState state); +static NMActStageReturn dhcp4_start(NMDevice *self); +static gboolean dhcp6_start(NMDevice *self, gboolean wait_for_ll); +static void nm_device_start_ip_check(NMDevice *self); +static void realize_start_setup(NMDevice * self, + const NMPlatformLink *plink, + gboolean assume_state_guess_assume, + const char * assume_state_connection_uuid, + gboolean set_nm_owned, + NMUnmanFlagOp unmanaged_user_explicit, + gboolean force_platform_init); +static void _set_mtu(NMDevice *self, guint32 mtu); +static void _commit_mtu(NMDevice *self, const NMIP4Config *config); +static void _cancel_activation(NMDevice *self); + +static void concheck_update_state(NMDevice * self, + int addr_family, + NMConnectivityState state, + gboolean is_periodic); + +static void sriov_op_cb(GError *error, gpointer user_data); + +static void device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self); +static gboolean device_link_changed(NMDevice *self); + +/*****************************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE( + queued_state_to_string, + NMDeviceState, + NM_UTILS_LOOKUP_DEFAULT(NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "???"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNKNOWN, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unknown"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNMANAGED, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unmanaged"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNAVAILABLE, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unavailable"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_DISCONNECTED, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "disconnected"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_PREPARE, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "prepare"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_CONFIG, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "config"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_NEED_AUTH, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "need-auth"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_IP_CONFIG, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "ip-config"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_IP_CHECK, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "ip-check"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_SECONDARIES, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "secondaries"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_ACTIVATED, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "activated"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_DEACTIVATING, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "deactivating"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_FAILED, + NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "failed"), ); + +const char * +nm_device_state_to_str(NMDeviceState state) +{ + return queued_state_to_string(state) + NM_STRLEN(NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE); +} + +NM_UTILS_LOOKUP_STR_DEFINE( + nm_device_state_reason_to_str, + NMDeviceStateReason, + NM_UTILS_LOOKUP_DEFAULT(NULL), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNKNOWN, "unknown"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NONE, "none"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NOW_MANAGED, "managed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NOW_UNMANAGED, "unmanaged"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONFIG_FAILED, "config-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE, "ip-config-unavailable"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED, "ip-config-expired"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NO_SECRETS, "no-secrets"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, "supplicant-disconnect"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED, + "supplicant-config-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED, "supplicant-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT, "supplicant-timeout"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_START_FAILED, "ppp-start-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_DISCONNECT, "ppp-disconnect"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_FAILED, "ppp-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_START_FAILED, "dhcp-start-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_ERROR, "dhcp-error"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_FAILED, "dhcp-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SHARED_START_FAILED, "sharing-start-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SHARED_FAILED, "sharing-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED, "autoip-start-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_ERROR, "autoip-error"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_FAILED, "autoip-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_BUSY, "modem-busy"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE, "modem-no-dialtone"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER, "modem-no-carrier"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT, "modem-dial-timeout"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED, "modem-dial-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED, "modem-init-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_APN_FAILED, "gsm-apn-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING, + "gsm-registration-idle"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED, + "gsm-registration-denied"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT, + "gsm-registration-timeout"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED, + "gsm-registration-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED, "gsm-pin-check-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_FIRMWARE_MISSING, "firmware-missing"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_REMOVED, "removed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SLEEPING, "sleeping"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_REMOVED, "connection-removed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_USER_REQUESTED, "user-requested"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CARRIER, "carrier-changed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED, "connection-assumed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, "supplicant-available"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND, "modem-not-found"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_BT_FAILED, "bluetooth-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED, "gsm-sim-not-inserted"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED, "gsm-sim-pin-required"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED, "gsm-sim-puk-required"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_WRONG, "gsm-sim-wrong"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_INFINIBAND_MODE, "infiniband-mode"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED, "dependency-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_BR2684_FAILED, "br2684-bridge-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE, + "modem-manager-unavailable"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SSID_NOT_FOUND, "ssid-not-found"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED, + "secondary-connection-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED, "dcb-fcoe-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED, "teamd-control-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_FAILED, "modem-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_AVAILABLE, "modem-available"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT, "sim-pin-incorrect"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NEW_ACTIVATION, "new-activation"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PARENT_CHANGED, "parent-changed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED, + "parent-managed-changed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_OVSDB_FAILED, "ovsdb-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE, "ip-address-duplicate"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED, "ip-method-unsupported"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED, + "sriov-configuration-failed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND, "peer-not-found"), ); + +#define reason_to_string_a(reason) NM_UTILS_LOOKUP_STR_A(nm_device_state_reason_to_str, reason) + +static NM_UTILS_LOOKUP_STR_DEFINE(mtu_source_to_str, + NMDeviceMtuSource, + NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_NONE, "none"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_PARENT, "parent"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_IP_CONFIG, + "ip-config"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_CONNECTION, + "connection"), ); + +/*****************************************************************************/ + +static void +_hostname_resolver_free(HostnameResolver *resolver) +{ + if (!resolver) + return; + + nm_clear_g_source(&resolver->timeout_id); + nm_clear_g_cancellable(&resolver->cancellable); + nm_g_object_unref(resolver->resolver); + nm_g_object_unref(resolver->address); + g_free(resolver->hostname); + nm_g_slice_free(resolver); +} + +/*****************************************************************************/ + +static NMSettingIP6ConfigPrivacy +_ip6_privacy_clamp(NMSettingIP6ConfigPrivacy use_tempaddr) +{ + switch (use_tempaddr) { + case NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED: + case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR: + case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR: + return use_tempaddr; + default: + return NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN; + } +} + +/*****************************************************************************/ + +static const char * +_prop_get_connection_stable_id(NMDevice * self, + NMConnection * connection, + NMUtilsStableType *out_stable_type) +{ + NMDevicePrivate *priv; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_CONNECTION(connection)); + nm_assert(out_stable_type); + + priv = NM_DEVICE_GET_PRIVATE(self); + + /* we cache the generated stable ID for the time of an activation. + * + * The reason is, that we don't want the stable-id to change as long + * as the device is active. + * + * Especially with ${RANDOM} stable-id we want to generate *one* configuration + * for each activation. */ + if (G_UNLIKELY(!priv->current_stable_id)) { + gs_free char * default_id = NULL; + gs_free char * generated = NULL; + NMUtilsStableType stable_type; + NMSettingConnection *s_con; + gboolean hwaddr_is_fake; + const char * hwaddr; + const char * stable_id; + const char * uuid; + + s_con = nm_connection_get_setting_connection(connection); + + stable_id = nm_setting_connection_get_stable_id(s_con); + + if (!stable_id) { + default_id = + nm_config_data_get_connection_default(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.stable-id"), + self); + stable_id = default_id; + } + + uuid = nm_connection_get_uuid(connection); + + /* the cloned-mac-address may be generated based on the stable-id. + * Thus, at this point, we can only use the permanent MAC address + * as seed. */ + hwaddr = nm_device_get_permanent_hw_address_full(self, TRUE, &hwaddr_is_fake); + + stable_type = nm_utils_stable_id_parse(stable_id, + nm_device_get_ip_iface(self), + !hwaddr_is_fake ? hwaddr : NULL, + nm_utils_boot_id_str(), + uuid, + &generated); + + /* current_stable_id_type is a bitfield! */ + priv->current_stable_id_type = stable_type; + nm_assert(stable_type <= (NMUtilsStableType) 0x3); + nm_assert(stable_type + (NMUtilsStableType) 1 > (NMUtilsStableType) 0); + nm_assert(priv->current_stable_id_type == stable_type); + + if (stable_type == NM_UTILS_STABLE_TYPE_UUID) + priv->current_stable_id = g_strdup(uuid); + else if (stable_type == NM_UTILS_STABLE_TYPE_STABLE_ID) + priv->current_stable_id = g_strdup(stable_id); + else if (stable_type == NM_UTILS_STABLE_TYPE_GENERATED) + priv->current_stable_id = + nm_str_realloc(nm_utils_stable_id_generated_complete(generated)); + else { + nm_assert(stable_type == NM_UTILS_STABLE_TYPE_RANDOM); + priv->current_stable_id = nm_str_realloc(nm_utils_stable_id_random()); + } + _LOGT(LOGD_DEVICE, + "stable-id: type=%d, \"%s\"" + "%s%s%s", + (int) priv->current_stable_id_type, + priv->current_stable_id, + NM_PRINT_FMT_QUOTED(stable_type == NM_UTILS_STABLE_TYPE_GENERATED, + " from \"", + generated, + "\"", + "")); + } + + nm_assert(priv->current_stable_id); + *out_stable_type = priv->current_stable_id_type; + return priv->current_stable_id; +} + +static GBytes * +_prop_get_ipv6_dhcp_duid(NMDevice * self, + NMConnection *connection, + GBytes * hwaddr, + gboolean * out_enforce) +{ + NMSettingIPConfig *s_ip6; + const char * duid; + gs_free char * duid_default = NULL; + const char * duid_error; + GBytes * duid_out; + gboolean duid_enforce = TRUE; + gs_free char * logstr1 = NULL; + const guint8 * hwaddr_bin; + gsize hwaddr_len; + int arp_type; + + s_ip6 = nm_connection_get_setting_ip6_config(connection); + duid = nm_setting_ip6_config_get_dhcp_duid(NM_SETTING_IP6_CONFIG(s_ip6)); + + if (!duid) { + duid_default = nm_config_data_get_connection_default(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv6.dhcp-duid"), + self); + duid = duid_default; + if (!duid) + duid = "lease"; + } + + if (nm_streq(duid, "lease")) { + duid_enforce = FALSE; + duid_out = nm_utils_generate_duid_from_machine_id(); + goto out_good; + } + + if (!_nm_utils_dhcp_duid_valid(duid, &duid_out)) { + duid_error = "invalid duid"; + goto out_fail; + } + + if (duid_out) + goto out_good; + + if (NM_IN_STRSET(duid, "ll", "llt")) { + if (!hwaddr) { + duid_error = "missing link-layer address"; + goto out_fail; + } + + hwaddr_bin = g_bytes_get_data(hwaddr, &hwaddr_len); + arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len); + if (arp_type < 0) { + duid_error = "unsupported link-layer address"; + goto out_fail; + } + + if (nm_streq(duid, "ll")) + duid_out = nm_utils_generate_duid_ll(arp_type, hwaddr_bin, hwaddr_len); + else { + duid_out = nm_utils_generate_duid_llt(arp_type, + hwaddr_bin, + hwaddr_len, + nm_utils_host_id_get_timestamp_ns() + / NM_UTILS_NSEC_PER_SEC); + } + + goto out_good; + } + + if (NM_IN_STRSET(duid, "stable-ll", "stable-llt", "stable-uuid")) { + /* preferably, we would salt the checksum differently for each @duid type. We missed + * to do that initially, so most types use the DEFAULT_SALT. + * + * Implementations that are added later, should use a distinct salt instead, + * like "stable-ll"/"stable-llt" with ARPHRD_INFINIBAND below. */ + const guint32 DEFAULT_SALT = 670531087u; + nm_auto_free_checksum GChecksum *sum = NULL; + NMUtilsStableType stable_type; + const char * stable_id = NULL; + guint32 salted_header; + const guint8 * host_id; + gsize host_id_len; + union { + guint8 sha256[NM_UTILS_CHECKSUM_LENGTH_SHA256]; + guint8 hwaddr_eth[ETH_ALEN]; + guint8 hwaddr_infiniband[INFINIBAND_ALEN]; + NMUuid uuid; + struct _nm_packed { + guint8 hwaddr[ETH_ALEN]; + guint32 timestamp; + } llt_eth; + struct _nm_packed { + guint8 hwaddr[INFINIBAND_ALEN]; + guint32 timestamp; + } llt_infiniband; + } digest; + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + + if (NM_IN_STRSET(duid, "stable-ll", "stable-llt")) { + /* for stable LL/LLT DUIDs, we still need a hardware address to detect + * the arp-type. Alternatively, we would be able to detect it based on + * other means (e.g. NMDevice type), but instead require the hardware + * address to be present. This is at least consistent with the "ll"/"llt" + * modes above. */ + if (!hwaddr) { + duid_error = "missing link-layer address"; + goto out_fail; + } + if ((arp_type = nm_utils_arp_type_detect_from_hwaddrlen(g_bytes_get_size(hwaddr))) + < 0) { + duid_error = "unsupported link-layer address"; + goto out_fail; + } + + if (arp_type == ARPHRD_ETHER) + salted_header = DEFAULT_SALT; + else { + nm_assert(arp_type == ARPHRD_INFINIBAND); + salted_header = 0x42492CEFu + ((guint32) arp_type); + } + } else { + salted_header = DEFAULT_SALT; + arp_type = -1; + } + + salted_header = htonl(salted_header + ((guint32) stable_type)); + + nm_utils_host_id_get(&host_id, &host_id_len); + + sum = g_checksum_new(G_CHECKSUM_SHA256); + g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header)); + g_checksum_update(sum, (const guchar *) stable_id, -1); + g_checksum_update(sum, (const guchar *) host_id, host_id_len); + nm_utils_checksum_get_digest(sum, digest.sha256); + + G_STATIC_ASSERT_EXPR(sizeof(digest) == sizeof(digest.sha256)); + + if (nm_streq(duid, "stable-ll")) { + switch (arp_type) { + case ARPHRD_ETHER: + duid_out = nm_utils_generate_duid_ll(arp_type, + digest.hwaddr_eth, + sizeof(digest.hwaddr_eth)); + break; + case ARPHRD_INFINIBAND: + duid_out = nm_utils_generate_duid_ll(arp_type, + digest.hwaddr_infiniband, + sizeof(digest.hwaddr_infiniband)); + break; + default: + g_return_val_if_reached(NULL); + } + } else if (nm_streq(duid, "stable-llt")) { + gint64 time; + guint32 timestamp; + +#define EPOCH_DATETIME_THREE_YEARS (356 * 24 * 3600 * 3) + + /* We want a variable time between the host_id timestamp and three years + * before. Let's compute the time (in seconds) from 0 to 3 years; then we'll + * subtract it from the host_id timestamp. + */ + time = nm_utils_host_id_get_timestamp_ns() / NM_UTILS_NSEC_PER_SEC; + + /* don't use too old timestamps. They cannot be expressed in DUID-LLT and + * would all be truncated to zero. */ + time = NM_MAX(time, NM_UTILS_EPOCH_DATETIME_200001010000 + EPOCH_DATETIME_THREE_YEARS); + + switch (arp_type) { + case ARPHRD_ETHER: + timestamp = unaligned_read_be32(&digest.llt_eth.timestamp); + time -= timestamp % EPOCH_DATETIME_THREE_YEARS; + duid_out = nm_utils_generate_duid_llt(arp_type, + digest.llt_eth.hwaddr, + sizeof(digest.llt_eth.hwaddr), + time); + break; + case ARPHRD_INFINIBAND: + timestamp = unaligned_read_be32(&digest.llt_infiniband.timestamp); + time -= timestamp % EPOCH_DATETIME_THREE_YEARS; + duid_out = nm_utils_generate_duid_llt(arp_type, + digest.llt_infiniband.hwaddr, + sizeof(digest.llt_infiniband.hwaddr), + time); + break; + default: + g_return_val_if_reached(NULL); + } + } else { + nm_assert(nm_streq(duid, "stable-uuid")); + duid_out = nm_utils_generate_duid_uuid(&digest.uuid); + } + + goto out_good; + } + + g_return_val_if_reached(NULL); + +out_fail: + nm_assert(!duid_out && duid_error); + { + NMUuid uuid; + + _LOGW(LOGD_IP6 | LOGD_DHCP6, + "ipv6.dhcp-duid: failure to generate %s DUID: %s. Fallback to random DUID-UUID.", + duid, + duid_error); + + nm_utils_random_bytes(&uuid, sizeof(uuid)); + duid_out = nm_utils_generate_duid_uuid(&uuid); + } + +out_good: + nm_assert(duid_out); + _LOGD(LOGD_IP6 | LOGD_DHCP6, + "ipv6.dhcp-duid: generate %s DUID '%s' (%s)", + duid, + (logstr1 = nm_dhcp_utils_duid_to_string(duid_out)), + duid_enforce ? "enforcing" : "prefer lease"); + + NM_SET_OUT(out_enforce, duid_enforce); + return duid_out; +} + +static guint32 +_prop_get_ipv6_ra_timeout(NMDevice *self) +{ + NMConnection *connection; + gint32 timeout; + + G_STATIC_ASSERT_EXPR(NM_RA_TIMEOUT_DEFAULT == 0); + G_STATIC_ASSERT_EXPR(NM_RA_TIMEOUT_INFINITY == G_MAXINT32); + + connection = nm_device_get_applied_connection(self); + + timeout = nm_setting_ip6_config_get_ra_timeout( + NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection))); + if (timeout > 0) + return timeout; + nm_assert(timeout == 0); + + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv6.ra-timeout"), + self, + 0, + G_MAXINT32, + 0); +} + +static NMSettingConnectionMdns +_prop_get_connection_mdns(NMDevice *self) +{ + NMConnection * connection; + NMSettingConnectionMdns mdns = NM_SETTING_CONNECTION_MDNS_DEFAULT; + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_MDNS_DEFAULT); + + connection = nm_device_get_applied_connection(self); + if (connection) + mdns = nm_setting_connection_get_mdns(nm_connection_get_setting_connection(connection)); + if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT) + return mdns; + + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.mdns"), + self, + NM_SETTING_CONNECTION_MDNS_NO, + NM_SETTING_CONNECTION_MDNS_YES, + NM_SETTING_CONNECTION_MDNS_DEFAULT); +} + +static NMSettingConnectionLlmnr +_prop_get_connection_llmnr(NMDevice *self) +{ + NMConnection * connection; + NMSettingConnectionLlmnr llmnr = NM_SETTING_CONNECTION_LLMNR_DEFAULT; + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_LLMNR_DEFAULT); + + connection = nm_device_get_applied_connection(self); + if (connection) + llmnr = nm_setting_connection_get_llmnr(nm_connection_get_setting_connection(connection)); + if (llmnr != NM_SETTING_CONNECTION_LLMNR_DEFAULT) + return llmnr; + + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.llmnr"), + self, + NM_SETTING_CONNECTION_LLMNR_NO, + NM_SETTING_CONNECTION_LLMNR_YES, + NM_SETTING_CONNECTION_LLMNR_DEFAULT); +} + +static guint32 +_prop_get_ipvx_route_table(NMDevice *self, int addr_family) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceClass * klass; + NMConnection * connection; + NMSettingIPConfig * s_ip; + guint32 route_table = 0; + gboolean is_user_config = TRUE; + NMSettingConnection *s_con; + NMSettingVrf * s_vrf; + + nm_assert_addr_family(addr_family); + + /* the route table setting affects how we sync routes. We shall + * not change it while the device is active, hence, cache it. */ + if (NM_IS_IPv4(addr_family)) { + if (priv->v4_route_table_initialized) + return priv->v4_route_table; + } else { + if (priv->v6_route_table_initialized) + return priv->v6_route_table; + } + + connection = nm_device_get_applied_connection(self); + if (connection) { + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + if (s_ip) + route_table = nm_setting_ip_config_get_route_table(s_ip); + } + if (route_table == 0u) { + gint64 v; + + v = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_IS_IPv4(addr_family) + ? NM_CON_DEFAULT("ipv4.route-table") + : NM_CON_DEFAULT("ipv6.route-table"), + self, + 0, + G_MAXUINT32, + -1); + if (v != -1) { + route_table = v; + is_user_config = FALSE; + } + } + + if (route_table == 0u && connection + && (s_con = nm_connection_get_setting_connection(connection)) + && (nm_streq0(nm_setting_connection_get_slave_type(s_con), NM_SETTING_VRF_SETTING_NAME) + && priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF)) { + const NMPlatformLnkVrf *lnk; + + lnk = nm_platform_link_get_lnk_vrf(nm_device_get_platform(self), + nm_device_get_ifindex(priv->master), + NULL); + + if (lnk) + route_table = lnk->table; + } + + if (route_table == 0u && connection + && (s_vrf = (NMSettingVrf *) nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF))) { + route_table = nm_setting_vrf_get_table(s_vrf); + } + + klass = NM_DEVICE_GET_CLASS(self); + if (klass->coerce_route_table) + route_table = klass->coerce_route_table(self, addr_family, route_table, is_user_config); + + if (NM_IS_IPv4(addr_family)) { + priv->v4_route_table_initialized = TRUE; + priv->v4_route_table = route_table; + } else { + priv->v6_route_table_initialized = TRUE; + priv->v6_route_table = route_table; + } + + _LOGT(LOGD_DEVICE, + "ipv%c.route-table = %u%s", + nm_utils_addr_family_to_char(addr_family), + (guint)(route_table ?: RT_TABLE_MAIN), + route_table != 0u ? "" : " (policy routing not enabled)"); + + return route_table; +} + +static gboolean +_prop_get_connection_lldp(NMDevice *self) +{ + NMConnection * connection; + NMSettingConnection * s_con; + NMSettingConnectionLldp lldp = NM_SETTING_CONNECTION_LLDP_DEFAULT; + + connection = nm_device_get_applied_connection(self); + g_return_val_if_fail(connection, FALSE); + + s_con = nm_connection_get_setting_connection(connection); + g_return_val_if_fail(s_con, FALSE); + + lldp = nm_setting_connection_get_lldp(s_con); + if (lldp == NM_SETTING_CONNECTION_LLDP_DEFAULT) { + lldp = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.lldp"), + self, + NM_SETTING_CONNECTION_LLDP_DEFAULT, + NM_SETTING_CONNECTION_LLDP_ENABLE_RX, + NM_SETTING_CONNECTION_LLDP_DEFAULT); + if (lldp == NM_SETTING_CONNECTION_LLDP_DEFAULT) + lldp = NM_SETTING_CONNECTION_LLDP_DISABLE; + } + return lldp == NM_SETTING_CONNECTION_LLDP_ENABLE_RX; +} + +static guint +_prop_get_ipv4_dad_timeout(NMDevice *self) +{ + NMConnection * connection; + NMSettingIPConfig *s_ip4 = NULL; + int timeout = -1; + + connection = nm_device_get_applied_connection(self); + if (connection) + s_ip4 = nm_connection_get_setting_ip4_config(connection); + if (s_ip4) + timeout = nm_setting_ip_config_get_dad_timeout(s_ip4); + if (timeout >= 0) + return timeout; + + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv4.dad-timeout"), + self, + 0, + NM_SETTING_IP_CONFIG_DAD_TIMEOUT_MAX, + 0); +} + +static guint32 +_prop_get_ipvx_dhcp_timeout(NMDevice *self, int addr_family) +{ + NMDeviceClass *klass; + NMConnection * connection; + int timeout_i; + guint32 timeout; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert_addr_family(addr_family); + + connection = nm_device_get_applied_connection(self); + + timeout_i = nm_setting_ip_config_get_dhcp_timeout( + nm_connection_get_setting_ip_config(connection, addr_family)); + nm_assert(timeout_i >= 0 && timeout_i <= G_MAXINT32); + + timeout = (guint32) timeout_i; + if (timeout) + goto out; + + timeout = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_IS_IPv4(addr_family) + ? NM_CON_DEFAULT("ipv4.dhcp-timeout") + : NM_CON_DEFAULT("ipv6.dhcp-timeout"), + self, + 0, + G_MAXINT32, + 0); + if (timeout) + goto out; + + klass = NM_DEVICE_GET_CLASS(self); + if (klass->get_dhcp_timeout_for_device) { + timeout = klass->get_dhcp_timeout_for_device(self, addr_family); + if (timeout) + goto out; + } + + timeout = NM_DHCP_TIMEOUT_DEFAULT; + +out: + G_STATIC_ASSERT_EXPR(G_MAXINT32 == NM_DHCP_TIMEOUT_INFINITY); + nm_assert(timeout > 0); + nm_assert(timeout <= G_MAXINT32); + return timeout; +} + +/** + * _prop_get_ipvx_dhcp_iaid: + * @self: the #NMDevice + * @addr_family: the address family + * @connection: the connection + * @log_silent: whether to log the result. + * @out_is_explicit: on return, %TRUE if the user set a valid IAID in + * the connection or in global configuration; %FALSE if the connection + * property was empty and no valid global configuration was provided. + * + * Returns: a IAID value for this device and the given connection. + */ +static guint32 +_prop_get_ipvx_dhcp_iaid(NMDevice * self, + int addr_family, + NMConnection *connection, + gboolean log_silent, + gboolean * out_is_explicit) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMSettingIPConfig *s_ip; + const char * iaid_str; + gs_free char * iaid_str_free = NULL; + guint32 iaid; + const char * iface; + const char * fail_reason; + gboolean is_explicit = TRUE; + + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + iaid_str = nm_setting_ip_config_get_dhcp_iaid(s_ip); + if (!iaid_str) { + iaid_str_free = nm_config_data_get_connection_default( + NM_CONFIG_GET_DATA, + IS_IPv4 ? NM_CON_DEFAULT("ipv4.dhcp-iaid") : NM_CON_DEFAULT("ipv6.dhcp-iaid"), + self); + iaid_str = iaid_str_free; + if (!iaid_str) { + iaid_str = NM_IAID_IFNAME; + is_explicit = FALSE; + } else if (!_nm_utils_iaid_verify(iaid_str, NULL)) { + if (!log_silent) { + _LOGW(LOGD_DEVICE, + "invalid global default '%s' for ipv%c.dhcp-iaid", + iaid_str, + nm_utils_addr_family_to_char(addr_family)); + } + iaid_str = NM_IAID_IFNAME; + is_explicit = FALSE; + } + } + + if (nm_streq0(iaid_str, NM_IAID_MAC)) { + const NMPlatformLink *pllink; + + pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self)); + if (!pllink || pllink->l_address.len < 4) { + fail_reason = "invalid link-layer address"; + goto out_fail; + } + + /* @iaid is in native endianness. Use unaligned_read_be32() + * so that the IAID for a given MAC address is the same on + * BE and LE machines. */ + iaid = unaligned_read_be32(&pllink->l_address.data[pllink->l_address.len - 4]); + goto out_good; + } else if (nm_streq0(iaid_str, NM_IAID_PERM_MAC)) { + guint8 hwaddr_buf[NM_UTILS_HWADDR_LEN_MAX]; + const char *hwaddr_str; + gsize hwaddr_len; + + hwaddr_str = nm_device_get_permanent_hw_address(self); + if (!hwaddr_str) { + fail_reason = "no permanent link-layer address"; + goto out_fail; + } + + if (!_nm_utils_hwaddr_aton(hwaddr_str, hwaddr_buf, sizeof(hwaddr_buf), &hwaddr_len)) + g_return_val_if_reached(0); + + if (hwaddr_len < 4) { + fail_reason = "invalid link-layer address"; + goto out_fail; + } + + iaid = unaligned_read_be32(&hwaddr_buf[hwaddr_len - 4]); + goto out_good; + } else if (nm_streq(iaid_str, "stable")) { + nm_auto_free_checksum GChecksum *sum = NULL; + guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA1]; + NMUtilsStableType stable_type; + const char * stable_id; + guint32 salted_header; + const guint8 * host_id; + gsize host_id_len; + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + salted_header = htonl(53390459 + stable_type); + nm_utils_host_id_get(&host_id, &host_id_len); + iface = nm_device_get_ip_iface(self); + + sum = g_checksum_new(G_CHECKSUM_SHA1); + g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header)); + g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1); + g_checksum_update(sum, (const guchar *) iface, strlen(iface) + 1); + g_checksum_update(sum, (const guchar *) host_id, host_id_len); + nm_utils_checksum_get_digest(sum, digest); + + iaid = unaligned_read_be32(digest); + goto out_good; + } else if ((iaid = _nm_utils_ascii_str_to_int64(iaid_str, 10, 0, G_MAXUINT32, -1)) != -1) { + goto out_good; + } else { + iface = nm_device_get_ip_iface(self); + iaid = nm_utils_create_dhcp_iaid(TRUE, (const guint8 *) iface, strlen(iface)); + goto out_good; + } + +out_fail: + nm_assert(fail_reason); + if (!log_silent) { + _LOGW(LOGD_DEVICE | LOGD_DHCPX(IS_IPv4) | LOGD_IPX(IS_IPv4), + "ipv%c.dhcp-iaid: failure to generate IAID: %s. Using interface-name based IAID", + nm_utils_addr_family_to_char(addr_family), + fail_reason); + } + is_explicit = FALSE; + iface = nm_device_get_ip_iface(self); + iaid = nm_utils_create_dhcp_iaid(TRUE, (const guint8 *) iface, strlen(iface)); +out_good: + if (!log_silent) { + _LOGD(LOGD_DEVICE | LOGD_DHCPX(IS_IPv4) | LOGD_IPX(IS_IPv4), + "ipv%c.dhcp-iaid: using %u (0x%08x) IAID (str: '%s', explicit %d)", + nm_utils_addr_family_to_char(addr_family), + iaid, + iaid, + iaid_str, + is_explicit); + } + NM_SET_OUT(out_is_explicit, is_explicit); + return iaid; +} + +static NMDhcpHostnameFlags +_prop_get_ipvx_dhcp_hostname_flags(NMDevice *self, int addr_family) +{ + NMConnection * connection; + NMSettingIPConfig * s_ip; + NMDhcpHostnameFlags flags; + gs_free_error GError *error = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_DHCP_HOSTNAME_FLAG_NONE); + + connection = nm_device_get_applied_connection(self); + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + g_return_val_if_fail(s_ip, NM_DHCP_HOSTNAME_FLAG_NONE); + + if (!nm_setting_ip_config_get_dhcp_send_hostname(s_ip)) + return NM_DHCP_HOSTNAME_FLAG_NONE; + + flags = nm_setting_ip_config_get_dhcp_hostname_flags(s_ip); + if (flags != NM_DHCP_HOSTNAME_FLAG_NONE) + return flags; + + flags = nm_config_data_get_connection_default_int64( + NM_CONFIG_GET_DATA, + NM_IS_IPv4(addr_family) ? NM_CON_DEFAULT("ipv4.dhcp-hostname-flags") + : NM_CON_DEFAULT("ipv6.dhcp-hostname-flags"), + self, + 0, + NM_DHCP_HOSTNAME_FLAG_FQDN_CLEAR_FLAGS, + 0); + + if (!_nm_utils_validate_dhcp_hostname_flags(flags, addr_family, &error)) { + _LOGW(LOGD_DEVICE, + "invalid global default value 0x%x for ipv%c.%s: %s", + (guint) flags, + nm_utils_addr_family_to_char(addr_family), + NM_SETTING_IP_CONFIG_DHCP_HOSTNAME_FLAGS, + error->message); + flags = NM_DHCP_HOSTNAME_FLAG_NONE; + } + + if (flags != NM_DHCP_HOSTNAME_FLAG_NONE) + return flags; + + if (NM_IS_IPv4(addr_family)) + return NM_DHCP_HOSTNAME_FLAGS_FQDN_DEFAULT_IP4; + else + return NM_DHCP_HOSTNAME_FLAGS_FQDN_DEFAULT_IP6; +} + +static const char * +_prop_get_connection_mud_url(NMDevice *self, NMSettingConnection *s_con, char **out_mud_url) +{ + const char * mud_url; + gs_free char *s = NULL; + + nm_assert(out_mud_url && !*out_mud_url); + + mud_url = nm_setting_connection_get_mud_url(s_con); + + if (mud_url) { + if (nm_streq(mud_url, NM_CONNECTION_MUD_URL_NONE)) + return NULL; + return mud_url; + } + + s = nm_config_data_get_connection_default(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.mud-url"), + self); + if (s) { + if (nm_streq(s, NM_CONNECTION_MUD_URL_NONE)) + return NULL; + if (nm_sd_http_url_is_valid_https(s)) + return (*out_mud_url = g_steal_pointer(&s)); + } + + return NULL; +} + +static GBytes * +_prop_get_ipv4_dhcp_client_id(NMDevice *self, NMConnection *connection, GBytes *hwaddr) +{ + NMSettingIPConfig *s_ip4; + const char * client_id; + gs_free char * client_id_default = NULL; + guint8 * client_id_buf; + const char * fail_reason; + guint8 hwaddr_bin_buf[NM_UTILS_HWADDR_LEN_MAX]; + const guint8 * hwaddr_bin; + int arp_type; + gsize hwaddr_len; + GBytes * result; + gs_free char * logstr1 = NULL; + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + client_id = nm_setting_ip4_config_get_dhcp_client_id(NM_SETTING_IP4_CONFIG(s_ip4)); + + if (!client_id) { + client_id_default = + nm_config_data_get_connection_default(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv4.dhcp-client-id"), + self); + if (client_id_default && client_id_default[0]) { + /* a non-empty client-id is always valid, see nm_dhcp_utils_client_id_string_to_bytes(). */ + client_id = client_id_default; + } + } + + if (!client_id) { + _LOGD(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4, + "ipv4.dhcp-client-id: no explicit client-id configured"); + return NULL; + } + + if (nm_streq(client_id, "mac")) { + if (!hwaddr) { + fail_reason = "missing link-layer address"; + goto out_fail; + } + + hwaddr_bin = g_bytes_get_data(hwaddr, &hwaddr_len); + arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len); + if (arp_type < 0) { + fail_reason = "unsupported link-layer address"; + goto out_fail; + } + + result = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_bin, hwaddr_len); + goto out_good; + } + + if (nm_streq(client_id, "perm-mac")) { + const char *hwaddr_str; + + hwaddr_str = nm_device_get_permanent_hw_address(self); + if (!hwaddr_str) { + fail_reason = "missing permanent link-layer address"; + goto out_fail; + } + + if (!_nm_utils_hwaddr_aton(hwaddr_str, hwaddr_bin_buf, sizeof(hwaddr_bin_buf), &hwaddr_len)) + g_return_val_if_reached(NULL); + + arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len); + if (arp_type < 0) { + fail_reason = "unsupported permanent link-layer address"; + goto out_fail; + } + + result = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_bin_buf, hwaddr_len); + goto out_good; + } + + if (nm_streq(client_id, "duid")) { + guint32 iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET, connection, FALSE, NULL); + + result = nm_utils_dhcp_client_id_systemd_node_specific(iaid); + goto out_good; + } + + if (nm_streq(client_id, "ipv6-duid")) { + gs_unref_bytes GBytes *duid = NULL; + gboolean iaid_is_explicit; + guint32 iaid; + const guint8 * duid_arr; + gsize duid_len; + + iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET, connection, FALSE, &iaid_is_explicit); + if (!iaid_is_explicit) + iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET6, connection, FALSE, &iaid_is_explicit); + + duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, NULL); + + nm_assert(duid); + + duid_arr = g_bytes_get_data(duid, &duid_len); + + nm_assert(duid_arr); + nm_assert(duid_len >= 2u + 1u); + nm_assert(duid_len <= 2u + 128u); + + result = nm_utils_dhcp_client_id_duid(iaid, duid_arr, duid_len); + goto out_good; + } + + if (nm_streq(client_id, "stable")) { + nm_auto_free_checksum GChecksum *sum = NULL; + guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA1]; + NMUtilsStableType stable_type; + const char * stable_id; + guint32 salted_header; + const guint8 * host_id; + gsize host_id_len; + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + salted_header = htonl(2011610591 + stable_type); + nm_utils_host_id_get(&host_id, &host_id_len); + + sum = g_checksum_new(G_CHECKSUM_SHA1); + g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header)); + g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1); + g_checksum_update(sum, (const guchar *) host_id, host_id_len); + nm_utils_checksum_get_digest(sum, digest); + + client_id_buf = g_malloc(1 + 15); + client_id_buf[0] = 0; + memcpy(&client_id_buf[1], digest, 15); + result = g_bytes_new_take(client_id_buf, 1 + 15); + goto out_good; + } + + result = nm_dhcp_utils_client_id_string_to_bytes(client_id); + goto out_good; + +out_fail: + nm_assert(fail_reason); + _LOGW(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4, + "ipv4.dhcp-client-id: failure to generate client id (%s). Use random client id", + fail_reason); + client_id_buf = g_malloc(1 + 15); + client_id_buf[0] = 0; + nm_utils_random_bytes(&client_id_buf[1], 15); + result = g_bytes_new_take(client_id_buf, 1 + 15); + +out_good: + nm_assert(result); + _LOGD(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4, + "ipv4.dhcp-client-id: use \"%s\" client ID: %s", + client_id, + (logstr1 = nm_dhcp_utils_duid_to_string(result))); + return result; +} + +static GBytes * +_prop_get_ipv4_dhcp_vendor_class_identifier(NMDevice *self, NMSettingIP4Config *s_ip4) +{ + gs_free char *config_data_prop = NULL; + gs_free char *to_free = NULL; + const char * conn_prop; + GBytes * bytes = NULL; + const char * bin; + gsize len; + + conn_prop = nm_setting_ip4_config_get_dhcp_vendor_class_identifier(s_ip4); + + if (!conn_prop) { + /* set in NetworkManager.conf ? */ + config_data_prop = nm_config_data_get_connection_default( + NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv4.dhcp-vendor-class-identifier"), + self); + + if (config_data_prop && nm_utils_validate_dhcp4_vendor_class_id(config_data_prop, NULL)) + conn_prop = config_data_prop; + } + + if (conn_prop) { + bin = nm_utils_buf_utf8safe_unescape(conn_prop, + NM_UTILS_STR_UTF8_SAFE_FLAG_NONE, + &len, + (gpointer *) &to_free); + if (to_free) + bytes = g_bytes_new_take(g_steal_pointer(&to_free), len); + else + bytes = g_bytes_new(bin, len); + } + + return bytes; +} + +static NMSettingIP6ConfigPrivacy +_prop_get_ipv6_ip6_privacy(NMDevice *self) +{ + NMSettingIP6ConfigPrivacy ip6_privacy; + NMConnection * connection; + + g_return_val_if_fail(self, NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + + /* 1.) First look at the per-connection setting. If it is not -1 (unknown), + * use it. */ + connection = nm_device_get_applied_connection(self); + if (connection) { + NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection); + + if (s_ip6) { + ip6_privacy = nm_setting_ip6_config_get_ip6_privacy(NM_SETTING_IP6_CONFIG(s_ip6)); + ip6_privacy = _ip6_privacy_clamp(ip6_privacy); + if (ip6_privacy != NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN) + return ip6_privacy; + } + } + + /* 2.) use the default value from the configuration. */ + ip6_privacy = + nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("ipv6.ip6-privacy"), + self, + NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN, + NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR, + NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + if (ip6_privacy != NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN) + return ip6_privacy; + + if (!nm_device_get_ip_ifindex(self)) + return NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN; + + /* 3.) No valid default-value configured. Fallback to reading sysctl. + * + * Instead of reading static config files in /etc, just read the current sysctl value. + * This works as NM only writes to "/proc/sys/net/ipv6/conf/IFNAME/use_tempaddr", but leaves + * the "default" entry untouched. */ + ip6_privacy = nm_platform_sysctl_get_int32( + nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv6/conf/default/use_tempaddr"), + NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + return _ip6_privacy_clamp(ip6_privacy); +} + +static const char * +_prop_get_x_cloned_mac_address(NMDevice * self, + NMConnection *connection, + gboolean is_wifi, + char ** out_addr) +{ + NMSetting * setting; + const char *addr = NULL; + + nm_assert(out_addr && !*out_addr); + + setting = nm_connection_get_setting(connection, + is_wifi ? NM_TYPE_SETTING_WIRELESS : NM_TYPE_SETTING_WIRED); + if (setting) { + addr = is_wifi ? nm_setting_wireless_get_cloned_mac_address((NMSettingWireless *) setting) + : nm_setting_wired_get_cloned_mac_address((NMSettingWired *) setting); + } + + if (!addr) { + gs_free char *a = NULL; + + a = nm_config_data_get_connection_default( + NM_CONFIG_GET_DATA, + is_wifi ? NM_CON_DEFAULT("wifi.cloned-mac-address") + : NM_CON_DEFAULT("ethernet.cloned-mac-address"), + self); + + addr = NM_CLONED_MAC_PRESERVE; + + if (!a) { + if (is_wifi) { + NMSettingMacRandomization v; + + /* for backward compatibility, read the deprecated wifi.mac-address-randomization setting. */ + a = nm_config_data_get_connection_default( + NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("wifi.mac-address-randomization"), + self); + v = _nm_utils_ascii_str_to_int64(a, + 10, + NM_SETTING_MAC_RANDOMIZATION_DEFAULT, + NM_SETTING_MAC_RANDOMIZATION_ALWAYS, + NM_SETTING_MAC_RANDOMIZATION_DEFAULT); + if (v == NM_SETTING_MAC_RANDOMIZATION_ALWAYS) + addr = NM_CLONED_MAC_RANDOM; + } + } else if (NM_CLONED_MAC_IS_SPECIAL(a) || nm_utils_hwaddr_valid(a, ETH_ALEN)) + addr = *out_addr = g_steal_pointer(&a); + } + + return addr; +} + +static const char * +_prop_get_x_generate_mac_address_mask(NMDevice * self, + NMConnection *connection, + gboolean is_wifi, + char ** out_value) +{ + NMSetting * setting; + const char *value = NULL; + char * a; + + nm_assert(out_value && !*out_value); + + setting = nm_connection_get_setting(connection, + is_wifi ? NM_TYPE_SETTING_WIRELESS : NM_TYPE_SETTING_WIRED); + if (setting) { + value = + is_wifi + ? nm_setting_wireless_get_generate_mac_address_mask((NMSettingWireless *) setting) + : nm_setting_wired_get_generate_mac_address_mask((NMSettingWired *) setting); + if (value) + return value; + } + + a = nm_config_data_get_connection_default( + NM_CONFIG_GET_DATA, + is_wifi ? NM_CON_DEFAULT("wifi.generate-mac-address-mask") + : NM_CON_DEFAULT("ethernet.generate-mac-address-mask"), + self); + if (!a) + return NULL; + *out_value = a; + return a; +} + +/*****************************************************************************/ + +static void +_ethtool_features_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state) +{ + gs_free NMEthtoolFeatureStates *features; + + features = g_steal_pointer(ðtool_state->features); + + if (!nm_platform_ethtool_set_features(platform, + ethtool_state->ifindex, + features, + ethtool_state->requested, + FALSE)) + _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more offload features"); + else + _LOGD(LOGD_DEVICE, "ethtool: offload features successfully reset"); +} + +static void +_ethtool_features_set(NMDevice * self, + NMPlatform * platform, + EthtoolState * ethtool_state, + NMSettingEthtool *s_ethtool) +{ + gs_free NMEthtoolFeatureStates *features = NULL; + + if (ethtool_state->features) + _ethtool_features_reset(self, platform, ethtool_state); + + if (nm_setting_ethtool_init_features(s_ethtool, ethtool_state->requested) == 0) + return; + + features = nm_platform_ethtool_get_link_features(platform, ethtool_state->ifindex); + if (!features) { + _LOGW(LOGD_DEVICE, "ethtool: failure setting offload features (cannot read features)"); + return; + } + + if (!nm_platform_ethtool_set_features(platform, + ethtool_state->ifindex, + features, + ethtool_state->requested, + TRUE)) + _LOGW(LOGD_DEVICE, "ethtool: failure setting one or more offload features"); + else + _LOGD(LOGD_DEVICE, "ethtool: offload features successfully set"); + + ethtool_state->features = g_steal_pointer(&features); +} + +static void +_ethtool_coalesce_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state) +{ + gs_free NMEthtoolCoalesceState *coalesce = NULL; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_PLATFORM(platform)); + nm_assert(ethtool_state); + + coalesce = g_steal_pointer(ðtool_state->coalesce); + if (!coalesce) + return; + + if (!nm_platform_ethtool_set_coalesce(platform, ethtool_state->ifindex, coalesce)) + _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more coalesce settings"); + else + _LOGD(LOGD_DEVICE, "ethtool: coalesce settings successfully reset"); +} + +static void +_ethtool_coalesce_set(NMDevice * self, + NMPlatform * platform, + EthtoolState * ethtool_state, + NMSettingEthtool *s_ethtool) +{ + NMEthtoolCoalesceState coalesce_old; + NMEthtoolCoalesceState coalesce_new; + gboolean has_old = FALSE; + GHashTable * hash; + GHashTableIter iter; + const char * name; + GVariant * variant; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_PLATFORM(platform)); + nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool)); + nm_assert(ethtool_state); + nm_assert(!ethtool_state->coalesce); + + hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE); + if (!hash) + return; + + g_hash_table_iter_init(&iter, hash); + while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) { + NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name); + + if (!nm_ethtool_id_is_coalesce(ethtool_id)) + continue; + + if (!has_old) { + if (!nm_platform_ethtool_get_link_coalesce(platform, + ethtool_state->ifindex, + &coalesce_old)) { + _LOGW(LOGD_DEVICE, "ethtool: failure getting coalesce settings (cannot read)"); + return; + } + has_old = TRUE; + coalesce_new = coalesce_old; + } + + nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32)); + coalesce_new.s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(ethtool_id)] = g_variant_get_uint32(variant); + } + + if (!has_old) + return; + + ethtool_state->coalesce = nm_memdup(&coalesce_old, sizeof(coalesce_old)); + + if (!nm_platform_ethtool_set_coalesce(platform, ethtool_state->ifindex, &coalesce_new)) { + _LOGW(LOGD_DEVICE, "ethtool: failure setting coalesce settings"); + return; + } + + _LOGD(LOGD_DEVICE, "ethtool: coalesce settings successfully set"); +} + +static void +_ethtool_ring_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state) +{ + gs_free NMEthtoolRingState *ring = NULL; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_PLATFORM(platform)); + nm_assert(ethtool_state); + + ring = g_steal_pointer(ðtool_state->ring); + if (!ring) + return; + + if (!nm_platform_ethtool_set_ring(platform, ethtool_state->ifindex, ring)) + _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more ring settings"); + else + _LOGD(LOGD_DEVICE, "ethtool: ring settings successfully reset"); +} + +static void +_ethtool_ring_set(NMDevice * self, + NMPlatform * platform, + EthtoolState * ethtool_state, + NMSettingEthtool *s_ethtool) +{ + NMEthtoolRingState ring_old; + NMEthtoolRingState ring_new; + GHashTable * hash; + GHashTableIter iter; + const char * name; + GVariant * variant; + gboolean has_old = FALSE; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_PLATFORM(platform)); + nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool)); + nm_assert(ethtool_state); + nm_assert(!ethtool_state->ring); + + hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE); + if (!hash) + return; + + g_hash_table_iter_init(&iter, hash); + while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) { + NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name); + guint32 u32; + + if (!nm_ethtool_id_is_ring(ethtool_id)) + continue; + + nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32)); + + if (!has_old) { + if (!nm_platform_ethtool_get_link_ring(platform, ethtool_state->ifindex, &ring_old)) { + _LOGW(LOGD_DEVICE, + "ethtool: failure setting ring options (cannot read existing setting)"); + return; + } + has_old = TRUE; + ring_new = ring_old; + } + + u32 = g_variant_get_uint32(variant); + + switch (ethtool_id) { + case NM_ETHTOOL_ID_RING_RX: + ring_new.rx_pending = u32; + break; + case NM_ETHTOOL_ID_RING_RX_JUMBO: + ring_new.rx_jumbo_pending = u32; + break; + case NM_ETHTOOL_ID_RING_RX_MINI: + ring_new.rx_mini_pending = u32; + break; + case NM_ETHTOOL_ID_RING_TX: + ring_new.tx_pending = u32; + break; + default: + nm_assert_not_reached(); + } + } + + if (!has_old) + return; + + ethtool_state->ring = nm_memdup(&ring_old, sizeof(ring_old)); + + if (!nm_platform_ethtool_set_ring(platform, ethtool_state->ifindex, &ring_new)) { + _LOGW(LOGD_DEVICE, "ethtool: failure setting ring settings"); + return; + } + + _LOGD(LOGD_DEVICE, "ethtool: ring settings successfully set"); +} + +static void +_ethtool_state_reset(NMDevice *self) +{ + NMPlatform * platform = nm_device_get_platform(self); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free EthtoolState *ethtool_state = g_steal_pointer(&priv->ethtool_state); + + if (!ethtool_state) + return; + + if (ethtool_state->features) + _ethtool_features_reset(self, platform, ethtool_state); + if (ethtool_state->coalesce) + _ethtool_coalesce_reset(self, platform, ethtool_state); + if (ethtool_state->ring) + _ethtool_ring_reset(self, platform, ethtool_state); +} + +static void +_ethtool_state_set(NMDevice *self) +{ + int ifindex; + NMPlatform * platform; + NMConnection * connection; + NMSettingEthtool *s_ethtool; + gs_free EthtoolState *ethtool_state = NULL; + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex <= 0) + return; + + platform = nm_device_get_platform(self); + nm_assert(platform); + + connection = nm_device_get_applied_connection(self); + if (!connection) + return; + + s_ethtool = NM_SETTING_ETHTOOL(nm_connection_get_setting(connection, NM_TYPE_SETTING_ETHTOOL)); + if (!s_ethtool) + return; + + ethtool_state = g_new0(EthtoolState, 1); + ethtool_state->ifindex = ifindex; + + _ethtool_features_set(self, platform, ethtool_state, s_ethtool); + _ethtool_coalesce_set(self, platform, ethtool_state, s_ethtool); + _ethtool_ring_set(self, platform, ethtool_state, s_ethtool); + + if (ethtool_state->features || ethtool_state->coalesce || ethtool_state->ring) + priv->ethtool_state = g_steal_pointer(ðtool_state); +} + +/*****************************************************************************/ + +static gboolean +is_loopback(NMDevice *self) +{ + return NM_IS_DEVICE_GENERIC(self) && NM_DEVICE_GET_PRIVATE(self)->ifindex == 1; +} + +gboolean +nm_device_is_vpn(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + /* NetworkManager currently treats VPN connections (loaded from NetworkManager VPN plugins) + * differently. Those are considered VPNs. + * However, some native device types may also be considered VPNs... + * + * We should avoid distinguishing between is-vpn and "regular" devices. Is an (unencrypted) + * IP tunnel a VPN? Is MACSec on top of an IP tunnel a VPN? + * Sometimes we differentiate, but avoid unless reasonable. */ + + return NM_IS_DEVICE_WIREGUARD(self); +} + +NMSettings * +nm_device_get_settings(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->settings; +} + +NMManager * +nm_device_get_manager(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->manager; +} + +NMNetns * +nm_device_get_netns(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->netns; +} + +NMDedupMultiIndex * +nm_device_get_multi_index(NMDevice *self) +{ + return nm_netns_get_multi_idx(nm_device_get_netns(self)); +} + +NMPlatform * +nm_device_get_platform(NMDevice *self) +{ + return nm_netns_get_platform(nm_device_get_netns(self)); +} + +static NMConnectivity * +concheck_get_mgr(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (G_UNLIKELY(!priv->concheck_mgr)) + priv->concheck_mgr = g_object_ref(nm_connectivity_get()); + return priv->concheck_mgr; +} + +NMIP4Config * +nm_device_ip4_config_new(NMDevice *self) +{ + return nm_ip4_config_new(nm_device_get_multi_index(self), nm_device_get_ip_ifindex(self)); +} + +NMIP6Config * +nm_device_ip6_config_new(NMDevice *self) +{ + return nm_ip6_config_new(nm_device_get_multi_index(self), nm_device_get_ip_ifindex(self)); +} + +NMIPConfig * +nm_device_ip_config_new(NMDevice *self, int addr_family) +{ + nm_assert_addr_family(addr_family); + + return NM_IS_IPv4(addr_family) ? (gpointer) nm_device_ip4_config_new(self) + : (gpointer) nm_device_ip6_config_new(self); +} + +NML3ConfigData * +nm_device_create_l3_config_data(NMDevice *self) +{ + int ifindex; + + nm_assert(NM_IS_DEVICE(self)); + + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex <= 0) + g_return_val_if_reached(NULL); + + return nm_l3_config_data_new(nm_device_get_multi_index(self), ifindex); +} + +static void +applied_config_clear(AppliedConfig *config) +{ + g_clear_object(&config->current); + g_clear_object(&config->orig); +} + +static void +applied_config_init(AppliedConfig *config, gpointer ip_config) +{ + nm_assert(!ip_config || (!config->orig && !config->current) + || nm_ip_config_get_addr_family(ip_config) + == nm_ip_config_get_addr_family(config->orig ?: config->current)); + nm_assert(!ip_config || NM_IS_IP_CONFIG(ip_config)); + + nm_g_object_ref(ip_config); + applied_config_clear(config); + config->orig = ip_config; +} + +static void +applied_config_init_new(AppliedConfig *config, NMDevice *self, int addr_family) +{ + gs_unref_object NMIPConfig *c = nm_device_ip_config_new(self, addr_family); + + applied_config_init(config, c); +} + +static NMIPConfig * +applied_config_get_current(AppliedConfig *config) +{ + return config->current ?: config->orig; +} + +static void +applied_config_add_address(AppliedConfig *config, const NMPlatformIPAddress *address) +{ + if (config->orig) + nm_ip_config_add_address(config->orig, address); + else + nm_assert(!config->current); + + if (config->current) + nm_ip_config_add_address(config->current, address); +} + +static void +applied_config_add_nameserver(AppliedConfig *config, const NMIPAddr *ns) +{ + if (config->orig) + nm_ip_config_add_nameserver(config->orig, ns); + else + nm_assert(!config->current); + + if (config->current) + nm_ip_config_add_nameserver(config->current, ns); +} + +static void +applied_config_add_search(AppliedConfig *config, const char *new) +{ + if (config->orig) + nm_ip_config_add_search(config->orig, new); + else + nm_assert(!config->current); + + if (config->current) + nm_ip_config_add_search(config->current, new); +} + +static void +applied_config_reset_searches(AppliedConfig *config) +{ + if (config->orig) + nm_ip_config_reset_searches(config->orig); + else + nm_assert(!config->current); + + if (config->current) + nm_ip_config_reset_searches(config->current); +} + +static void +applied_config_reset_nameservers(AppliedConfig *config) +{ + if (config->orig) + nm_ip_config_reset_nameservers(config->orig); + else + nm_assert(!config->current); + + if (config->current) + nm_ip_config_reset_nameservers(config->current); +} + +/*****************************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE( + _sys_iface_state_to_str, + NMDeviceSysIfaceState, + NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, "external"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_ASSUME, "assume"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_MANAGED, "managed"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_REMOVED, "removed"), ); + +NMDeviceSysIfaceState +nm_device_sys_iface_state_get(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_SYS_IFACE_STATE_EXTERNAL); + + return NM_DEVICE_GET_PRIVATE(self)->sys_iface_state; +} + +gboolean +nm_device_sys_iface_state_is_external(NMDevice *self) +{ + return NM_IN_SET(nm_device_sys_iface_state_get(self), NM_DEVICE_SYS_IFACE_STATE_EXTERNAL); +} + +gboolean +nm_device_sys_iface_state_is_external_or_assume(NMDevice *self) +{ + return NM_IN_SET(nm_device_sys_iface_state_get(self), + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_ASSUME); +} + +void +nm_device_sys_iface_state_set(NMDevice *self, NMDeviceSysIfaceState sys_iface_state) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IN_SET(sys_iface_state, + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_ASSUME, + NM_DEVICE_SYS_IFACE_STATE_MANAGED, + NM_DEVICE_SYS_IFACE_STATE_REMOVED)); + + priv = NM_DEVICE_GET_PRIVATE(self); + if (priv->sys_iface_state != sys_iface_state) { + _LOGT(LOGD_DEVICE, + "sys-iface-state: %s -> %s", + _sys_iface_state_to_str(priv->sys_iface_state), + _sys_iface_state_to_str(sys_iface_state)); + priv->sys_iface_state_ = sys_iface_state; + } + + /* this function only sets a flag, no immediate actions are initiated. + * + * If you change this, make sure that all callers are fine with such actions. */ + + nm_assert(priv->sys_iface_state == sys_iface_state); +} + +static void +_active_connection_set_state_flags_full(NMDevice * self, + NMActivationStateFlags flags, + NMActivationStateFlags mask) +{ + NMActiveConnection *ac; + + ac = NM_ACTIVE_CONNECTION(nm_device_get_act_request(self)); + if (ac) + nm_active_connection_set_state_flags_full(ac, flags, mask); +} + +static void +_active_connection_set_state_flags(NMDevice *self, NMActivationStateFlags flags) +{ + _active_connection_set_state_flags_full(self, flags, flags); +} + +/*****************************************************************************/ + +void +nm_device_assume_state_get(NMDevice * self, + gboolean * out_assume_state_guess_assume, + const char **out_assume_state_connection_uuid) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + NM_SET_OUT(out_assume_state_guess_assume, priv->assume_state_guess_assume); + NM_SET_OUT(out_assume_state_connection_uuid, priv->assume_state_connection_uuid); +} + +static void +_assume_state_set(NMDevice * self, + gboolean assume_state_guess_assume, + const char *assume_state_connection_uuid) +{ + NMDevicePrivate *priv; + + nm_assert(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + if (priv->assume_state_guess_assume == !!assume_state_guess_assume + && nm_streq0(priv->assume_state_connection_uuid, assume_state_connection_uuid)) + return; + + _LOGD(LOGD_DEVICE, + "assume-state: set guess-assume=%c, connection=%s%s%s", + assume_state_guess_assume ? '1' : '0', + NM_PRINT_FMT_QUOTE_STRING(assume_state_connection_uuid)); + priv->assume_state_guess_assume = assume_state_guess_assume; + g_free(priv->assume_state_connection_uuid); + priv->assume_state_connection_uuid = g_strdup(assume_state_connection_uuid); +} + +void +nm_device_assume_state_reset(NMDevice *self) +{ + g_return_if_fail(NM_IS_DEVICE(self)); + + _assume_state_set(self, FALSE, NULL); +} + +/*****************************************************************************/ + +static void +init_ip_config_dns_priority(NMDevice *self, NMIPConfig *config) +{ + const char *property; + int priority; + + property = (nm_ip_config_get_addr_family(config) == AF_INET) + ? NM_CON_DEFAULT("ipv4.dns-priority") + : NM_CON_DEFAULT("ipv6.dns-priority"); + + priority = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + property, + self, + G_MININT, + G_MAXINT, + 0); + + if (priority == 0) { + priority = + nm_device_is_vpn(self) ? NM_DNS_PRIORITY_DEFAULT_VPN : NM_DNS_PRIORITY_DEFAULT_NORMAL; + } + + nm_ip_config_set_dns_priority(config, priority); +} + +/*****************************************************************************/ + +static char * +nm_device_sysctl_ip_conf_get(NMDevice *self, int addr_family, const char *property) +{ + const char *ifname; + + nm_assert_addr_family(addr_family); + + ifname = nm_device_get_ip_iface_from_platform(self); + if (!ifname) + return NULL; + return nm_platform_sysctl_ip_conf_get(nm_device_get_platform(self), + addr_family, + ifname, + property); +} + +static gint64 +nm_device_sysctl_ip_conf_get_int_checked(NMDevice * self, + int addr_family, + const char *property, + guint base, + gint64 min, + gint64 max, + gint64 fallback) +{ + const char *ifname; + + nm_assert_addr_family(addr_family); + + ifname = nm_device_get_ip_iface_from_platform(self); + if (!ifname) { + errno = EINVAL; + return fallback; + } + return nm_platform_sysctl_ip_conf_get_int_checked(nm_device_get_platform(self), + addr_family, + ifname, + property, + base, + min, + max, + fallback); +} + +static void +set_ipv6_token(NMDevice *self, NMUtilsIPv6IfaceId iid, const char *token_str) +{ + NMPlatform * platform; + int ifindex; + const NMPlatformLink *link; + char buf[32]; + gint64 val; + + /* Setting the kernel token is not strictly necessary as the + * IPv6 address is generated in userspace. However it is + * convenient so that users can see the token with iproute + * ('ip token'). */ + platform = nm_device_get_platform(self); + ifindex = nm_device_get_ip_ifindex(self); + link = nm_platform_link_get(platform, ifindex); + + if (link && link->inet6_token.id == iid.id) { + _LOGT(LOGD_DEVICE | LOGD_IP6, "token %s already set", token_str); + return; + } + + /* The kernel allows setting a token only when 'accept_ra' + * is 1: temporarily flip it if necessary; unfortunately + * this will also generate an additional Router Solicitation + * from kernel. */ + val = nm_device_sysctl_ip_conf_get_int_checked(self, + AF_INET6, + "accept_ra", + 10, + G_MININT32, + G_MAXINT32, + 1); + if (val != 1) + nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "1"); + + nm_platform_link_set_ipv6_token(platform, ifindex, iid); + + if (val != 1) { + nm_sprintf_buf(buf, "%d", (int) val); + nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", buf); + } +} + +gboolean +nm_device_sysctl_ip_conf_set(NMDevice * self, + int addr_family, + const char *property, + const char *value) +{ + NMPlatform * platform = nm_device_get_platform(self); + gs_free char *value_to_free = NULL; + const char * ifname; + + nm_assert_addr_family(addr_family); + + ifname = nm_device_get_ip_iface_from_platform(self); + if (!ifname) + return FALSE; + + if (!value) { + /* Set to a default value when we've got a NULL @value. */ + value_to_free = nm_platform_sysctl_ip_conf_get(platform, addr_family, "default", property); + value = value_to_free; + if (!value) + return FALSE; + } + + return nm_platform_sysctl_ip_conf_set(platform, addr_family, ifname, property, value); +} + +/*****************************************************************************/ + +gboolean +nm_device_has_capability(NMDevice *self, NMDeviceCapabilities caps) +{ + return NM_FLAGS_ANY(NM_DEVICE_GET_PRIVATE(self)->capabilities, caps); +} + +static void +_add_capabilities(NMDevice *self, NMDeviceCapabilities capabilities) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!NM_FLAGS_ALL(priv->capabilities, capabilities)) { + priv->capabilities |= capabilities; + _notify(self, PROP_CAPABILITIES); + } +} + +/*****************************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE(_ip_state_to_string, + NMDeviceIPState, + NM_UTILS_LOOKUP_DEFAULT_WARN("unknown"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_NONE, "none"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_WAIT, "wait"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_CONF, "conf"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_DONE, "done"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_FAIL, "fail"), ); + +static void +_set_ip_state(NMDevice *self, int addr_family, NMDeviceIPState new_state) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + nm_assert_addr_family(addr_family); + + if (priv->ip_state_x[IS_IPv4] == new_state) + return; + + _LOGT(LOGD_DEVICE, + "ip%c-state: set to %d (%s)", + nm_utils_addr_family_to_char(addr_family), + (int) new_state, + _ip_state_to_string(new_state)); + + priv->ip_state_x_[IS_IPv4] = new_state; + + if (new_state == NM_DEVICE_IP_STATE_DONE) { + /* we only set the IPx_READY flag once we reach NM_DEVICE_IP_STATE_DONE state. We don't + * ever clear it, even if we later enter NM_DEVICE_IP_STATE_FAIL state. + * + * This is not documented/guaranteed behavior, but seems to make sense for now. */ + _active_connection_set_state_flags(self, + NM_IS_IPv4(addr_family) + ? NM_ACTIVATION_STATE_FLAG_IP4_READY + : NM_ACTIVATION_STATE_FLAG_IP6_READY); + } +} + +/*****************************************************************************/ + +const char * +nm_device_get_udi(NMDevice *self) +{ + g_return_val_if_fail(self != NULL, NULL); + + return NM_DEVICE_GET_PRIVATE(self)->udi; +} + +const char * +nm_device_get_iface(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->iface; +} + +static gboolean +_set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + int * p_ifindex; + + if (ifindex < 0) + ifindex = 0; + + p_ifindex = is_ip_ifindex ? &priv->ip_ifindex_ : &priv->ifindex_; + + if (*p_ifindex == ifindex) + return FALSE; + + *p_ifindex = ifindex; + + _LOGD(LOGD_DEVICE, "ifindex: set %sifindex %d", is_ip_ifindex ? "ip-" : "", ifindex); + + if (!is_ip_ifindex) + _notify(self, PROP_IFINDEX); + + if (priv->manager) + nm_manager_emit_device_ifindex_changed(priv->manager, self); + return TRUE; +} + +/** + * nm_device_take_over_link: + * @self: the #NMDevice + * @ifindex: a ifindex + * @old_name: (transfer full): on return, the name of the old link, if + * the link was renamed + * @error: location to store error, or %NULL + * + * Given an existing link, move it under the control of a device. In + * particular, the link will be renamed to match the device name. If the + * link was renamed, the old name is returned in @old_name. + * + * Returns: %TRUE if the device took control of the link, %FALSE otherwise + */ +gboolean +nm_device_take_over_link(NMDevice *self, int ifindex, char **old_name, GError **error) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const NMPlatformLink *plink; + NMPlatform * platform; + + nm_assert(ifindex > 0); + NM_SET_OUT(old_name, NULL); + + if (priv->ifindex > 0 && priv->ifindex != ifindex) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "the device already has ifindex %d", + priv->ifindex); + return FALSE; + } + + platform = nm_device_get_platform(self); + plink = nm_platform_link_get(platform, ifindex); + if (!plink) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "link %d not found", ifindex); + return FALSE; + } + + if (!nm_streq(plink->name, nm_device_get_iface(self))) { + gboolean up; + gboolean success; + gs_free char *name = NULL; + + up = NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP); + name = g_strdup(plink->name); + + /* Rename the link to the device ifname */ + if (up) + nm_platform_link_set_down(platform, ifindex); + success = nm_platform_link_set_name(platform, ifindex, nm_device_get_iface(self)); + if (up) + nm_platform_link_set_up(platform, ifindex, NULL); + + if (!success) { + nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "failure renaming link %d", ifindex); + return FALSE; + } + + NM_SET_OUT(old_name, g_steal_pointer(&name)); + } + + _set_ifindex(self, ifindex, FALSE); + + return TRUE; +} + +int +nm_device_get_ifindex(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), 0); + + return NM_DEVICE_GET_PRIVATE(self)->ifindex; +} + +/** + * nm_device_is_software: + * @self: the #NMDevice + * + * Indicates if the device is a software-based virtual device without + * backing hardware, which can be added and removed programmatically. + * + * Returns: %TRUE if the device is a software-based device + */ +gboolean +nm_device_is_software(NMDevice *self) +{ + return NM_FLAGS_HAS(NM_DEVICE_GET_PRIVATE(self)->capabilities, NM_DEVICE_CAP_IS_SOFTWARE); +} + +/** + * nm_device_is_real: + * @self: the #NMDevice + * + * Returns: %TRUE if the device exists, %FALSE if the device is a placeholder + */ +gboolean +nm_device_is_real(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + return NM_DEVICE_GET_PRIVATE(self)->real; +} + +const char * +nm_device_get_ip_iface(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(self != NULL, NULL); + + priv = NM_DEVICE_GET_PRIVATE(self); + /* If it's not set, default to iface */ + return priv->ip_iface ?: priv->iface; +} + +const char * +nm_device_get_ip_iface_from_platform(NMDevice *self) +{ + int ifindex; + + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex <= 0) + return NULL; + + return nm_platform_link_get_name(nm_device_get_platform(self), ifindex); +} + +int +nm_device_get_ip_ifindex(const NMDevice *self) +{ + const NMDevicePrivate *priv; + + g_return_val_if_fail(self != NULL, 0); + + priv = NM_DEVICE_GET_PRIVATE(self); + /* If it's not set, default to ifindex */ + return priv->ip_iface ? priv->ip_ifindex : priv->ifindex; +} + +static void +_set_ip_ifindex(NMDevice *self, int ifindex, const char *ifname) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatform * platform; + gboolean eq_name; + + /* normalize arguments */ + if (ifindex <= 0) { + ifindex = 0; + ifname = NULL; + } + + eq_name = nm_streq0(priv->ip_iface, ifname); + + if (eq_name && priv->ip_ifindex == ifindex) + return; + + _LOGD(LOGD_DEVICE, + "ip-ifindex: update ip-interface to %s%s%s, ifindex %d", + NM_PRINT_FMT_QUOTE_STRING(ifname), + ifindex); + + _set_ifindex(self, ifindex, TRUE); + + if (!eq_name) { + g_free(priv->ip_iface_); + priv->ip_iface_ = g_strdup(ifname); + _notify(self, PROP_IP_IFACE); + } + + if (priv->ip_ifindex > 0) { + platform = nm_device_get_platform(self); + + nm_platform_process_events_ensure_link(platform, priv->ip_ifindex, priv->ip_iface); + + if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) + nm_platform_link_set_user_ipv6ll_enabled(platform, priv->ip_ifindex, TRUE); + + if (!nm_platform_link_is_up(platform, priv->ip_ifindex)) + nm_platform_link_set_up(platform, priv->ip_ifindex, NULL); + } + + /* We don't care about any saved values from the old iface */ + g_hash_table_remove_all(priv->ip6_saved_properties); +} + +gboolean +nm_device_set_ip_ifindex(NMDevice *self, int ifindex) +{ + char ifname_buf[IFNAMSIZ]; + const char *ifname = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(nm_device_is_activating(self), FALSE); + + if (ifindex > 0) { + ifname = nm_platform_if_indextoname(nm_device_get_platform(self), ifindex, ifname_buf); + if (!ifname) + _LOGW(LOGD_DEVICE, "ip-ifindex: ifindex %d not found", ifindex); + } + + _set_ip_ifindex(self, ifindex, ifname); + return ifindex > 0; +} + +/** + * nm_device_set_ip_iface: + * @self: the #NMDevice + * @ifname: the new IP interface name + * + * Updates the IP interface name and possibly the ifindex. + * + * Returns: %TRUE if an interface with name @ifname exists, + * and %FALSE, if @ifname is %NULL or no such interface exists. + */ +gboolean +nm_device_set_ip_iface(NMDevice *self, const char *ifname) +{ + int ifindex = 0; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(nm_device_is_activating(self), FALSE); + + if (ifname) { + ifindex = nm_platform_if_nametoindex(nm_device_get_platform(self), ifname); + if (ifindex <= 0) + _LOGW(LOGD_DEVICE, "ip-ifindex: ifname %s not found", ifname); + } + + _set_ip_ifindex(self, ifindex, ifname); + return ifindex > 0; +} + +/*****************************************************************************/ + +int +nm_device_parent_get_ifindex(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), 0); + + priv = NM_DEVICE_GET_PRIVATE(self); + return priv->parent_ifindex; +} + +NMDevice * +nm_device_parent_get_device(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->parent_device.obj; +} + +static void +parent_changed_notify(NMDevice *self, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent) +{ + /* empty handler to allow subclasses to always chain up the virtual function. */ +} + +static gboolean +_parent_set_ifindex(NMDevice *self, int parent_ifindex, gboolean force_check) +{ + NMDevicePrivate *priv; + NMDevice * parent_device; + gboolean changed = FALSE; + int old_ifindex; + gs_unref_object NMDevice *old_device = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (parent_ifindex <= 0) + parent_ifindex = 0; + + old_ifindex = priv->parent_ifindex; + + if (priv->parent_ifindex == parent_ifindex) { + if (parent_ifindex > 0) { + if (!force_check && priv->parent_device.obj + && nm_device_get_ifindex(priv->parent_device.obj) == parent_ifindex) + return FALSE; + } else { + if (!priv->parent_device.obj) + return FALSE; + } + } else { + priv->parent_ifindex = parent_ifindex; + changed = TRUE; + } + + if (parent_ifindex > 0) { + parent_device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, parent_ifindex); + if (parent_device == self) + parent_device = NULL; + } else + parent_device = NULL; + + if (parent_device != priv->parent_device.obj) { + old_device = nm_g_object_ref(priv->parent_device.obj); + nm_dbus_track_obj_path_set(&priv->parent_device, parent_device, TRUE); + changed = TRUE; + } + + if (changed) { + if (priv->parent_ifindex <= 0) + _LOGD(LOGD_DEVICE, "parent: clear"); + else if (!priv->parent_device.obj) + _LOGD(LOGD_DEVICE, "parent: ifindex %d, no device", priv->parent_ifindex); + else { + _LOGD(LOGD_DEVICE, + "parent: ifindex %d, device %p, %s", + priv->parent_ifindex, + priv->parent_device.obj, + nm_device_get_iface(priv->parent_device.obj)); + } + + NM_DEVICE_GET_CLASS(self)->parent_changed_notify(self, + old_ifindex, + old_device, + priv->parent_ifindex, + priv->parent_device.obj); + } + return changed; +} + +void +nm_device_parent_set_ifindex(NMDevice *self, int parent_ifindex) +{ + _parent_set_ifindex(self, parent_ifindex, FALSE); +} + +gboolean +nm_device_parent_notify_changed(NMDevice *self, NMDevice *change_candidate, gboolean device_removed) +{ + NMDevicePrivate *priv; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(NM_IS_DEVICE(change_candidate)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->parent_ifindex > 0) { + if (priv->parent_device.obj == change_candidate + || priv->parent_ifindex == nm_device_get_ifindex(change_candidate)) + return _parent_set_ifindex(self, priv->parent_ifindex, device_removed); + } + return FALSE; +} + +/*****************************************************************************/ + +const char * +nm_device_parent_find_for_connection(NMDevice *self, const char *current_setting_parent) +{ + const char *new_parent; + NMDevice * parent_device; + + parent_device = nm_device_parent_get_device(self); + if (!parent_device) + return NULL; + + new_parent = nm_device_get_iface(parent_device); + if (!new_parent) + return NULL; + + if (current_setting_parent && !nm_streq(current_setting_parent, new_parent) + && nm_utils_is_uuid(current_setting_parent)) { + NMSettingsConnection *parent_connection; + + /* Don't change a parent specified by UUID if it's still valid */ + parent_connection = nm_settings_get_connection_by_uuid(nm_device_get_settings(self), + current_setting_parent); + if (parent_connection + && nm_device_check_connection_compatible( + parent_device, + nm_settings_connection_get_connection(parent_connection), + NULL)) + return current_setting_parent; + } + + return new_parent; +} + +/*****************************************************************************/ + +static void +_stats_update_counters(NMDevice *self, guint64 tx_bytes, guint64 rx_bytes) +{ + NMDevicePrivate *priv; + gboolean tx_changed = FALSE; + gboolean rx_changed = FALSE; + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->stats.tx_bytes != tx_bytes) { + priv->stats.tx_bytes = tx_bytes; + tx_changed = TRUE; + } + if (priv->stats.rx_bytes != rx_bytes) { + priv->stats.rx_bytes = rx_bytes; + rx_changed = TRUE; + } + + nm_gobject_notify_together(self, + tx_changed ? PROP_STATISTICS_TX_BYTES : PROP_0, + rx_changed ? PROP_STATISTICS_RX_BYTES : PROP_0); +} + +static void +_stats_update_counters_from_pllink(NMDevice *self, const NMPlatformLink *pllink) +{ + _stats_update_counters(self, pllink->tx_bytes, pllink->rx_bytes); +} + +static gboolean +_stats_timeout_cb(gpointer user_data) +{ + NMDevice *self = user_data; + int ifindex; + + ifindex = nm_device_get_ip_ifindex(self); + + _LOGT(LOGD_DEVICE, "stats: refresh %d", ifindex); + + if (ifindex > 0) + nm_platform_link_refresh(nm_device_get_platform(self), ifindex); + + return G_SOURCE_CONTINUE; +} + +static guint +_stats_refresh_rate_real(guint refresh_rate_ms) +{ + const guint STATS_REFRESH_RATE_MS_MIN = 200; + + if (refresh_rate_ms == 0) + return 0; + + if (refresh_rate_ms < STATS_REFRESH_RATE_MS_MIN) { + /* you cannot set the refresh-rate arbitrarily small. E.g. + * setting to 1ms is just killing. Have a lowest number. */ + return STATS_REFRESH_RATE_MS_MIN; + } + + return refresh_rate_ms; +} + +static void +_stats_set_refresh_rate(NMDevice *self, guint refresh_rate_ms) +{ + NMDevicePrivate *priv; + int ifindex; + guint old_rate; + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->stats.refresh_rate_ms == refresh_rate_ms) + return; + + old_rate = priv->stats.refresh_rate_ms; + priv->stats.refresh_rate_ms = refresh_rate_ms; + _notify(self, PROP_STATISTICS_REFRESH_RATE_MS); + + _LOGD(LOGD_DEVICE, "stats: set refresh to %u ms", priv->stats.refresh_rate_ms); + + if (!nm_device_is_real(self)) + return; + + refresh_rate_ms = _stats_refresh_rate_real(refresh_rate_ms); + if (_stats_refresh_rate_real(old_rate) == refresh_rate_ms) + return; + + nm_clear_g_source(&priv->stats.timeout_id); + + if (!refresh_rate_ms) + return; + + /* trigger an initial refresh of the data whenever the refresh-rate changes. + * As we process the result in an idle handler with device_link_changed(), + * we don't get the result right away. */ + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex > 0) + nm_platform_link_refresh(nm_device_get_platform(self), ifindex); + + priv->stats.timeout_id = g_timeout_add(refresh_rate_ms, _stats_timeout_cb, self); +} + +/*****************************************************************************/ + +static gboolean +get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *out_iid) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatform * platform = nm_device_get_platform(self); + const NMPlatformLink *pllink; + const guint8 * hwaddr; + guint8 pseudo_hwaddr[ETH_ALEN]; + gsize hwaddr_len; + int ifindex; + gboolean success; + + /* If we get here, we *must* have a kernel netdev, which implies an ifindex */ + ifindex = nm_device_get_ip_ifindex(self); + g_return_val_if_fail(ifindex > 0, FALSE); + + pllink = nm_platform_link_get(platform, ifindex); + if (!pllink || NM_IN_SET(pllink->type, NM_LINK_TYPE_NONE, NM_LINK_TYPE_UNKNOWN)) + return FALSE; + + hwaddr = nmp_link_address_get(&pllink->l_address, &hwaddr_len); + if (hwaddr_len <= 0) + return FALSE; + + if (pllink->type == NM_LINK_TYPE_6LOWPAN) { + /* If the underlying IEEE 802.15.4 device has a short address we generate + * a "pseudo 48-bit address" that's to be used in the same fashion as a + * wired Ethernet address. The mechanism is specified in Section 6. of + * RFC 4944 */ + guint16 pan_id; + guint16 short_addr; + + short_addr = nm_platform_wpan_get_short_addr(platform, pllink->parent); + if (short_addr != G_MAXUINT16) { + pan_id = nm_platform_wpan_get_pan_id(platform, pllink->parent); + pseudo_hwaddr[0] = short_addr & 0xff; + pseudo_hwaddr[1] = (short_addr >> 8) & 0xff; + pseudo_hwaddr[2] = 0; + pseudo_hwaddr[3] = 0; + pseudo_hwaddr[4] = pan_id & 0xff; + pseudo_hwaddr[5] = (pan_id >> 8) & 0xff; + + hwaddr = pseudo_hwaddr; + hwaddr_len = G_N_ELEMENTS(pseudo_hwaddr); + } + } + + success = nm_utils_get_ipv6_interface_identifier(pllink->type, + hwaddr, + hwaddr_len, + priv->dev_id, + out_iid); + if (!success) { + _LOGW(LOGD_PLATFORM, + "failed to generate interface identifier " + "for link type %u hwaddr_len %zu", + pllink->type, + hwaddr_len); + } + return success; +} + +/** + * nm_device_get_ip_iface_identifier: + * @self: an #NMDevice + * @iid: where to place the interface identifier + * @ignore_token: force creation of a non-tokenized address + * + * Return the interface's identifier for the EUI64 address generation mode. + * It's either a manually set token or and identifier generated in a + * hardware-specific way. + * + * Unless @ignore_token is set the token is preferred. That is the case + * for link-local addresses (to mimic kernel behavior). + * + * Returns: #TRUE if the @iid could be set + */ +static gboolean +nm_device_get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *iid, gboolean ignore_token) +{ + NMSettingIP6Config *s_ip6; + const char * token = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + if (!ignore_token) { + s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG); + + g_return_val_if_fail(s_ip6, FALSE); + + token = nm_setting_ip6_config_get_token(s_ip6); + } + if (token) + return nm_utils_ipv6_interface_identifier_get_from_token(iid, token); + else + return NM_DEVICE_GET_CLASS(self)->get_ip_iface_identifier(self, iid); +} + +const char * +nm_device_get_driver(NMDevice *self) +{ + g_return_val_if_fail(self != NULL, NULL); + + return NM_DEVICE_GET_PRIVATE(self)->driver; +} + +const char * +nm_device_get_driver_version(NMDevice *self) +{ + g_return_val_if_fail(self != NULL, NULL); + + return NM_DEVICE_GET_PRIVATE(self)->driver_version; +} + +NMDeviceType +nm_device_get_device_type(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_TYPE_UNKNOWN); + + return NM_DEVICE_GET_PRIVATE(self)->type; +} + +NMLinkType +nm_device_get_link_type(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_LINK_TYPE_UNKNOWN); + + return NM_DEVICE_GET_PRIVATE(self)->link_type; +} + +/** + * nm_device_get_metered: + * @setting: the #NMDevice + * + * Returns: the #NMDevice:metered property of the device. + * + * Since: 1.2 + **/ +NMMetered +nm_device_get_metered(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_METERED_UNKNOWN); + + return NM_DEVICE_GET_PRIVATE(self)->metered; +} + +guint32 +nm_device_get_route_metric_default(NMDeviceType device_type) +{ + /* Device 'priority' is used for the default route-metric and is based on + * the device type. The settings ipv4.route-metric and ipv6.route-metric + * can overwrite this default. + * + * For both IPv4 and IPv6 we use the same default values. + * + * The route-metric is used for the metric of the routes of device. + * This also applies to the default route. Therefore it affects also + * which device is the "best". + * + * For comparison, note that iproute2 by default adds IPv4 routes with + * metric 0, and IPv6 routes with metric 1024. The latter is the IPv6 + * "user default" in the kernel (NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6). + * In kernel, the full uint32_t range is available for route + * metrics (except for IPv6, where 0 means 1024). + */ + + switch (device_type) { + /* 50 is also used for VPN plugins (NM_VPN_ROUTE_METRIC_DEFAULT). + * + * Note that returning 50 from this function means that this device-type is + * in some aspects a VPN. */ + case NM_DEVICE_TYPE_WIREGUARD: + return NM_VPN_ROUTE_METRIC_DEFAULT; + + case NM_DEVICE_TYPE_ETHERNET: + case NM_DEVICE_TYPE_VETH: + return 100; + case NM_DEVICE_TYPE_MACSEC: + return 125; + case NM_DEVICE_TYPE_INFINIBAND: + return 150; + case NM_DEVICE_TYPE_ADSL: + return 200; + case NM_DEVICE_TYPE_WIMAX: + return 250; + case NM_DEVICE_TYPE_BOND: + return 300; + case NM_DEVICE_TYPE_TEAM: + return 350; + case NM_DEVICE_TYPE_VLAN: + return 400; + case NM_DEVICE_TYPE_MACVLAN: + return 410; + case NM_DEVICE_TYPE_BRIDGE: + return 425; + case NM_DEVICE_TYPE_TUN: + return 450; + case NM_DEVICE_TYPE_PPP: + return 460; + case NM_DEVICE_TYPE_VRF: + return 470; + case NM_DEVICE_TYPE_VXLAN: + return 500; + case NM_DEVICE_TYPE_DUMMY: + return 550; + case NM_DEVICE_TYPE_WIFI: + return 600; + case NM_DEVICE_TYPE_OLPC_MESH: + return 650; + case NM_DEVICE_TYPE_IP_TUNNEL: + return 675; + case NM_DEVICE_TYPE_MODEM: + return 700; + case NM_DEVICE_TYPE_BT: + return 750; + case NM_DEVICE_TYPE_6LOWPAN: + return 775; + case NM_DEVICE_TYPE_OVS_BRIDGE: + case NM_DEVICE_TYPE_OVS_INTERFACE: + case NM_DEVICE_TYPE_OVS_PORT: + return 800; + case NM_DEVICE_TYPE_WPAN: + return 850; + case NM_DEVICE_TYPE_WIFI_P2P: + case NM_DEVICE_TYPE_GENERIC: + return 950; + case NM_DEVICE_TYPE_UNKNOWN: + return 10000; + case NM_DEVICE_TYPE_UNUSED1: + case NM_DEVICE_TYPE_UNUSED2: + /* omit default: to get compiler warning about missing switch cases */ + break; + } + return 11000; +} + +static gboolean +default_route_metric_penalty_detect(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + /* currently we don't differentiate between IPv4 and IPv6 when detecting + * connectivity. */ + if (priv->concheck_x[IS_IPv4].state != NM_CONNECTIVITY_FULL + && nm_connectivity_check_enabled(concheck_get_mgr(self))) + return TRUE; + + return FALSE; +} + +static guint32 +default_route_metric_penalty_get(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (NM_IS_IPv4(addr_family) ? priv->default_route_metric_penalty_ip4_has + : priv->default_route_metric_penalty_ip6_has) + return 20000; + return 0; +} + +guint32 +nm_device_get_route_metric(NMDevice *self, int addr_family) +{ + gint64 route_metric; + NMSettingIPConfig *s_ip; + NMConnection * connection; + const char * property; + + g_return_val_if_fail(NM_IS_DEVICE(self), G_MAXUINT32); + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), G_MAXUINT32); + + connection = nm_device_get_applied_connection(self); + if (connection) { + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + + /* Slave interfaces don't have IP settings, but we may get here when + * external changes are made or when noticing IP changes when starting + * the slave connection. + */ + if (s_ip) { + route_metric = nm_setting_ip_config_get_route_metric(s_ip); + if (route_metric >= 0) + goto out; + } + } + + /* use the current NMConfigData, which makes this configuration reloadable. + * Note that that means that the route-metric might change between SIGHUP. + * You must cache the returned value if that is a problem. */ + property = NM_IS_IPv4(addr_family) ? NM_CON_DEFAULT("ipv4.route-metric") + : NM_CON_DEFAULT("ipv6.route-metric"); + route_metric = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + property, + self, + 0, + G_MAXUINT32, + -1); + if (route_metric >= 0) + goto out; + + route_metric = nm_manager_device_route_metric_reserve(NM_MANAGER_GET, + nm_device_get_ip_ifindex(self), + nm_device_get_device_type(self)); +out: + return nm_utils_ip_route_metric_normalize(addr_family, route_metric); +} + +guint32 +nm_device_get_route_table(NMDevice *self, int addr_family) +{ + guint32 route_table; + + g_return_val_if_fail(NM_IS_DEVICE(self), RT_TABLE_MAIN); + + route_table = _prop_get_ipvx_route_table(self, addr_family); + return route_table ?: (guint32) RT_TABLE_MAIN; +} + +static NMIPRouteTableSyncMode +_get_route_table_sync_mode_stateful(NMDevice *self, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMDedupMultiIter ipconf_iter; + gboolean all_sync_now; + gboolean all_sync_eff; + + all_sync_now = _prop_get_ipvx_route_table(self, addr_family) != 0u; + + if (!all_sync_now) { + const NMPlatformIPRoute *route; + + /* If there's a local route switch to all-sync in order + * to properly manage the local table */ + nm_ip_config_iter_ip_route_for_each (&ipconf_iter, priv->con_ip_config_x[IS_IPv4], &route) { + if (nm_platform_route_type_uncoerce(route->type_coerced) == RTN_LOCAL) { + all_sync_now = TRUE; + break; + } + } + } + + if (all_sync_now) + all_sync_eff = TRUE; + else { + /* When we change from all-sync to no all-sync, we do a last all-sync one + * more time. For that, we determine the effective all-state based on the + * cached/previous all-sync flag. + * + * The purpose of this is to support reapply of route-table (and thus the + * all-sync mode). If reapply toggles from all-sync to no-all-sync, we must + * sync one last time. */ + if (NM_IS_IPv4(addr_family)) + all_sync_eff = priv->v4_route_table_all_sync_before; + else + all_sync_eff = priv->v6_route_table_all_sync_before; + } + + if (NM_IS_IPv4(addr_family)) + priv->v4_route_table_all_sync_before = all_sync_now; + else + priv->v6_route_table_all_sync_before = all_sync_now; + + return all_sync_eff ? NM_IP_ROUTE_TABLE_SYNC_MODE_ALL : NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN; +} + +const NMPObject * +nm_device_get_best_default_route(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + switch (addr_family) { + case AF_INET: + return priv->ip_config_4 ? nm_ip4_config_best_default_route_get(priv->ip_config_4) : NULL; + case AF_INET6: + return priv->ip_config_6 ? nm_ip6_config_best_default_route_get(priv->ip_config_6) : NULL; + case AF_UNSPEC: + return (priv->ip_config_4 ? nm_ip4_config_best_default_route_get(priv->ip_config_4) : NULL) + ?: (priv->ip_config_6 ? nm_ip6_config_best_default_route_get(priv->ip_config_6) + : NULL); + default: + g_return_val_if_reached(NULL); + } +} + +const char * +nm_device_get_type_desc(NMDevice *self) +{ + g_return_val_if_fail(self != NULL, NULL); + + return NM_DEVICE_GET_PRIVATE(self)->type_desc; +} + +const char * +nm_device_get_type_description(NMDevice *self) +{ + g_return_val_if_fail(self != NULL, NULL); + + /* Beware: this function should return the same + * value as nm_device_get_type_description() in libnm. */ + + return NM_DEVICE_GET_CLASS(self)->get_type_description(self); +} + +static const char * +get_type_description(NMDevice *self) +{ + NMDeviceClass *klass; + + nm_assert(NM_IS_DEVICE(self)); + + /* the default implementation for the description just returns the (modified) + * class name and depends entirely on the type of self. Note that we cache the + * description in the klass itself. + * + * Also note, that as the GObject class gets inited, it inherrits the fields + * of the parent class. That means, if NMDeviceVethClass was initialized after + * NMDeviceEthernetClass already has the description cached in the class + * (because we already fetched the description for an ethernet device), + * then default_type_description will wrongly contain "ethernet". + * To avoid that, and catch the situation, also cache the klass for + * which the description was cached. If that doesn't match, it was + * inherited and we need to reset it. */ + klass = NM_DEVICE_GET_CLASS(self); + if (G_UNLIKELY(klass->default_type_description_klass != klass)) { + const char *typename; + gs_free char *s = NULL; + + typename = G_OBJECT_TYPE_NAME(self); + if (g_str_has_prefix(typename, "NMDevice")) { + typename += 8; + if (nm_streq(typename, "Veth")) + typename = "Ethernet"; + } + s = g_ascii_strdown(typename, -1); + klass->default_type_description = g_intern_string(s); + klass->default_type_description_klass = klass; + } + + nm_assert(klass->default_type_description); + return klass->default_type_description; +} + +gboolean +nm_device_has_carrier(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->carrier; +} + +NMActRequest * +nm_device_get_act_request(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->act_request.obj; +} + +NMActivationStateFlags +nm_device_get_activation_state_flags(NMDevice *self) +{ + NMActRequest *ac; + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_ACTIVATION_STATE_FLAG_NONE); + + ac = NM_DEVICE_GET_PRIVATE(self)->act_request.obj; + if (!ac) + return NM_ACTIVATION_STATE_FLAG_NONE; + return nm_active_connection_get_state_flags(NM_ACTIVE_CONNECTION(ac)); +} + +NMSettingsConnection * +nm_device_get_settings_connection(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + priv = NM_DEVICE_GET_PRIVATE(self); + + return priv->act_request.obj ? nm_act_request_get_settings_connection(priv->act_request.obj) + : NULL; +} + +NMConnection * +nm_device_get_settings_connection_get_connection(NMDevice *self) +{ + NMSettingsConnection *sett_con; + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->act_request.obj) + return NULL; + + sett_con = nm_act_request_get_settings_connection(priv->act_request.obj); + if (!sett_con) + return NULL; + + return nm_settings_connection_get_connection(sett_con); +} + +NMConnection * +nm_device_get_applied_connection(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + priv = NM_DEVICE_GET_PRIVATE(self); + + return priv->act_request.obj ? nm_act_request_get_applied_connection(priv->act_request.obj) + : NULL; +} + +gboolean +nm_device_has_unmodified_applied_connection(NMDevice *self, NMSettingCompareFlags compare_flags) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->act_request.obj) + return FALSE; + + return nm_active_connection_has_unmodified_applied_connection( + (NMActiveConnection *) priv->act_request.obj, + compare_flags); +} + +gpointer +nm_device_get_applied_setting(NMDevice *self, GType setting_type) +{ + NMConnection *connection; + + connection = nm_device_get_applied_connection(self); + return connection ? nm_connection_get_setting(connection, setting_type) : NULL; +} + +RfKillType +nm_device_get_rfkill_type(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + return NM_DEVICE_GET_PRIVATE(self)->rfkill_type; +} + +static const char * +nm_device_get_physical_port_id(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->physical_port_id; +} + +/*****************************************************************************/ + +typedef enum { + CONCHECK_SCHEDULE_UPDATE_INTERVAL, + CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART, + CONCHECK_SCHEDULE_CHECK_EXTERNAL, + CONCHECK_SCHEDULE_CHECK_PERIODIC, + CONCHECK_SCHEDULE_RETURNED_MIN, + CONCHECK_SCHEDULE_RETURNED_BUMP, + CONCHECK_SCHEDULE_RETURNED_MAX, +} ConcheckScheduleMode; + +static NMDeviceConnectivityHandle *concheck_start(NMDevice * self, + int addr_family, + NMDeviceConnectivityCallback callback, + gpointer user_data, + gboolean is_periodic); + +static void +concheck_periodic_schedule_set(NMDevice *self, int addr_family, ConcheckScheduleMode mode); + +static gboolean +_concheck_periodic_timeout_cb(NMDevice *self, int addr_family) +{ + _LOGt(LOGD_CONCHECK, + "connectivity: [IPv%c] periodic timeout", + nm_utils_addr_family_to_char(addr_family)); + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_CHECK_PERIODIC); + return G_SOURCE_REMOVE; +} + +static gboolean +concheck_ip4_periodic_timeout_cb(gpointer user_data) +{ + return _concheck_periodic_timeout_cb(user_data, AF_INET); +} + +static gboolean +concheck_ip6_periodic_timeout_cb(gpointer user_data) +{ + return _concheck_periodic_timeout_cb(user_data, AF_INET6); +} + +static gboolean +concheck_is_possible(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!nm_device_is_real(self) || is_loopback(self)) + return FALSE; + + /* we enable periodic checks for every device state (except UNKNOWN). Especially with + * unmanaged devices, it is interesting to know whether we have connectivity on that device. */ + if (priv->state == NM_DEVICE_STATE_UNKNOWN) + return FALSE; + + return TRUE; +} + +static gboolean +concheck_periodic_schedule_do(NMDevice *self, int addr_family, gint64 now_ns) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean periodic_check_disabled = FALSE; + gint64 expiry, tdiff; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + /* we always cancel whatever was pending. */ + if (nm_clear_g_source(&priv->concheck_x[IS_IPv4].p_cur_id)) + periodic_check_disabled = TRUE; + + if (priv->concheck_x[IS_IPv4].p_max_interval == 0) { + /* periodic checks are disabled */ + goto out; + } + + if (!concheck_is_possible(self)) + goto out; + + nm_assert(now_ns > 0); + nm_assert(priv->concheck_x[IS_IPv4].p_cur_interval > 0); + + /* we schedule the timeout based on our current settings cur-interval and cur-basetime. + * Before calling concheck_periodic_schedule_do(), make sure that these properties are + * correct. */ + + expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns + + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC); + tdiff = expiry - now_ns; + + _LOGT(LOGD_CONCHECK, + "connectivity: [IPv%c] periodic-check: %sscheduled in %lld milliseconds (%u seconds " + "interval)", + nm_utils_addr_family_to_char(addr_family), + periodic_check_disabled ? "re-" : "", + (long long) (tdiff / NM_UTILS_NSEC_PER_MSEC), + priv->concheck_x[IS_IPv4].p_cur_interval); + + priv->concheck_x[IS_IPv4].p_cur_id = + g_timeout_add(NM_MAX((gint64) 0, tdiff) / NM_UTILS_NSEC_PER_MSEC, + IS_IPv4 ? concheck_ip4_periodic_timeout_cb : concheck_ip6_periodic_timeout_cb, + self); + return TRUE; +out: + if (periodic_check_disabled) { + _LOGT(LOGD_CONCHECK, + "connectivity: [IPv%c] periodic-check: unscheduled", + nm_utils_addr_family_to_char(addr_family)); + } + return FALSE; +} + +#define CONCHECK_P_PROBE_INTERVAL 1 + +static void +concheck_periodic_schedule_set(NMDevice *self, int addr_family, ConcheckScheduleMode mode) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gint64 new_expiry, exp_expiry, cur_expiry, tdiff; + gint64 now_ns = 0; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + if (priv->concheck_x[IS_IPv4].p_max_interval == 0) { + /* periodic check is disabled. Nothing to do. */ + return; + } + + if (!priv->concheck_x[IS_IPv4].p_cur_id) { + /* we currently don't have a timeout scheduled. No need to reschedule + * another one... */ + if (NM_IN_SET(mode, + CONCHECK_SCHEDULE_UPDATE_INTERVAL, + CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART)) { + /* ... unless, we are about to start periodic checks after update-interval. + * In this case, fall through and restart the periodic checks below. */ + mode = CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART; + } else + return; + } + + switch (mode) { + case CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART: + priv->concheck_x[IS_IPv4].p_cur_interval = + NM_MIN(priv->concheck_x[IS_IPv4].p_max_interval, CONCHECK_P_PROBE_INTERVAL); + priv->concheck_x[IS_IPv4].p_cur_basetime_ns = + nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns); + if (concheck_periodic_schedule_do(self, addr_family, now_ns)) + concheck_start(self, addr_family, NULL, NULL, TRUE); + return; + + case CONCHECK_SCHEDULE_UPDATE_INTERVAL: + /* called with "UPDATE_INTERVAL" and already have a p_cur_id scheduled. */ + + nm_assert(priv->concheck_x[IS_IPv4].p_max_interval > 0); + nm_assert(priv->concheck_x[IS_IPv4].p_cur_interval > 0); + + if (priv->concheck_x[IS_IPv4].p_cur_interval <= priv->concheck_x[IS_IPv4].p_max_interval) { + /* we currently have a shorter interval set, than what we now have. Either, + * because we are probing, or because the previous max interval was shorter. + * + * Either way, the current timer is set just fine. Nothing to do, we will + * probe our way up. */ + return; + } + + cur_expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns + + (priv->concheck_x[IS_IPv4].p_max_interval * NM_UTILS_NSEC_PER_SEC); + nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns); + + priv->concheck_x[IS_IPv4].p_cur_interval = priv->concheck_x[IS_IPv4].p_max_interval; + if (cur_expiry <= now_ns) { + /* Since the last time we scheduled a periodic check, already more than the + * new max_interval passed. We need to start a check right away (and + * schedule a timeout in cur-interval in the future). */ + priv->concheck_x[IS_IPv4].p_cur_basetime_ns = now_ns; + if (concheck_periodic_schedule_do(self, addr_family, now_ns)) + concheck_start(self, addr_family, NULL, NULL, TRUE); + } else { + /* we are reducing the max-interval to a shorter interval that we have currently + * scheduled (with cur_interval). + * + * However, since the last time we scheduled the check, not even the new max-interval + * expired. All we need to do, is reschedule the timer to expire sooner. The cur_basetime + * is unchanged. */ + concheck_periodic_schedule_do(self, addr_family, now_ns); + } + return; + + case CONCHECK_SCHEDULE_CHECK_EXTERNAL: + /* a external connectivity check delays our periodic check. We reset the counter. */ + priv->concheck_x[IS_IPv4].p_cur_basetime_ns = + nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns); + concheck_periodic_schedule_do(self, addr_family, now_ns); + return; + + case CONCHECK_SCHEDULE_CHECK_PERIODIC: + { + gboolean any_periodic_pending; + NMDeviceConnectivityHandle *handle; + guint old_interval = priv->concheck_x[IS_IPv4].p_cur_interval; + + any_periodic_pending = FALSE; + c_list_for_each_entry (handle, &priv->concheck_lst_head, concheck_lst) { + if (handle->addr_family != addr_family) + continue; + if (handle->is_periodic_bump) { + handle->is_periodic_bump = FALSE; + handle->is_periodic_bump_on_complete = FALSE; + any_periodic_pending = TRUE; + } + } + if (any_periodic_pending) { + /* we reached a timeout to schedule a new periodic request, however we still + * have period requests pending that didn't complete yet. We need to bump the + * interval already. */ + priv->concheck_x[IS_IPv4].p_cur_interval = + NM_MIN(old_interval * 2, priv->concheck_x[IS_IPv4].p_max_interval); + } + + /* we just reached a timeout. The expected expiry (exp_expiry) should be + * pretty close to now_ns. + * + * We want to reschedule the timeout at exp_expiry (aka now) + cur_interval. */ + nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns); + exp_expiry = + priv->concheck_x[IS_IPv4].p_cur_basetime_ns + (old_interval * NM_UTILS_NSEC_PER_SEC); + new_expiry = + exp_expiry + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC); + tdiff = NM_MAX(new_expiry - now_ns, 0); + priv->concheck_x[IS_IPv4].p_cur_basetime_ns = + (now_ns + tdiff) - (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC); + if (concheck_periodic_schedule_do(self, addr_family, now_ns)) { + handle = concheck_start(self, addr_family, NULL, NULL, TRUE); + if (old_interval != priv->concheck_x[IS_IPv4].p_cur_interval) { + /* we just bumped the interval already when scheduling this check. + * When the handle returns, don't bump a second time. + * + * But if we reach the timeout again before the handle returns (this + * code here) we will still bump the interval. */ + handle->is_periodic_bump_on_complete = FALSE; + } + } + return; + } + + /* we just got an event that we lost connectivity (that is, concheck returned). We reset + * the interval to min/max or increase the probe interval (bump). */ + case CONCHECK_SCHEDULE_RETURNED_MIN: + priv->concheck_x[IS_IPv4].p_cur_interval = + NM_MIN(priv->concheck_x[IS_IPv4].p_max_interval, CONCHECK_P_PROBE_INTERVAL); + break; + case CONCHECK_SCHEDULE_RETURNED_MAX: + priv->concheck_x[IS_IPv4].p_cur_interval = priv->concheck_x[IS_IPv4].p_max_interval; + break; + case CONCHECK_SCHEDULE_RETURNED_BUMP: + priv->concheck_x[IS_IPv4].p_cur_interval = + NM_MIN(priv->concheck_x[IS_IPv4].p_cur_interval * 2, + priv->concheck_x[IS_IPv4].p_max_interval); + break; + } + + /* we are here, because we returned from a connectivity check and adjust the current interval. + * + * But note that we calculate the new timeout based on the time when we scheduled the + * last check, instead of counting from now. The reason is that we want that the times + * when we schedule checks be at precise intervals, without including the time it took for + * the connectivity check. */ + new_expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns + + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC); + tdiff = NM_MAX(new_expiry - nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns), 0); + priv->concheck_x[IS_IPv4].p_cur_basetime_ns = + now_ns + tdiff - (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC); + concheck_periodic_schedule_do(self, addr_family, now_ns); +} + +static void +concheck_update_interval(NMDevice *self, int addr_family, gboolean check_now) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + guint new_interval; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + new_interval = nm_connectivity_get_interval(concheck_get_mgr(self)); + + new_interval = NM_MIN(new_interval, 7 * 24 * 3600); + + if (new_interval != priv->concheck_x[IS_IPv4].p_max_interval) { + _LOGT(LOGD_CONCHECK, + "connectivity: [IPv%c] periodic-check: set interval to %u seconds", + nm_utils_addr_family_to_char(addr_family), + new_interval); + priv->concheck_x[IS_IPv4].p_max_interval = new_interval; + } + + if (!new_interval) { + /* this will cancel any potentially pending timeout because max-interval is zero. + * But it logs a nice message... */ + concheck_periodic_schedule_do(self, addr_family, 0); + + /* also update the fake connectivity state. */ + concheck_update_state(self, addr_family, NM_CONNECTIVITY_FAKE, TRUE); + return; + } + + concheck_periodic_schedule_set(self, + addr_family, + check_now ? CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART + : CONCHECK_SCHEDULE_UPDATE_INTERVAL); +} + +void +nm_device_check_connectivity_update_interval(NMDevice *self) +{ + concheck_update_interval(self, AF_INET, TRUE); + concheck_update_interval(self, AF_INET6, TRUE); +} + +static void +concheck_update_state(NMDevice * self, + int addr_family, + NMConnectivityState state, + gboolean allow_periodic_bump) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + /* @state is a result of the connectivity check. We only expect a precise + * number of possible values. */ + nm_assert(NM_IN_SET(state, + NM_CONNECTIVITY_LIMITED, + NM_CONNECTIVITY_PORTAL, + NM_CONNECTIVITY_FULL, + NM_CONNECTIVITY_FAKE, + NM_CONNECTIVITY_NONE, + NM_CONNECTIVITY_ERROR)); + + if (state == NM_CONNECTIVITY_ERROR) { + /* on error, we don't change the current connectivity state, + * except making UNKNOWN to NONE. */ + state = priv->concheck_x[IS_IPv4].state; + if (state == NM_CONNECTIVITY_UNKNOWN) + state = NM_CONNECTIVITY_NONE; + } else if (state == NM_CONNECTIVITY_FAKE) { + /* If the connectivity check is disabled and we obtain a fake + * result, make an optimistic guess. */ + if (priv->state == NM_DEVICE_STATE_ACTIVATED) { + /* FIXME: the fake connectivity state depends on the availability of + * a default route. However, we have no mechanism that rechecks the + * value if a device route appears/disappears after the device + * was activated. */ + if (nm_device_get_best_default_route(self, AF_UNSPEC)) + state = NM_CONNECTIVITY_FULL; + else + state = NM_CONNECTIVITY_LIMITED; + } else + state = NM_CONNECTIVITY_NONE; + } + + if (priv->concheck_x[IS_IPv4].state == state) { + /* we got a connectivity update, but the state didn't change. If we were probing, + * we bump the probe frequency. */ + if (allow_periodic_bump) + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_BUMP); + return; + } + /* we need to update the probe interval before emitting signals. Emitting + * a signal might call back into NMDevice and change the probe settings. + * So, do that first. */ + if (state == NM_CONNECTIVITY_FULL) { + /* we reached full connectivity state. Stop probing by setting the + * interval to the max. */ + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_MAX); + } else if (priv->concheck_x[IS_IPv4].state == NM_CONNECTIVITY_FULL) { + /* we are about to loose connectivity. (re)start probing by setting + * the timeout interval to the min. */ + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_MIN); + } else { + if (allow_periodic_bump) + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_BUMP); + } + + _LOGD(LOGD_CONCHECK, + "connectivity state changed from %s to %s", + nm_connectivity_state_to_string(priv->concheck_x[IS_IPv4].state), + nm_connectivity_state_to_string(state)); + priv->concheck_x[IS_IPv4].state = state; + + _notify(self, IS_IPv4 ? PROP_IP4_CONNECTIVITY : PROP_IP6_CONNECTIVITY); + + if (priv->state == NM_DEVICE_STATE_ACTIVATED && !nm_device_sys_iface_state_is_external(self)) { + if (nm_device_get_best_default_route(self, AF_INET) + && !ip_config_merge_and_apply(self, AF_INET, TRUE)) + _LOGW(LOGD_IP4, "Failed to update IPv4 route metric"); + if (nm_device_get_best_default_route(self, AF_INET6) + && !ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "Failed to update IPv6 route metric"); + } +} + +static const char * +nm_device_get_effective_ip_config_method(NMDevice *self, int addr_family) +{ + NMDeviceClass *klass; + NMConnection * connection = nm_device_get_applied_connection(self); + const char * method; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_val_if_fail(NM_IS_CONNECTION(connection), "" /* bogus */); + + method = nm_utils_get_ip_config_method(connection, addr_family); + + if ((IS_IPv4 && nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) + || (!IS_IPv4 && nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO))) { + klass = NM_DEVICE_GET_CLASS(self); + if (klass->get_auto_ip_config_method) { + const char *auto_method; + + auto_method = klass->get_auto_ip_config_method(self, addr_family); + if (auto_method) + return auto_method; + } + } + + return method; +} + +static void +concheck_handle_complete(NMDeviceConnectivityHandle *handle, GError *error) +{ + const int IS_IPv4 = NM_IS_IPv4(handle->addr_family); + + /* The moment we invoke the callback, we unlink it. It signals + * that @handle is handled -- as far as the callee of callback + * is concerned. */ + c_list_unlink(&handle->concheck_lst); + + if (handle->c_handle) + nm_connectivity_check_cancel(handle->c_handle); + + if (handle->callback) { + handle->callback(handle->self, + handle, + NM_DEVICE_GET_PRIVATE(handle->self)->concheck_x[IS_IPv4].state, + error, + handle->user_data); + } + + g_slice_free(NMDeviceConnectivityHandle, handle); +} + +static void +concheck_cb(NMConnectivity * connectivity, + NMConnectivityCheckHandle *c_handle, + NMConnectivityState state, + gpointer user_data) +{ + _nm_unused gs_unref_object NMDevice *self_keep_alive = NULL; + NMDevice * self; + NMDevicePrivate * priv; + NMDeviceConnectivityHandle * handle; + NMDeviceConnectivityHandle * other_handle; + gboolean handle_is_alive; + gboolean allow_periodic_bump; + gboolean any_periodic_before; + gboolean any_periodic_after; + guint64 seq; + + handle = user_data; + nm_assert(handle->c_handle == c_handle); + nm_assert(NM_IS_DEVICE(handle->self)); + + handle->c_handle = NULL; + self = handle->self; + + if (state == NM_CONNECTIVITY_CANCELLED) { + /* the only place where we nm_connectivity_check_cancel(@c_handle), is + * from inside concheck_handle_complete(). This is a recursive call, + * nothing to do. */ + _LOGT(LOGD_CONCHECK, + "connectivity: [IPv%c] complete check (seq:%llu, cancelled)", + nm_utils_addr_family_to_char(handle->addr_family), + (long long unsigned) handle->seq); + return; + } + + /* we keep NMConnectivity instance alive. It cannot be disposing. */ + nm_assert(state != NM_CONNECTIVITY_DISPOSING); + + self_keep_alive = g_object_ref(self); + + /* keep @self alive, while we invoke callbacks. */ + priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(handle && c_list_contains(&priv->concheck_lst_head, &handle->concheck_lst)); + + seq = handle->seq; + + _LOGT(LOGD_CONCHECK, + "connectivity: [Ipv%c] complete check (seq:%llu, state:%s)", + nm_utils_addr_family_to_char(handle->addr_family), + (long long unsigned) handle->seq, + nm_connectivity_state_to_string(state)); + + /* find out, if there are any periodic checks pending (either whether they + * were scheduled before or after @handle. */ + any_periodic_before = FALSE; + any_periodic_after = FALSE; + c_list_for_each_entry (other_handle, &priv->concheck_lst_head, concheck_lst) { + if (other_handle->addr_family != handle->addr_family) + continue; + if (other_handle->is_periodic_bump_on_complete) { + if (other_handle->seq < seq) + any_periodic_before = TRUE; + else if (other_handle->seq > seq) + any_periodic_after = TRUE; + } + } + if (NM_IN_SET(state, NM_CONNECTIVITY_ERROR)) { + /* the request failed. We consider this periodic check only as completed if + * this was a periodic check, and there are not checks pending (either + * before or after this one). + * + * We allow_periodic_bump, if the request failed and there are + * still other requests periodic pending. */ + allow_periodic_bump = + handle->is_periodic_bump_on_complete && !any_periodic_before && !any_periodic_after; + } else { + /* the request succeeded. This marks the completion of a periodic check, + * if this handle was periodic, or any previously scheduled one (that + * we are going to complete below). */ + allow_periodic_bump = handle->is_periodic_bump_on_complete || any_periodic_before; + } + + /* first update the new state, and emit signals. */ + concheck_update_state(self, handle->addr_family, state, allow_periodic_bump); + + handle_is_alive = FALSE; + + /* we might have invoked callbacks during concheck_update_state(). The caller might have + * cancelled and thus destroyed @handle. We have to check whether handle is still alive, + * by searching it in the list of alive handles. + * + * Also, we might want to complete all pending callbacks that were started before + * @handle, as they are automatically obsoleted. */ +check_handles: + c_list_for_each_entry (other_handle, &priv->concheck_lst_head, concheck_lst) { + if (other_handle->addr_family != handle->addr_family) + continue; + if (other_handle->seq >= seq) { + /* it's not guaranteed that @handle is still in the list. It might already + * be canceled while invoking callbacks for a previous other_handle. + * If it is already cancelled, @handle is a dangling pointer. + * + * Since @seq is assigned uniquely and increasing, either @other_handle is + * @handle (and thus, handle is alive), or it isn't. */ + if (other_handle == handle) + handle_is_alive = TRUE; + break; + } + + nm_assert(other_handle != handle); + + if (!NM_IN_SET(state, NM_CONNECTIVITY_ERROR)) { + /* we also want to complete handles that were started before the current + * @handle. Their response is out-dated. */ + concheck_handle_complete(other_handle, NULL); + + /* we invoked callbacks, other handles might be cancelled and removed from the list. + * Need to iterate the list from the start. */ + goto check_handles; + } + } + + if (!handle_is_alive) { + /* We didn't find @handle in the list of alive handles. Thus, the handles + * was cancelled while we were invoking events. Nothing to do, and don't + * touch the dangling pointer. */ + return; + } + + concheck_handle_complete(handle, NULL); +} + +static NMDeviceConnectivityHandle * +concheck_start(NMDevice * self, + int addr_family, + NMDeviceConnectivityCallback callback, + gpointer user_data, + gboolean is_periodic) +{ + static guint64 seq_counter = 0; + NMDevicePrivate * priv; + NMDeviceConnectivityHandle *handle; + const char * ifname; + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + priv = NM_DEVICE_GET_PRIVATE(self); + + handle = g_slice_new0(NMDeviceConnectivityHandle); + handle->seq = ++seq_counter; + handle->self = self; + handle->callback = callback; + handle->user_data = user_data; + handle->is_periodic = is_periodic; + handle->is_periodic_bump = is_periodic; + handle->is_periodic_bump_on_complete = is_periodic; + handle->addr_family = addr_family; + + c_list_link_tail(&priv->concheck_lst_head, &handle->concheck_lst); + + _LOGT(LOGD_CONCHECK, + "connectivity: [IPv%c] start check (seq:%llu%s)", + nm_utils_addr_family_to_char(addr_family), + (long long unsigned) handle->seq, + is_periodic ? ", periodic-check" : ""); + + if (NM_IS_IPv4(addr_family) && !priv->concheck_rp_filter_checked) { + if ((ifname = nm_device_get_ip_iface_from_platform(self))) { + gboolean due_to_all; + int val; + + val = nm_platform_sysctl_ip_conf_get_rp_filter_ipv4(nm_device_get_platform(self), + ifname, + TRUE, + &due_to_all); + if (val == 1) { + _LOGW(LOGD_CONCHECK, + "connectivity: \"/proc/sys/net/ipv4/conf/%s/rp_filter\" is set to \"1\". " + "This might break connectivity checking for IPv4 on this device", + due_to_all ? "all" : ifname); + } + } + + /* we only check once per device. It's a warning after all. */ + priv->concheck_rp_filter_checked = TRUE; + } + + handle->c_handle = nm_connectivity_check_start(concheck_get_mgr(self), + handle->addr_family, + nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + nm_device_get_ip_iface(self), + concheck_cb, + handle); + return handle; +} + +NMDeviceConnectivityHandle * +nm_device_check_connectivity(NMDevice * self, + int addr_family, + NMDeviceConnectivityCallback callback, + gpointer user_data) +{ + if (!concheck_is_possible(self)) + return NULL; + + concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_CHECK_EXTERNAL); + return concheck_start(self, addr_family, callback, user_data, FALSE); +} + +void +nm_device_check_connectivity_cancel(NMDeviceConnectivityHandle *handle) +{ + gs_free_error GError *cancelled_error = NULL; + + g_return_if_fail(handle); + g_return_if_fail(NM_IS_DEVICE(handle->self)); + g_return_if_fail(!c_list_is_empty(&handle->concheck_lst)); + + /* nobody has access to periodic handles, and cannot cancel + * them externally. */ + nm_assert(!handle->is_periodic); + + nm_utils_error_set_cancelled(&cancelled_error, FALSE, "NMDevice"); + concheck_handle_complete(handle, cancelled_error); +} + +NMConnectivityState +nm_device_get_connectivity_state(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_CONNECTIVITY_UNKNOWN); + + priv = NM_DEVICE_GET_PRIVATE(self); + + switch (addr_family) { + case AF_INET: + case AF_INET6: + return priv->concheck_x[NM_IS_IPv4(addr_family)].state; + default: + nm_assert(addr_family == AF_UNSPEC); + return NM_MAX_WITH_CMP(nm_connectivity_state_cmp, + priv->concheck_x[0].state, + priv->concheck_x[1].state); + } +} + +/*****************************************************************************/ + +static SlaveInfo * +find_slave_info(NMDevice *self, NMDevice *slave) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + CList * iter; + SlaveInfo * info; + + c_list_for_each (iter, &priv->slaves) { + info = c_list_entry(iter, SlaveInfo, lst_slave); + if (info->slave == slave) + return info; + } + return NULL; +} + +/** + * nm_device_master_enslave_slave: + * @self: the master device + * @slave: the slave device to enslave + * @connection: (allow-none): the slave device's connection + * + * If @self is capable of enslaving other devices (ie it's a bridge, bond, team, + * etc) then this function enslaves @slave. + * + * Returns: %TRUE on success, %FALSE on failure or if this device cannot enslave + * other devices. + */ +static gboolean +nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *connection) +{ + NMDevicePrivate *priv; + SlaveInfo * info; + gboolean success = FALSE; + gboolean configure; + + g_return_val_if_fail(self != NULL, FALSE); + g_return_val_if_fail(slave != NULL, FALSE); + g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + info = find_slave_info(self, slave); + if (!info) + return FALSE; + + if (info->slave_is_enslaved) + success = TRUE; + else { + configure = (info->configure && connection != NULL); + if (configure) + g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE); + + success = NM_DEVICE_GET_CLASS(self)->enslave_slave(self, slave, connection, configure); + info->slave_is_enslaved = success; + } + + nm_device_slave_notify_enslave(info->slave, success); + + /* Ensure the device's hardware address is up-to-date; it often changes + * when slaves change. + */ + nm_device_update_hw_address(self); + + /* Send ARP announcements if did not yet and have addresses. */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !priv->acd.announcing) + nm_device_arp_announce(self); + + /* Restart IP configuration if we're waiting for slaves. Do this + * after updating the hardware address as IP config may need the + * new address. + */ + if (success) { + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_WAIT) + nm_device_activate_stage3_ip_start(self, AF_INET); + + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_WAIT) + nm_device_activate_stage3_ip_start(self, AF_INET6); + } + + /* Since slave devices don't have their own IP configuration, + * set the MTU here. + */ + _commit_mtu(slave, NM_DEVICE_GET_PRIVATE(slave)->ip_config_4); + + return success; +} + +/** + * nm_device_master_release_one_slave: + * @self: the master device + * @slave: the slave device to release + * @configure: whether @self needs to actually release @slave + * @force: force the release of @slave even if it wasn't added + * to @master by NetworkManager + * @reason: the state change reason for the @slave + * + * If @self is capable of enslaving other devices (ie it's a bridge, bond, team, + * etc) then this function releases the previously enslaved @slave and/or + * updates the state of @self and @slave to reflect its release. + */ +static void +nm_device_master_release_one_slave(NMDevice * self, + NMDevice * slave, + gboolean configure, + gboolean force, + NMDeviceStateReason reason) +{ + NMDevicePrivate *priv; + NMDevicePrivate *slave_priv; + SlaveInfo * info; + gs_unref_object NMDevice *self_free = NULL; + + g_return_if_fail(NM_DEVICE(self)); + g_return_if_fail(NM_DEVICE(slave)); + g_return_if_fail(!force || configure); + g_return_if_fail(NM_DEVICE_GET_CLASS(self)->release_slave != NULL); + + info = find_slave_info(self, slave); + + _LOGT(LOGD_CORE, + "master: release one slave %p/%s %s%s", + slave, + nm_device_get_iface(slave), + !info ? "(not registered)" : (info->slave_is_enslaved ? "(enslaved)" : "(not enslaved)"), + force ? " (force-configure)" : (configure ? " (configure)" : "")); + + if (!info) + g_return_if_reached(); + + priv = NM_DEVICE_GET_PRIVATE(self); + slave_priv = NM_DEVICE_GET_PRIVATE(slave); + + g_return_if_fail(self == slave_priv->master); + nm_assert(slave == info->slave); + + /* first, let subclasses handle the release ... */ + if (info->slave_is_enslaved || nm_device_sys_iface_state_is_external(slave) || force) + NM_DEVICE_GET_CLASS(self)->release_slave(self, slave, configure); + + /* raise notifications about the release, including clearing is_enslaved. */ + nm_device_slave_notify_release(slave, reason); + + /* keep both alive until the end of the function. + * Transfers ownership from slave_priv->master. */ + self_free = self; + + c_list_unlink(&info->lst_slave); + slave_priv->master = NULL; + + g_signal_handler_disconnect(slave, info->watch_id); + g_object_unref(slave); + g_slice_free(SlaveInfo, info); + + if (c_list_is_empty(&priv->slaves)) { + _active_connection_set_state_flags_full(self, + 0, + NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES); + } + + /* Ensure the device's hardware address is up-to-date; it often changes + * when slaves change. + */ + nm_device_update_hw_address(self); + nm_device_set_unmanaged_by_flags(slave, + NM_UNMANAGED_IS_SLAVE, + NM_UNMAN_FLAG_OP_FORGET, + NM_DEVICE_STATE_REASON_REMOVED); +} + +/** + * can_unmanaged_external_down: + * @self: the device + * + * Check whether the device should stay NM_UNMANAGED_EXTERNAL_DOWN unless + * IFF_UP-ed externally. + */ +static gboolean +can_unmanaged_external_down(NMDevice *self) +{ + return !NM_DEVICE_GET_PRIVATE(self)->nm_owned && nm_device_is_software(self); +} + +static NMUnmanFlagOp +is_unmanaged_external_down(NMDevice *self, gboolean consider_can) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (consider_can && !NM_DEVICE_GET_CLASS(self)->can_unmanaged_external_down(self)) + return NM_UNMAN_FLAG_OP_FORGET; + + /* Manage externally-created software interfaces only when they are IFF_UP */ + if (priv->ifindex <= 0 || !priv->up + || !(!c_list_is_empty(&priv->slaves) + || nm_platform_link_can_assume(nm_device_get_platform(self), priv->ifindex))) + return NM_UNMAN_FLAG_OP_SET_UNMANAGED; + + return NM_UNMAN_FLAG_OP_SET_MANAGED; +} + +static void +set_unmanaged_external_down(NMDevice *self, gboolean only_if_unmanaged) +{ + NMUnmanFlagOp ext_flags; + + if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN)) + return; + + if (only_if_unmanaged) { + if (!nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN)) + return; + } + + ext_flags = is_unmanaged_external_down(self, FALSE); + if (ext_flags != NM_UNMAN_FLAG_OP_SET_UNMANAGED) { + /* Ensure the assume check is queued before any queued state changes + * from the transition to UNAVAILABLE. + */ + nm_device_queue_recheck_assume(self); + } + + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_EXTERNAL_DOWN, + ext_flags, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); +} + +void +nm_device_update_dynamic_ip_setup(NMDevice *self) +{ + NMDevicePrivate *priv; + GError * error = NULL; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->state < NM_DEVICE_STATE_IP_CONFIG || priv->state > NM_DEVICE_STATE_ACTIVATED) + return; + + g_hash_table_remove_all(priv->ip6_saved_properties); + + if (priv->dhcp_data_4.client) { + if (!nm_device_dhcp4_renew(self, FALSE)) { + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DHCP_FAILED); + return; + } + } + if (priv->dhcp_data_6.client) { + if (!nm_device_dhcp6_renew(self, FALSE)) { + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DHCP_FAILED); + return; + } + } + if (priv->ndisc) { + /* FIXME: todo */ + } + if (priv->dnsmasq_manager) { + /* FIXME: todo */ + } + + if (priv->lldp_listener && nm_lldp_listener_is_running(priv->lldp_listener)) { + nm_lldp_listener_stop(priv->lldp_listener); + if (!nm_lldp_listener_start(priv->lldp_listener, nm_device_get_ifindex(self), &error)) { + _LOGD(LOGD_DEVICE, + "LLDP listener %p could not be restarted: %s", + priv->lldp_listener, + error->message); + g_clear_error(&error); + } + } +} + +/*****************************************************************************/ + +static void +carrier_changed_notify(NMDevice *self, gboolean carrier) +{ + /* stub */ +} + +static void +carrier_changed(NMDevice *self, gboolean carrier) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->state <= NM_DEVICE_STATE_UNMANAGED) + return; + + nm_device_recheck_available_connections(self); + + /* ignore-carrier devices ignore all carrier-down events */ + if (priv->ignore_carrier && !carrier) + return; + + if (nm_device_is_master(self)) { + if (carrier) { + /* Force master to retry getting ip addresses when carrier + * is restored. */ + if (priv->state == NM_DEVICE_STATE_ACTIVATED) + nm_device_update_dynamic_ip_setup(self); + /* If needed, also resume IP configuration that is + * waiting for carrier. */ + if (nm_device_activate_ip4_state_in_wait(self)) + nm_device_activate_stage3_ip_start(self, AF_INET); + if (nm_device_activate_ip6_state_in_wait(self)) + nm_device_activate_stage3_ip_start(self, AF_INET6); + return; + } + /* fall-through and change state of device */ + } else if (priv->is_enslaved && !carrier) { + /* Slaves don't deactivate when they lose carrier; for + * bonds/teams in particular that would be actively + * counterproductive. + */ + return; + } + + if (carrier) { + if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) { + nm_device_queue_state(self, + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_CARRIER); + } else if (priv->state == NM_DEVICE_STATE_DISCONNECTED) { + /* If the device is already in DISCONNECTED state without a carrier + * (probably because it is tagged for carrier ignore) ensure that + * when the carrier appears, auto connections are rechecked for + * the device. + */ + nm_device_emit_recheck_auto_activate(self); + } else if (priv->state == NM_DEVICE_STATE_ACTIVATED) { + /* If the device is active without a carrier (probably because it is + * tagged for carrier ignore) ensure that when the carrier appears we + * renew DHCP leases and such. + */ + nm_device_update_dynamic_ip_setup(self); + } + } else { + if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) { + if (priv->queued_state.id && priv->queued_state.state >= NM_DEVICE_STATE_DISCONNECTED) + queued_state_clear(self); + } else { + nm_device_queue_state(self, + NM_DEVICE_STATE_UNAVAILABLE, + NM_DEVICE_STATE_REASON_CARRIER); + } + } +} + +static gboolean +carrier_disconnected_action_cb(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + _LOGD(LOGD_DEVICE, + "carrier: link disconnected (calling deferred action) (id=%u)", + priv->carrier_defer_id); + + priv->carrier_defer_id = 0; + carrier_changed(self, FALSE); + return FALSE; +} + +static void +carrier_disconnected_action_cancel(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + guint id = priv->carrier_defer_id; + + if (nm_clear_g_source(&priv->carrier_defer_id)) { + _LOGD(LOGD_DEVICE, "carrier: link disconnected (canceling deferred action) (id=%u)", id); + } +} + +void +nm_device_set_carrier(NMDevice *self, gboolean carrier) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceState state = nm_device_get_state(self); + gboolean notify_flags = FALSE; + + if (priv->carrier == carrier) + return; + + if (NM_FLAGS_ALL(priv->capabilities, + NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER)) { + if (carrier) + priv->interface_flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER; + else + priv->interface_flags &= ~NM_DEVICE_INTERFACE_FLAG_CARRIER; + notify_flags = TRUE; + } + + priv->carrier = carrier; + if (notify_flags) + nm_gobject_notify_together(self, PROP_CARRIER, PROP_INTERFACE_FLAGS); + else + _notify(self, PROP_CARRIER); + + if (priv->carrier) { + _LOGI(LOGD_DEVICE, "carrier: link connected"); + carrier_disconnected_action_cancel(self); + NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier); + carrier_changed(self, TRUE); + + if (priv->carrier_wait_id) { + nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE); + _carrier_wait_check_queued_act_request(self); + } + } else { + if (priv->carrier_wait_id) + nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE); + NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier); + if (state <= NM_DEVICE_STATE_DISCONNECTED && !priv->queued_act_request) { + _LOGD(LOGD_DEVICE, "carrier: link disconnected"); + carrier_changed(self, FALSE); + } else { + gint64 now_ms, until_ms; + + now_ms = nm_utils_get_monotonic_timestamp_msec(); + until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms); + priv->carrier_defer_id = + g_timeout_add(until_ms - now_ms, carrier_disconnected_action_cb, self); + _LOGD(LOGD_DEVICE, + "carrier: link disconnected (deferring action for %ld milliseconds) (id=%u)", + (long) (until_ms - now_ms), + priv->carrier_defer_id); + } + } +} + +static void +nm_device_set_carrier_from_platform(NMDevice *self) +{ + int ifindex; + + if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)) { + if (!nm_device_has_capability(self, NM_DEVICE_CAP_NONSTANDARD_CARRIER) + && (ifindex = nm_device_get_ip_ifindex(self)) > 0) { + nm_device_set_carrier( + self, + nm_platform_link_is_connected(nm_device_get_platform(self), ifindex)); + } + } else { + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + /* Fake online link when carrier detection is not available. */ + if (!priv->carrier) { + priv->carrier = TRUE; + _notify(self, PROP_CARRIER); + } + } +} + +/*****************************************************************************/ + +static void +device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDevice * master; + nm_auto_nmpobj const NMPObject *plink_master_keep_alive = NULL; + const NMPlatformLink * plink_master; + + g_return_if_fail(plink); + + if (plink->master <= 0) + goto out; + + master = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, plink->master); + plink_master = nm_platform_link_get(nm_device_get_platform(self), plink->master); + plink_master_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink_master)); + + if (master == NULL && plink_master && nm_streq0(plink_master->name, "ovs-system") + && plink_master->type == NM_LINK_TYPE_OPENVSWITCH) { + _LOGD(LOGD_DEVICE, "the device claimed by openvswitch"); + goto out; + } + + priv->master_ifindex = plink->master; + + if (priv->master) { + if (plink->master > 0 && plink->master == nm_device_get_ifindex(priv->master)) { + /* call add-slave again. We expect @self already to be added to + * the master, but this also triggers a recheck-assume. */ + nm_device_master_add_slave(priv->master, self, FALSE); + goto out; + } + + nm_device_master_release_one_slave(priv->master, + self, + FALSE, + FALSE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + } + + if (master && NM_DEVICE_GET_CLASS(master)->enslave_slave) { + nm_device_master_add_slave(master, self, FALSE); + goto out; + } + + if (master) { + _LOGD(LOGD_DEVICE, + "enslaved to non-master-type device %s; ignoring", + nm_device_get_iface(master)); + } else { + _LOGD(LOGD_DEVICE, + "enslaved to unknown device %d (%s%s%s)", + plink->master, + NM_PRINT_FMT_QUOTED(plink_master, "\"", plink_master->name, "\"", "??")); + } + if (!priv->ifindex_changed_id) { + priv->ifindex_changed_id = g_signal_connect(nm_device_get_manager(self), + NM_MANAGER_DEVICE_IFINDEX_CHANGED, + G_CALLBACK(device_ifindex_changed_cb), + self); + } + return; + +out: + nm_clear_g_signal_handler(nm_device_get_manager(self), &priv->ifindex_changed_id); +} + +static void +device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->master_ifindex != nm_device_get_ifindex(device_changed)) + return; + + _LOGD(LOGD_DEVICE, + "master %s with ifindex %d appeared", + nm_device_get_iface(device_changed), + nm_device_get_ifindex(device_changed)); + if (!priv->device_link_changed_id) + priv->device_link_changed_id = g_idle_add((GSourceFunc) device_link_changed, self); +} + +static void +ndisc_set_router_config(NMNDisc *ndisc, NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_unref_array GArray *addresses = NULL; + gs_unref_array GArray *dns_servers = NULL; + gs_unref_array GArray * dns_domains = NULL; + guint len; + guint i; + const NMDedupMultiHeadEntry *head_entry; + NMDedupMultiIter ipconf_iter; + + if (nm_ndisc_get_node_type(ndisc) != NM_NDISC_NODE_TYPE_ROUTER) + return; + + head_entry = nm_ip6_config_lookup_addresses(priv->ip_config_6); + addresses = + g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscAddress), head_entry ? head_entry->len : 0); + nm_dedup_multi_iter_for_each (&ipconf_iter, head_entry) { + const NMPlatformIP6Address *addr = NMP_OBJECT_CAST_IP6_ADDRESS(ipconf_iter.current->obj); + NMNDiscAddress * ndisc_addr; + guint32 lifetime; + guint32 preferred; + + if (IN6_IS_ADDR_UNSPECIFIED(&addr->address) || IN6_IS_ADDR_LINKLOCAL(&addr->address)) + continue; + + if (addr->n_ifa_flags & IFA_F_TENTATIVE || addr->n_ifa_flags & IFA_F_DADFAILED) + continue; + + if (addr->plen != 64) + continue; + + lifetime = nm_utils_lifetime_get(addr->timestamp, + addr->lifetime, + addr->preferred, + NM_NDISC_EXPIRY_BASE_TIMESTAMP / 1000, + &preferred); + if (!lifetime) + continue; + + g_array_set_size(addresses, addresses->len + 1); + ndisc_addr = &g_array_index(addresses, NMNDiscAddress, addresses->len - 1); + ndisc_addr->address = addr->address; + ndisc_addr->expiry_msec = + _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, lifetime); + ndisc_addr->expiry_preferred_msec = + _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, preferred); + } + + len = nm_ip6_config_get_num_nameservers(priv->ip_config_6); + dns_servers = g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscDNSServer), len); + g_array_set_size(dns_servers, len); + for (i = 0; i < len; i++) { + const struct in6_addr *nameserver = nm_ip6_config_get_nameserver(priv->ip_config_6, i); + NMNDiscDNSServer * ndisc_nameserver; + + ndisc_nameserver = &g_array_index(dns_servers, NMNDiscDNSServer, i); + ndisc_nameserver->address = *nameserver; + ndisc_nameserver->expiry_msec = + _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, NM_NDISC_ROUTER_LIFETIME); + } + + len = nm_ip6_config_get_num_searches(priv->ip_config_6); + dns_domains = g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscDNSDomain), len); + g_array_set_size(dns_domains, len); + for (i = 0; i < len; i++) { + const char * search = nm_ip6_config_get_search(priv->ip_config_6, i); + NMNDiscDNSDomain *ndisc_search; + + ndisc_search = &g_array_index(dns_domains, NMNDiscDNSDomain, i); + ndisc_search->domain = (char *) search; + ndisc_search->expiry_msec = + _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, NM_NDISC_ROUTER_LIFETIME); + } + + nm_ndisc_set_config(ndisc, addresses, dns_servers, dns_domains); +} + +static void +device_update_interface_flags(NMDevice *self, const NMPlatformLink *plink) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceInterfaceFlags flags = NM_DEVICE_INTERFACE_FLAG_NONE; + + if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP)) + flags |= NM_DEVICE_INTERFACE_FLAG_UP; + if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_LOWER_UP)) + flags |= NM_DEVICE_INTERFACE_FLAG_LOWER_UP; + + if (NM_FLAGS_ALL(priv->capabilities, + NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER)) { + if (priv->carrier) + flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER; + } else { + if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_LOWER_UP)) + flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER; + } + + if (flags != priv->interface_flags) { + priv->interface_flags = flags; + _notify(self, PROP_INTERFACE_FLAGS); + } +} + +static gboolean +device_link_changed(NMDevice *self) +{ + NMDeviceClass * klass = NM_DEVICE_GET_CLASS(self); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + gboolean ip_ifname_changed = FALSE; + nm_auto_nmpobj const NMPObject *pllink_keep_alive = NULL; + const NMPlatformLink * pllink; + const char * str; + int ifindex; + gboolean was_up; + gboolean update_unmanaged_specs = FALSE; + gboolean got_hw_addr = FALSE, had_hw_addr; + gboolean seen_down = priv->device_link_changed_down; + + priv->device_link_changed_id = 0; + priv->device_link_changed_down = FALSE; + + ifindex = nm_device_get_ifindex(self); + if (ifindex <= 0) + return G_SOURCE_REMOVE; + pllink = nm_platform_link_get(nm_device_get_platform(self), ifindex); + if (!pllink) + return G_SOURCE_REMOVE; + + pllink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(pllink)); + + str = nm_platform_link_get_udi(nm_device_get_platform(self), pllink->ifindex); + if (!nm_streq0(str, priv->udi)) { + g_free(priv->udi); + priv->udi = g_strdup(str); + _notify(self, PROP_UDI); + } + + str = nm_platform_link_get_path(nm_device_get_platform(self), pllink->ifindex); + if (!nm_streq0(str, priv->path)) { + g_free(priv->path); + priv->path = g_strdup(str); + _notify(self, PROP_PATH); + } + + if (!nm_streq0(pllink->driver, priv->driver)) { + g_free(priv->driver); + priv->driver = g_strdup(pllink->driver); + _notify(self, PROP_DRIVER); + } + + _set_mtu(self, pllink->mtu); + + if (ifindex == nm_device_get_ip_ifindex(self)) + _stats_update_counters_from_pllink(self, pllink); + + had_hw_addr = (priv->hw_addr != NULL); + nm_device_update_hw_address(self); + got_hw_addr = (!had_hw_addr && priv->hw_addr); + nm_device_update_permanent_hw_address(self, FALSE); + + if (pllink->name[0] && !nm_streq(priv->iface, pllink->name)) { + _LOGI(LOGD_DEVICE, + "interface index %d renamed iface from '%s' to '%s'", + priv->ifindex, + priv->iface, + pllink->name); + g_free(priv->iface_); + priv->iface_ = g_strdup(pllink->name); + + /* If the device has no explicit ip_iface, then changing iface changes ip_iface too. */ + ip_ifname_changed = !priv->ip_iface; + + if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) + nm_device_set_unmanaged_by_user_settings(self); + else + update_unmanaged_specs = TRUE; + + _notify(self, PROP_IFACE); + if (ip_ifname_changed) + _notify(self, PROP_IP_IFACE); + + /* Re-match available connections against the new interface name */ + nm_device_recheck_available_connections(self); + + /* Let any connections that use the new interface name have a chance + * to auto-activate on the device. + */ + nm_device_emit_recheck_auto_activate(self); + } + + if (priv->ndisc && pllink->inet6_token.id) { + if (nm_ndisc_set_iid(priv->ndisc, pllink->inet6_token)) + _LOGD(LOGD_DEVICE, "IPv6 tokenized identifier present on device %s", priv->iface); + } + + /* Update carrier from link event if applicable. */ + if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT) + && !nm_device_has_capability(self, NM_DEVICE_CAP_NONSTANDARD_CARRIER)) + nm_device_set_carrier(self, pllink->connected); + + device_update_interface_flags(self, pllink); + + klass->link_changed(self, pllink); + + /* Update DHCP, etc, if needed */ + if (ip_ifname_changed) + nm_device_update_dynamic_ip_setup(self); + + was_up = priv->up; + priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP); + + if (pllink->initialized && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) { + NMDeviceStateReason reason; + + nm_device_set_unmanaged_by_user_udev(self); + nm_device_set_unmanaged_by_user_conf(self); + + reason = NM_DEVICE_STATE_REASON_NOW_MANAGED; + + /* If the device is a external-down candidated but no longer has external + * down set, we must clear the platform-unmanaged flag with reason + * "assumed". */ + if (nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN) + && !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN)) { + /* actually, user-udev overwrites external-down. So we only assume the device, + * when it is a external-down candidate, which is not managed via udev. */ + if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_UDEV)) { + /* Ensure the assume check is queued before any queued state changes + * from the transition to UNAVAILABLE. + */ + nm_device_queue_recheck_assume(self); + reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED; + } + } + + nm_device_set_unmanaged_by_flags(self, NM_UNMANAGED_PLATFORM_INIT, FALSE, reason); + } + + set_unmanaged_external_down(self, FALSE); + + device_recheck_slave_status(self, pllink); + + if (priv->up && (!was_up || seen_down)) { + /* the link was down and just came up. That happens for example, while changing MTU. + * We must restore IP configuration. */ + if (NM_IN_SET(priv->ip_state_4, NM_DEVICE_IP_STATE_CONF, NM_DEVICE_IP_STATE_DONE)) { + if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) + _LOGW(LOGD_IP4, "failed applying IP4 config after link comes up again"); + } + + priv->linklocal6_dad_counter = 0; + if (NM_IN_SET(priv->ip_state_6, NM_DEVICE_IP_STATE_CONF, NM_DEVICE_IP_STATE_DONE)) { + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "failed applying IP6 config after link comes up again"); + } + } + + if (update_unmanaged_specs) + nm_device_set_unmanaged_by_user_settings(self); + + if (got_hw_addr && !priv->up && nm_device_get_state(self) == NM_DEVICE_STATE_UNAVAILABLE) { + /* + * If the device is UNAVAILABLE, any previous try to + * bring it up probably has failed because of the + * invalid hardware address; try again. + */ + nm_device_bring_up(self, TRUE, NULL); + nm_device_queue_recheck_available(self, + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_NONE); + } + + return G_SOURCE_REMOVE; +} + +static gboolean +device_ip_link_changed(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const NMPlatformLink *pllink; + const char * ip_iface; + + priv->device_ip_link_changed_id = 0; + + if (priv->ip_ifindex <= 0) + return G_SOURCE_REMOVE; + + nm_assert(priv->ip_iface); + + pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ip_ifindex); + if (!pllink) + return G_SOURCE_REMOVE; + + if (priv->ifindex <= 0 && pllink->mtu) + _set_mtu(self, pllink->mtu); + + _stats_update_counters_from_pllink(self, pllink); + + ip_iface = pllink->name; + + if (!ip_iface[0]) + return FALSE; + + if (!nm_streq(priv->ip_iface, ip_iface)) { + _LOGI(LOGD_DEVICE, + "ip-ifname: interface index %d renamed ip_iface (%d) from '%s' to '%s'", + priv->ifindex, + priv->ip_ifindex, + priv->ip_iface, + ip_iface); + g_free(priv->ip_iface_); + priv->ip_iface_ = g_strdup(ip_iface); + _notify(self, PROP_IP_IFACE); + + nm_device_update_dynamic_ip_setup(self); + } + + return G_SOURCE_REMOVE; +} + +static void +link_changed_cb(NMPlatform * platform, + int obj_type_i, + int ifindex, + NMPlatformLink *info, + int change_type_i, + NMDevice * self) +{ + const NMPlatformSignalChangeType change_type = change_type_i; + NMDevicePrivate * priv; + + if (change_type != NM_PLATFORM_SIGNAL_CHANGED) + return; + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (ifindex == nm_device_get_ifindex(self)) { + if (!(info->n_ifi_flags & IFF_UP)) + priv->device_link_changed_down = TRUE; + if (!priv->device_link_changed_id) { + priv->device_link_changed_id = g_idle_add((GSourceFunc) device_link_changed, self); + _LOGD(LOGD_DEVICE, "queued link change for ifindex %d", ifindex); + } + } else if (ifindex == nm_device_get_ip_ifindex(self)) { + if (!priv->device_ip_link_changed_id) { + priv->device_ip_link_changed_id = + g_idle_add((GSourceFunc) device_ip_link_changed, self); + _LOGD(LOGD_DEVICE, "queued link change for ip-ifindex %d", ifindex); + } + } +} + +/*****************************************************************************/ + +static void +link_changed(NMDevice *self, const NMPlatformLink *pllink) +{ + /* stub implementation of virtual function to allow subclasses to chain up. */ +} + +static gboolean +link_type_compatible(NMDevice *self, NMLinkType link_type, gboolean *out_compatible, GError **error) +{ + NMDeviceClass *klass; + NMLinkType device_type; + guint i = 0; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + klass = NM_DEVICE_GET_CLASS(self); + + if (!klass->link_types) { + NM_SET_OUT(out_compatible, FALSE); + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Device does not support platform links"); + return FALSE; + } + + device_type = self->_priv->link_type; + if (device_type > NM_LINK_TYPE_UNKNOWN && device_type != link_type) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Needed link type 0x%x does not match the platform link type 0x%X", + device_type, + link_type); + return FALSE; + } + + for (i = 0; klass->link_types[i] > NM_LINK_TYPE_UNKNOWN; i++) { + if (klass->link_types[i] == link_type) + return TRUE; + if (klass->link_types[i] == NM_LINK_TYPE_ANY) + return TRUE; + } + + NM_SET_OUT(out_compatible, FALSE); + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Device does not support platform link type 0x%X", + link_type); + return FALSE; +} + +/** + * nm_device_realize_start(): + * @self: the #NMDevice + * @plink: an existing platform link or %NULL + * @assume_state_guess_assume: set the guess_assume state. + * @assume_state_connection_uuid: set the connection uuid to assume. + * @set_nm_owned: for software device, if TRUE set nm-owned. + * @unmanaged_user_explicit: the user-explicit unmanaged flag to apply + * on the device initially. + * @out_compatible: %TRUE on return if @self is compatible with @plink + * @error: location to store error, or %NULL + * + * Initializes and sets up the device using existing backing resources. Before + * the device is ready for use nm_device_realize_finish() must be called. + * @out_compatible will only be set if @plink is not %NULL, and + * + * Important: if nm_device_realize_start() returns %TRUE, the caller MUST + * also call nm_device_realize_finish() to balance g_object_freeze_notify(). + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +nm_device_realize_start(NMDevice * self, + const NMPlatformLink *plink, + gboolean assume_state_guess_assume, + const char * assume_state_connection_uuid, + gboolean set_nm_owned, + NMUnmanFlagOp unmanaged_user_explicit, + gboolean * out_compatible, + GError ** error) +{ + nm_auto_nmpobj const NMPObject *plink_keep_alive = NULL; + + nm_assert(!plink || NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK); + + NM_SET_OUT(out_compatible, TRUE); + + if (plink) { + if (!nm_streq0(nm_device_get_iface(self), plink->name)) { + NM_SET_OUT(out_compatible, FALSE); + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Device interface name does not match platform link"); + return FALSE; + } + + if (!link_type_compatible(self, plink->type, out_compatible, error)) + return FALSE; + + plink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink)); + } + + realize_start_setup(self, + plink, + assume_state_guess_assume, + assume_state_connection_uuid, + set_nm_owned, + unmanaged_user_explicit, + FALSE); + return TRUE; +} + +/** + * nm_device_create_and_realize(): + * @self: the #NMDevice + * @connection: the #NMConnection being activated + * @parent: the parent #NMDevice if any + * @error: location to store error, or %NULL + * + * Creates any backing resources needed to realize the device to proceed + * with activating @connection. + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +nm_device_create_and_realize(NMDevice * self, + NMConnection *connection, + NMDevice * parent, + GError ** error) +{ + nm_auto_nmpobj const NMPObject *plink_keep_alive = NULL; + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const NMPlatformLink * plink; + gboolean nm_owned; + + /* Must be set before device is realized */ + plink = nm_platform_link_get_by_ifname(nm_device_get_platform(self), priv->iface); + nm_owned = !plink || !link_type_compatible(self, plink->type, NULL, NULL); + _LOGD(LOGD_DEVICE, "create (is %snm-owned)", nm_owned ? "" : "not "); + + plink = NULL; + /* Create any resources the device needs */ + if (NM_DEVICE_GET_CLASS(self)->create_and_realize) { + if (!NM_DEVICE_GET_CLASS(self)->create_and_realize(self, connection, parent, &plink, error)) + return FALSE; + if (plink) { + nm_assert(NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK); + plink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink)); + } + } + + priv->nm_owned = nm_owned; + + realize_start_setup(self, + plink, + FALSE, /* assume_state_guess_assume */ + NULL, /* assume_state_connection_uuid */ + FALSE, + NM_UNMAN_FLAG_OP_FORGET, + TRUE); + nm_device_realize_finish(self, plink); + + if (nm_device_get_managed(self, FALSE)) { + nm_device_state_changed(self, + NM_DEVICE_STATE_UNAVAILABLE, + NM_DEVICE_STATE_REASON_NOW_MANAGED); + } + return TRUE; +} + +static gboolean +can_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink) +{ + return TRUE; +} + +void +nm_device_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const char * str; + gboolean ifindex_changed; + guint32 mtu; + + if (!NM_DEVICE_GET_CLASS(self)->can_update_from_platform_link(self, plink)) + return; + + g_return_if_fail(plink == NULL || link_type_compatible(self, plink->type, NULL, NULL)); + + str = plink ? nm_platform_link_get_udi(nm_device_get_platform(self), plink->ifindex) : NULL; + if (!nm_streq0(str, priv->udi)) { + g_free(priv->udi); + priv->udi = g_strdup(str); + _notify(self, PROP_UDI); + } + + str = plink ? nm_platform_link_get_path(nm_device_get_platform(self), plink->ifindex) : NULL; + if (!nm_streq0(str, priv->path)) { + g_free(priv->path); + priv->path = g_strdup(str); + _notify(self, PROP_PATH); + } + + if (plink && !nm_str_is_empty(plink->name) && nm_utils_strdup_reset(&priv->iface_, plink->name)) + _notify(self, PROP_IFACE); + + str = plink ? plink->driver : NULL; + if (!nm_streq0(str, priv->driver)) { + g_free(priv->driver); + priv->driver = g_strdup(str); + _notify(self, PROP_DRIVER); + } + + if (plink) { + priv->up = NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP); + if (plink->ifindex == nm_device_get_ip_ifindex(self)) + _stats_update_counters_from_pllink(self, plink); + } else { + priv->up = FALSE; + } + + mtu = plink ? plink->mtu : 0; + _set_mtu(self, mtu); + + ifindex_changed = _set_ifindex(self, plink ? plink->ifindex : 0, FALSE); + + if (ifindex_changed) + NM_DEVICE_GET_CLASS(self)->link_changed(self, plink); + + device_update_interface_flags(self, plink); +} + +/*****************************************************************************/ + +static void +sriov_op_start(NMDevice *self, SriovOp *op) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(!priv->sriov.pending); + + op->cancellable = g_cancellable_new(); + op->device = g_object_ref(self); + priv->sriov.pending = op; + + nm_platform_link_set_sriov_params_async(nm_device_get_platform(self), + priv->ifindex, + op->num_vfs, + op->autoprobe, + sriov_op_cb, + op, + op->cancellable); +} + +static void +sriov_op_cb(GError *error, gpointer user_data) +{ + SriovOp * op = user_data; + gs_unref_object NMDevice *self = op->device; + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(op == priv->sriov.pending); + + g_clear_object(&op->cancellable); + + if (op->callback) + op->callback(error, op->callback_data); + + priv->sriov.pending = NULL; + nm_g_slice_free(op); + + if (priv->sriov.next) { + sriov_op_start(self, g_steal_pointer(&priv->sriov.next)); + } +} + +static void +sriov_op_queue_op(NMDevice *self, SriovOp *op) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->sriov.next) { + SriovOp *op_next = g_steal_pointer(&priv->sriov.next); + + priv->sriov.next = op; + + /* Cancel the next operation immediately */ + if (op_next->callback) { + gs_free_error GError *error = NULL; + + nm_utils_error_set_cancelled(&error, FALSE, NULL); + op_next->callback(error, op_next->callback_data); + } + + nm_g_slice_free(op_next); + return; + } + + if (priv->sriov.pending) { + priv->sriov.next = op; + g_cancellable_cancel(priv->sriov.pending->cancellable); + return; + } + + if (op) + sriov_op_start(self, op); +} + +static void +sriov_op_queue(NMDevice * self, + guint num_vfs, + NMOptionBool autoprobe, + NMPlatformAsyncCallback callback, + gpointer callback_data) +{ + SriovOp *op; + + /* We usually never want to cancel an async write operation, unless it's superseded + * by a newer operation (that resets the state). That is, because we need to ensure + * that we never end up doing two concurrent writes (since we write on a background + * thread, that would be unordered/racy). + * Of course, since we queue requests only per-device, when devices get renamed we + * might end up writing the same sysctl concurrently still. But that's really + * unlikely, and don't rename after udev completes! + * + * The "next" operation is not yet even started. It can be replaced/canceled right away + * when a newer request comes. + * The "pending" operation is currently ongoing, and we may cancel it if + * we have a follow-up operation (queued in "next"). Unless we have a such + * a newer request, we cannot cancel it! + * + * FIXME(shutdown): However, during shutdown we don't have a follow-up write request to cancel + * this operation and we have to give it at least some time to complete. The solution is that + * we register a way to abort the last call during shutdown, and after NM_SHUTDOWN_TIMEOUT_MS + * grace period we pull the plug and cancel it. */ + + op = g_slice_new(SriovOp); + *op = (SriovOp){ + .num_vfs = num_vfs, + .autoprobe = autoprobe, + .callback = callback, + .callback_data = callback_data, + }; + sriov_op_queue_op(self, op); +} + +static void +device_init_static_sriov_num_vfs(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free char * value = NULL; + int num_vfs; + + if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)) { + value = nm_config_data_get_device_config(NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS, + self, + NULL); + num_vfs = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXINT32, -1); + if (num_vfs >= 0) + sriov_op_queue(self, num_vfs, NM_OPTION_BOOL_DEFAULT, NULL, NULL); + } +} + +static void +config_changed(NMConfig * config, + NMConfigData * config_data, + NMConfigChangeFlags changes, + NMConfigData * old_data, + NMDevice * self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->state <= NM_DEVICE_STATE_DISCONNECTED || priv->state > NM_DEVICE_STATE_ACTIVATED) { + priv->ignore_carrier = nm_config_data_get_ignore_carrier(config_data, self); + if (NM_FLAGS_HAS(changes, NM_CONFIG_CHANGE_VALUES)) + device_init_static_sriov_num_vfs(self); + } +} + +static void +realize_start_notify(NMDevice *self, const NMPlatformLink *pllink) +{ + /* the default implementation of realize_start_notify() just calls + * link_changed() -- which by default does nothing. */ + NM_DEVICE_GET_CLASS(self)->link_changed(self, pllink); +} + +/** + * realize_start_setup(): + * @self: the #NMDevice + * @plink: the #NMPlatformLink if backed by a kernel netdevice + * @assume_state_guess_assume: set the guess_assume state. + * @assume_state_connection_uuid: set the connection uuid to assume. + * @set_nm_owned: if TRUE and device is a software-device, set nm-owned. + * TRUE. + * @unmanaged_user_explicit: the user-explict unmanaged flag to set. + * @force_platform_init: if TRUE the platform-init unmanaged flag is + * forcefully cleared. + * + * Update the device from backing resource properties (like hardware + * addresses, carrier states, driver/firmware info, etc). This function + * should only change properties for this device, and should not perform + * any tasks that affect other interfaces (like master/slave or parent/child + * stuff). + */ +static void +realize_start_setup(NMDevice * self, + const NMPlatformLink *plink, + gboolean assume_state_guess_assume, + const char * assume_state_connection_uuid, + gboolean set_nm_owned, + NMUnmanFlagOp unmanaged_user_explicit, + gboolean force_platform_init) +{ + NMDevicePrivate * priv; + NMDeviceClass * klass; + NMPlatform * platform; + NMDeviceCapabilities capabilities = 0; + NMConfig * config; + guint real_rate; + gboolean unmanaged; + + /* plink is a NMPlatformLink type, however, we require it to come from the platform + * cache (where else would it come from?). */ + nm_assert(!plink || NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK); + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + /* The device should not be realized */ + g_return_if_fail(!priv->real); + g_return_if_fail(nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)); + g_return_if_fail(priv->ip_ifindex <= 0); + g_return_if_fail(priv->ip_iface == NULL); + g_return_if_fail(!priv->queued_ip_config_id_4); + g_return_if_fail(!priv->queued_ip_config_id_6); + + _LOGD(LOGD_DEVICE, + "start setup of %s, kernel ifindex %d", + G_OBJECT_TYPE_NAME(self), + plink ? plink->ifindex : 0); + + klass = NM_DEVICE_GET_CLASS(self); + platform = nm_device_get_platform(self); + + /* Balanced by a thaw in nm_device_realize_finish() */ + g_object_freeze_notify(G_OBJECT(self)); + + priv->mtu_source = NM_DEVICE_MTU_SOURCE_NONE; + priv->mtu_initial = 0; + priv->ip6_mtu_initial = 0; + priv->ip6_mtu = 0; + _set_mtu(self, 0); + + _assume_state_set(self, assume_state_guess_assume, assume_state_connection_uuid); + + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_EXTERNAL); + + if (plink) + nm_device_update_from_platform_link(self, plink); + + if (priv->ifindex > 0) { + priv->physical_port_id = nm_platform_link_get_physical_port_id(platform, priv->ifindex); + _notify(self, PROP_PHYSICAL_PORT_ID); + + priv->dev_id = nm_platform_link_get_dev_id(platform, priv->ifindex); + + if (nm_platform_link_is_software(platform, priv->ifindex)) + capabilities |= NM_DEVICE_CAP_IS_SOFTWARE; + + _set_mtu(self, nm_platform_link_get_mtu(platform, priv->ifindex)); + + nm_platform_link_get_driver_info(platform, + priv->ifindex, + NULL, + &priv->driver_version, + &priv->firmware_version); + if (priv->driver_version) + _notify(self, PROP_DRIVER_VERSION); + if (priv->firmware_version) + _notify(self, PROP_FIRMWARE_VERSION); + + if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) + priv->ipv6ll_handle = nm_platform_link_get_user_ipv6ll_enabled(platform, priv->ifindex); + + if (nm_platform_link_supports_sriov(platform, priv->ifindex)) + capabilities |= NM_DEVICE_CAP_SRIOV; + } + + if (klass->get_generic_capabilities) + capabilities |= klass->get_generic_capabilities(self); + + _add_capabilities(self, capabilities); + + if (!priv->nm_owned && set_nm_owned && nm_device_is_software(self)) { + priv->nm_owned = TRUE; + _LOGD(LOGD_DEVICE, "set nm-owned from state file"); + } + + if (!priv->udi) { + /* Use a placeholder UDI until we get a real one */ + if (priv->udi_id == 0) { + static guint64 udi_id_counter = 0; + + priv->udi_id = ++udi_id_counter; + } + priv->udi = g_strdup_printf("/virtual/device/placeholder/%" G_GUINT64_FORMAT, priv->udi_id); + _notify(self, PROP_UDI); + } + + nm_device_update_hw_address(self); + nm_device_update_initial_hw_address(self); + nm_device_update_permanent_hw_address(self, FALSE); + + /* Note: initial hardware address must be read before calling get_ignore_carrier() */ + config = nm_config_get(); + priv->ignore_carrier = nm_config_data_get_ignore_carrier(nm_config_get_data(config), self); + if (!priv->config_changed_id) { + priv->config_changed_id = g_signal_connect(config, + NM_CONFIG_SIGNAL_CONFIG_CHANGED, + G_CALLBACK(config_changed), + self); + } + + nm_device_set_carrier_from_platform(self); + + nm_assert(!priv->stats.timeout_id); + real_rate = _stats_refresh_rate_real(priv->stats.refresh_rate_ms); + if (real_rate) + priv->stats.timeout_id = g_timeout_add(real_rate, _stats_timeout_cb, self); + + klass->realize_start_notify(self, plink); + + nm_assert(!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_EXPLICIT)); + nm_device_set_unmanaged_flags(self, NM_UNMANAGED_USER_EXPLICIT, unmanaged_user_explicit); + + /* Do not manage externally created software devices until they are IFF_UP + * or have IP addressing */ + nm_device_set_unmanaged_flags(self, + NM_UNMANAGED_EXTERNAL_DOWN, + is_unmanaged_external_down(self, TRUE)); + + /* Unmanaged the loopback device with an explicit NM_UNMANAGED_BY_TYPE flag. + * Later we might want to manage 'lo' too. Currently, that doesn't work because + * NetworkManager might down the interface or remove the 127.0.0.1 address. */ + nm_device_set_unmanaged_flags(self, NM_UNMANAGED_BY_TYPE, is_loopback(self)); + + nm_device_set_unmanaged_by_user_udev(self); + nm_device_set_unmanaged_by_user_conf(self); + + unmanaged = plink && !plink->initialized && !force_platform_init; + + nm_device_set_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT, unmanaged); +} + +/** + * nm_device_realize_finish(): + * @self: the #NMDevice + * @plink: the #NMPlatformLink if backed by a kernel netdevice + * + * Update the device's master/slave or parent/child relationships from + * backing resource properties. After this function finishes, the device + * is ready for network connectivity. + */ +void +nm_device_realize_finish(NMDevice *self, const NMPlatformLink *plink) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(!plink || link_type_compatible(self, plink->type, NULL, NULL)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_if_fail(!priv->real); + + if (plink) + device_recheck_slave_status(self, plink); + + priv->update_ip_config_completed_v4 = FALSE; + priv->update_ip_config_completed_v6 = FALSE; + + priv->real = TRUE; + _notify(self, PROP_REAL); + + nm_device_recheck_available_connections(self); + + /* Balanced by a freeze in realize_start_setup(). */ + g_object_thaw_notify(G_OBJECT(self)); +} + +static void +unrealize_notify(NMDevice *self) +{ + /* Stub implementation for unrealize_notify(). It does nothing, + * but allows derived classes to uniformly invoke the parent + * implementation. */ +} + +static gboolean +available_connections_check_delete_unrealized_on_idle(gpointer user_data) +{ + NMDevice * self = user_data; + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + priv->check_delete_unrealized_id = 0; + + if (g_hash_table_size(priv->available_connections) == 0 && !nm_device_is_real(self)) + g_signal_emit(self, signals[REMOVED], 0); + + return G_SOURCE_REMOVE; +} + +static void +available_connections_check_delete_unrealized(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + /* always rescheadule the remove signal. */ + nm_clear_g_source(&priv->check_delete_unrealized_id); + + if (g_hash_table_size(priv->available_connections) == 0 && !nm_device_is_real(self)) + priv->check_delete_unrealized_id = + g_idle_add(available_connections_check_delete_unrealized_on_idle, self); +} + +/** + * nm_device_unrealize(): + * @self: the #NMDevice + * @remove_resources: if %TRUE, remove backing resources + * @error: location to store error, or %NULL + * + * Clears any properties that depend on backing resources (kernel devices, + * etc) and removes those resources if @remove_resources is %TRUE. + * + * Returns: %TRUE on success, %FALSE on error + */ +gboolean +nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error) +{ + NMDevicePrivate *priv; + int ifindex; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + if (!nm_device_is_software(self) || !nm_device_is_real(self)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_SOFTWARE, + "This device is not a software device or is not realized"); + return FALSE; + } + + priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_val_if_fail(priv->iface != NULL, FALSE); + g_return_val_if_fail(priv->real, FALSE); + + ifindex = nm_device_get_ifindex(self); + + _LOGD(LOGD_DEVICE, "unrealize (ifindex %d)", ifindex > 0 ? ifindex : 0); + + nm_device_assume_state_reset(self); + + if (remove_resources) { + if (NM_DEVICE_GET_CLASS(self)->unrealize) { + if (!NM_DEVICE_GET_CLASS(self)->unrealize(self, error)) + return FALSE; + } else if (ifindex > 0) { + nm_platform_link_delete(nm_device_get_platform(self), ifindex); + } + } + + nm_clear_g_source(&priv->queued_ip_config_id_4); + nm_clear_g_source(&priv->queued_ip_config_id_6); + + g_object_freeze_notify(G_OBJECT(self)); + NM_DEVICE_GET_CLASS(self)->unrealize_notify(self); + + _parent_set_ifindex(self, 0, FALSE); + + _set_ifindex(self, 0, FALSE); + _set_ifindex(self, 0, TRUE); + if (nm_clear_g_free(&priv->ip_iface_)) + _notify(self, PROP_IP_IFACE); + + priv->master_ifindex = 0; + + _set_mtu(self, 0); + + if (priv->driver_version) { + nm_clear_g_free(&priv->driver_version); + _notify(self, PROP_DRIVER_VERSION); + } + if (priv->firmware_version) { + nm_clear_g_free(&priv->firmware_version); + _notify(self, PROP_FIRMWARE_VERSION); + } + if (priv->udi) { + nm_clear_g_free(&priv->udi); + _notify(self, PROP_UDI); + } + if (priv->path) { + nm_clear_g_free(&priv->path); + _notify(self, PROP_PATH); + } + if (priv->physical_port_id) { + nm_clear_g_free(&priv->physical_port_id); + _notify(self, PROP_PHYSICAL_PORT_ID); + } + + nm_clear_g_source(&priv->stats.timeout_id); + _stats_update_counters(self, 0, 0); + + priv->hw_addr_len_ = 0; + if (nm_clear_g_free(&priv->hw_addr)) + _notify(self, PROP_HW_ADDRESS); + priv->hw_addr_type = HW_ADDR_TYPE_UNSET; + if (nm_clear_g_free(&priv->hw_addr_perm)) + _notify(self, PROP_PERM_HW_ADDRESS); + nm_clear_g_free(&priv->hw_addr_initial); + + priv->capabilities = NM_DEVICE_CAP_NM_SUPPORTED; + if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities) + priv->capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self); + _notify(self, PROP_CAPABILITIES); + + nm_clear_g_signal_handler(nm_config_get(), &priv->config_changed_id); + nm_clear_g_signal_handler(priv->manager, &priv->ifindex_changed_id); + + priv->real = FALSE; + _notify(self, PROP_REAL); + + g_object_thaw_notify(G_OBJECT(self)); + + nm_device_set_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT, TRUE); + + nm_device_set_unmanaged_flags(self, + NM_UNMANAGED_PARENT | NM_UNMANAGED_BY_TYPE + | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_EXPLICIT + | NM_UNMANAGED_EXTERNAL_DOWN | NM_UNMANAGED_IS_SLAVE, + NM_UNMAN_FLAG_OP_FORGET); + + nm_device_state_changed(self, + NM_DEVICE_STATE_UNMANAGED, + remove_resources ? NM_DEVICE_STATE_REASON_USER_REQUESTED + : NM_DEVICE_STATE_REASON_NOW_UNMANAGED); + + /* Garbage-collect unneeded unrealized devices. */ + nm_device_recheck_available_connections(self); + + return TRUE; +} + +void +nm_device_notify_availability_maybe_changed(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->state != NM_DEVICE_STATE_DISCONNECTED) + return; + + /* A device could have stayed disconnected because it would + * want to register with a network server that now become + * available. */ + nm_device_recheck_available_connections(self); + if (g_hash_table_size(priv->available_connections) > 0) + nm_device_emit_recheck_auto_activate(self); +} + +/** + * nm_device_owns_iface(): + * @self: the #NMDevice + * @iface: an interface name + * + * Called by the manager to ask if the device or any of its components owns + * @iface. For example, a WWAN implementation would return %TRUE for an + * ethernet interface name that was owned by the WWAN device's modem component, + * because that ethernet interface is controlled by the WWAN device and cannot + * be used independently of the WWAN device. + * + * Returns: %TRUE if @self or its components own the interface name, + * %FALSE if not + */ +gboolean +nm_device_owns_iface(NMDevice *self, const char *iface) +{ + if (NM_DEVICE_GET_CLASS(self)->owns_iface) + return NM_DEVICE_GET_CLASS(self)->owns_iface(self, iface); + return FALSE; +} + +NMConnection * +nm_device_new_default_connection(NMDevice *self) +{ + NMConnection *connection; + GError * error = NULL; + + if (!NM_DEVICE_GET_CLASS(self)->new_default_connection) + return NULL; + + connection = NM_DEVICE_GET_CLASS(self)->new_default_connection(self); + if (!connection) + return NULL; + + if (!nm_connection_normalize(connection, NULL, NULL, &error)) { + _LOGD(LOGD_DEVICE, "device generated an invalid default connection: %s", error->message); + g_error_free(error); + g_return_val_if_reached(NULL); + } + + return connection; +} + +static void +slave_state_changed(NMDevice * slave, + NMDeviceState slave_new_state, + NMDeviceState slave_old_state, + NMDeviceStateReason reason, + NMDevice * self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean release = FALSE; + gboolean configure; + + _LOGD(LOGD_DEVICE, + "slave %s state change %d (%s) -> %d (%s)", + nm_device_get_iface(slave), + slave_old_state, + nm_device_state_to_str(slave_old_state), + slave_new_state, + nm_device_state_to_str(slave_new_state)); + + /* Don't try to enslave slaves until the master is ready */ + if (priv->state < NM_DEVICE_STATE_CONFIG) + return; + + if (slave_new_state == NM_DEVICE_STATE_IP_CONFIG) + nm_device_master_enslave_slave(self, slave, nm_device_get_applied_connection(slave)); + else if (slave_new_state > NM_DEVICE_STATE_ACTIVATED) + release = TRUE; + else if (slave_new_state <= NM_DEVICE_STATE_DISCONNECTED + && slave_old_state > NM_DEVICE_STATE_DISCONNECTED) { + /* Catch failures due to unavailable or unmanaged */ + release = TRUE; + } + + if (release) { + configure = priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED + && nm_device_sys_iface_state_get(slave) != NM_DEVICE_SYS_IFACE_STATE_EXTERNAL; + + nm_device_master_release_one_slave(self, slave, configure, FALSE, reason); + /* Bridge/bond/team interfaces are left up until manually deactivated */ + if (c_list_is_empty(&priv->slaves) && priv->state == NM_DEVICE_STATE_ACTIVATED) + _LOGD(LOGD_DEVICE, "last slave removed; remaining activated"); + } +} + +/** + * nm_device_master_add_slave: + * @self: the master device + * @slave: the slave device to enslave + * @configure: pass %TRUE if the slave should be configured by the master, or + * %FALSE if it is already configured outside NetworkManager + * + * If @self is capable of enslaving other devices (ie it's a bridge, bond, team, + * etc) then this function adds @slave to the slave list for later enslavement. + * + * Returns: %TRUE if the slave was enslaved. %FALSE means, the slave was already + * enslaved and nothing was done. + */ +static gboolean +nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure) +{ + NMDevicePrivate *priv; + NMDevicePrivate *slave_priv; + SlaveInfo * info; + gboolean changed = FALSE; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_DEVICE(slave), FALSE); + g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + slave_priv = NM_DEVICE_GET_PRIVATE(slave); + + info = find_slave_info(self, slave); + + _LOGT(LOGD_CORE, + "master: add one slave %p/%s%s", + slave, + nm_device_get_iface(slave), + info ? " (already registered)" : ""); + + if (configure) + g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE); + + if (!info) { + g_return_val_if_fail(!slave_priv->master, FALSE); + g_return_val_if_fail(!slave_priv->is_enslaved, FALSE); + + info = g_slice_new0(SlaveInfo); + info->slave = g_object_ref(slave); + info->configure = configure; + info->watch_id = + g_signal_connect(slave, NM_DEVICE_STATE_CHANGED, G_CALLBACK(slave_state_changed), self); + c_list_link_tail(&priv->slaves, &info->lst_slave); + slave_priv->master = g_object_ref(self); + + _active_connection_set_state_flags(self, NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES); + + /* no need to emit + * + * _notify (slave, PROP_MASTER); + * + * because slave_priv->is_enslaved is not true, thus the value + * didn't change yet. */ + + g_warn_if_fail(!NM_FLAGS_HAS(slave_priv->unmanaged_mask, NM_UNMANAGED_IS_SLAVE)); + nm_device_set_unmanaged_by_flags(slave, + NM_UNMANAGED_IS_SLAVE, + FALSE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + changed = TRUE; + } else + g_return_val_if_fail(slave_priv->master == self, FALSE); + + nm_device_queue_recheck_assume(self); + nm_device_queue_recheck_assume(slave); + + return changed; +} + +/** + * nm_device_master_check_slave_physical_port: + * @self: the master device + * @slave: a slave device + * @log_domain: domain to log a warning in + * + * Checks if @self already has a slave with the same #NMDevice:physical-port-id + * as @slave, and logs a warning if so. + */ +void +nm_device_master_check_slave_physical_port(NMDevice *self, NMDevice *slave, NMLogDomain log_domain) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const char * slave_physical_port_id, *existing_physical_port_id; + SlaveInfo * info; + CList * iter; + + slave_physical_port_id = nm_device_get_physical_port_id(slave); + if (!slave_physical_port_id) + return; + + c_list_for_each (iter, &priv->slaves) { + info = c_list_entry(iter, SlaveInfo, lst_slave); + if (info->slave == slave) + continue; + + existing_physical_port_id = nm_device_get_physical_port_id(info->slave); + if (nm_streq0(slave_physical_port_id, existing_physical_port_id)) { + _LOGW(log_domain, + "slave %s shares a physical port with existing slave %s", + nm_device_get_ip_iface(slave), + nm_device_get_ip_iface(info->slave)); + /* Since this function will get called for every slave, we only have + * to warn about the first match we find; if there are other matches + * later in the list, we will have already warned about them matching + * @existing earlier. + */ + return; + } + } +} + +/* release all slaves */ +void +nm_device_master_release_slaves(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceStateReason reason; + CList * iter, *safe; + + /* Don't release the slaves if this connection doesn't belong to NM. */ + if (nm_device_sys_iface_state_is_external(self)) + return; + + reason = priv->state_reason; + if (priv->state == NM_DEVICE_STATE_FAILED) + reason = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED; + + c_list_for_each_safe (iter, safe, &priv->slaves) { + SlaveInfo *info = c_list_entry(iter, SlaveInfo, lst_slave); + + nm_device_master_release_one_slave(self, info->slave, TRUE, FALSE, reason); + } +} + +/** + * nm_device_is_master: + * @self: the device + * + * Returns: %TRUE if the device can have slaves + */ +gboolean +nm_device_is_master(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + return NM_DEVICE_GET_CLASS(self)->is_master; +} + +/** + * nm_device_get_master: + * @self: the device + * + * If @self has been enslaved by another device, this returns that + * device. Otherwise, it returns %NULL. (In particular, note that if + * @self is in the process of activating as a slave, but has not yet + * been enslaved by its master, this will return %NULL.) + * + * Returns: (transfer none): @self's master, or %NULL + */ +NMDevice * +nm_device_get_master(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->is_enslaved) { + g_return_val_if_fail(priv->master, NULL); + return priv->master; + } + return NULL; +} + +static gboolean +get_ip_config_may_fail(NMDevice *self, int addr_family) +{ + NMConnection * connection; + NMSettingIPConfig *s_ip; + + connection = nm_device_get_applied_connection(self); + + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + + return !s_ip || nm_setting_ip_config_get_may_fail(s_ip); +} + +/* + * check_ip_state + * + * When @full_state_update is TRUE, transition the device from IP_CONFIG to the + * next state according to the outcome of IPv4 and IPv6 configuration. @may_fail + * indicates that we are called just after the initial configuration and thus + * IPv4/IPv6 are allowed to fail if the ipvx.may-fail properties say so, because + * the IP methods couldn't even be started. + * If @full_state_update is FALSE, just check if the connection should be failed + * due to the state of both ip families and the ipvx.may-fail settings. + */ +static void +check_ip_state(NMDevice *self, gboolean may_fail, gboolean full_state_update) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + gboolean ip4_disabled = FALSE, ip6_disabled = FALSE; + NMSettingIPConfig *s_ip4, *s_ip6; + NMDeviceState state; + + if (full_state_update && nm_device_get_state(self) != NM_DEVICE_STATE_IP_CONFIG) + return; + + /* Don't progress into IP_CHECK or SECONDARIES if we're waiting for the + * master to enslave us. */ + if (nm_active_connection_get_master(NM_ACTIVE_CONNECTION(priv->act_request.obj)) + && !priv->is_enslaved) + return; + + s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG); + if (s_ip4 + && nm_streq0(nm_setting_ip_config_get_method(s_ip4), NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) + ip4_disabled = TRUE; + + s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG); + if (s_ip6 + && NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip6), + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) + ip6_disabled = TRUE; + + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE + && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) { + /* Both method completed (or disabled), proceed with activation */ + nm_device_state_changed(self, NM_DEVICE_STATE_IP_CHECK, NM_DEVICE_STATE_REASON_NONE); + return; + } + + if ((priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL + || (ip4_disabled && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE)) + && (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL + || (ip6_disabled && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE))) { + /* Either both methods failed, or only one failed and the other is + * disabled */ + if (nm_device_sys_iface_state_is_external_or_assume(self)) { + /* We have assumed configuration, but couldn't redo it. No problem, + * move to check state. */ + _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_DONE); + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_DONE); + state = NM_DEVICE_STATE_IP_CHECK; + } else if (may_fail && get_ip_config_may_fail(self, AF_INET) + && get_ip_config_may_fail(self, AF_INET6)) { + /* Couldn't start either IPv6 and IPv4 autoconfiguration, + * but both are allowed to fail. */ + state = NM_DEVICE_STATE_SECONDARIES; + } else { + /* Autoconfiguration attempted without success. */ + state = NM_DEVICE_STATE_FAILED; + } + + if (full_state_update || state == NM_DEVICE_STATE_FAILED) { + nm_device_state_changed(self, state, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } + return; + } + + /* If a method is still pending but required, wait */ + if (priv->ip_state_4 != NM_DEVICE_IP_STATE_DONE && !get_ip_config_may_fail(self, AF_INET)) + return; + if (priv->ip_state_6 != NM_DEVICE_IP_STATE_DONE && !get_ip_config_may_fail(self, AF_INET6)) + return; + + /* If at least a method has completed, proceed with activation */ + if ((priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !ip4_disabled) + || (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE && !ip6_disabled)) { + if (full_state_update) + nm_device_state_changed(self, NM_DEVICE_STATE_IP_CHECK, NM_DEVICE_STATE_REASON_NONE); + return; + } +} + +/** + * nm_device_slave_notify_enslave: + * @self: the slave device + * @success: whether the enslaving operation succeeded + * + * Notifies a slave that either it has been enslaved, or else its master tried + * to enslave it and failed. + */ +static void +nm_device_slave_notify_enslave(NMDevice *self, gboolean success) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection = nm_device_get_applied_connection(self); + gboolean activating = (priv->state == NM_DEVICE_STATE_IP_CONFIG); + + g_return_if_fail(priv->master); + + if (!priv->is_enslaved) { + if (success) { + if (activating) { + _LOGI(LOGD_DEVICE, + "Activation: connection '%s' enslaved, continuing activation", + nm_connection_get_id(connection)); + } else + _LOGI(LOGD_DEVICE, "enslaved to %s", nm_device_get_iface(priv->master)); + + priv->is_enslaved = TRUE; + + _notify(self, PROP_MASTER); + _notify(priv->master, PROP_SLAVES); + } else if (activating) { + _LOGW(LOGD_DEVICE, + "Activation: connection '%s' could not be enslaved", + nm_connection_get_id(connection)); + } + } + + if (activating) { + if (success) + check_ip_state(self, FALSE, TRUE); + else + nm_device_queue_state(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_UNKNOWN); + } else + nm_device_queue_recheck_assume(self); +} + +/** + * nm_device_slave_notify_release: + * @self: the slave device + * @reason: the reason associated with the state change + * + * Notifies a slave that it has been released, and why. + */ +static void +nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection = nm_device_get_applied_connection(self); + const char * master_status; + + g_return_if_fail(priv->master); + + if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state <= NM_DEVICE_STATE_ACTIVATED) { + switch (nm_device_state_reason_check(reason)) { + case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED: + master_status = "failed"; + break; + case NM_DEVICE_STATE_REASON_USER_REQUESTED: + reason = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED; + master_status = "deactivated by user request"; + break; + case NM_DEVICE_STATE_REASON_CONNECTION_REMOVED: + reason = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED; + master_status = "deactivated because master was removed"; + break; + default: + master_status = "deactivated"; + break; + } + + _LOGD(LOGD_DEVICE, + "Activation: connection '%s' master %s", + nm_connection_get_id(connection), + master_status); + + /* Cancel any pending activation sources */ + _cancel_activation(self); + nm_device_queue_state(self, NM_DEVICE_STATE_DEACTIVATING, reason); + } else + _LOGI(LOGD_DEVICE, "released from master device %s", nm_device_get_iface(priv->master)); + + if (priv->is_enslaved) { + priv->is_enslaved = FALSE; + _notify(self, PROP_MASTER); + _notify(priv->master, PROP_SLAVES); + } +} + +/** + * nm_device_removed: + * @self: the #NMDevice + * @unconfigure_ip_config: whether to clear the IP config objects + * of the device (provided, it is still not cleared at this point). + * + * Called by the manager when the device was removed. Releases the device from + * the master in case it's enslaved. + */ +void +nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + if (priv->master) { + /* this is called when something externally messes with the slave or during shut-down. + * Release the slave from master, but don't touch the device. */ + nm_device_master_release_one_slave(priv->master, + self, + FALSE, + FALSE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + } + + if (unconfigure_ip_config) { + nm_device_set_ip_config(self, AF_INET, NULL, FALSE, NULL); + nm_device_set_ip_config(self, AF_INET6, NULL, FALSE, NULL); + } else { + if (priv->dhcp_data_4.client) + nm_dhcp_client_stop(priv->dhcp_data_4.client, FALSE); + if (priv->dhcp_data_6.client) + nm_dhcp_client_stop(priv->dhcp_data_6.client, FALSE); + } +} + +static gboolean +is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->carrier || priv->ignore_carrier) + return TRUE; + + if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER)) + return TRUE; + + /* master types are always available even without carrier. */ + if (nm_device_is_master(self)) + return TRUE; + + return FALSE; +} + +/** + * nm_device_is_available: + * @self: the #NMDevice + * @flags: additional flags to influence the check. Flags have the + * meaning to increase the availability of a device. + * + * Checks if @self would currently be capable of activating a + * connection. In particular, it checks that the device is ready (eg, + * is not missing firmware), that it has carrier (if necessary), and + * that any necessary external software (eg, ModemManager, + * wpa_supplicant) is available. + * + * @self can only be in a state higher than + * %NM_DEVICE_STATE_UNAVAILABLE when nm_device_is_available() returns + * %TRUE. (But note that it can still be %NM_DEVICE_STATE_UNMANAGED + * when it is available.) + * + * Returns: %TRUE or %FALSE + */ +gboolean +nm_device_is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->firmware_missing) + return FALSE; + + return NM_DEVICE_GET_CLASS(self)->is_available(self, flags); +} + +gboolean +nm_device_ignore_carrier_by_default(NMDevice *self) +{ + /* master types ignore-carrier by default. */ + return nm_device_is_master(self); +} + +gboolean +nm_device_get_enabled(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + if (NM_DEVICE_GET_CLASS(self)->get_enabled) + return NM_DEVICE_GET_CLASS(self)->get_enabled(self); + return TRUE; +} + +void +nm_device_set_enabled(NMDevice *self, gboolean enabled) +{ + g_return_if_fail(NM_IS_DEVICE(self)); + + if (NM_DEVICE_GET_CLASS(self)->set_enabled) + NM_DEVICE_GET_CLASS(self)->set_enabled(self, enabled); +} + +static NM_UTILS_FLAGS2STR_DEFINE(_autoconnect_blocked_flags_to_string, + NMDeviceAutoconnectBlockedFlags, + NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_NONE, "none"), + NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_USER, "user"), + NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN, + "wrong-pin"), + NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT, + "manual-disconnect"), ); + +NMDeviceAutoconnectBlockedFlags +nm_device_autoconnect_blocked_get(NMDevice *self, NMDeviceAutoconnectBlockedFlags mask) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + if (mask == 0) + mask = NM_DEVICE_AUTOCONNECT_BLOCKED_ALL; + + priv = NM_DEVICE_GET_PRIVATE(self); + return priv->autoconnect_blocked_flags & mask; +} + +void +nm_device_autoconnect_blocked_set_full(NMDevice * self, + NMDeviceAutoconnectBlockedFlags mask, + NMDeviceAutoconnectBlockedFlags value) +{ + NMDevicePrivate *priv; + gboolean changed; + char buf1[128], buf2[128]; + + g_return_if_fail(NM_IS_DEVICE(self)); + nm_assert(mask); + nm_assert(!NM_FLAGS_ANY(mask, ~NM_DEVICE_AUTOCONNECT_BLOCKED_ALL)); + nm_assert(!NM_FLAGS_ANY(value, ~mask)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + value = (priv->autoconnect_blocked_flags & ~mask) | (mask & value); + if (value == priv->autoconnect_blocked_flags) + return; + + changed = ((!value) != (!priv->autoconnect_blocked_flags)); + + _LOGT( + LOGD_DEVICE, + "autoconnect-blocked: set \"%s\" (was \"%s\")", + _autoconnect_blocked_flags_to_string(value, buf1, sizeof(buf1)), + _autoconnect_blocked_flags_to_string(priv->autoconnect_blocked_flags, buf2, sizeof(buf2))); + + priv->autoconnect_blocked_flags = value; + nm_assert(priv->autoconnect_blocked_flags == value); + if (changed) + _notify(self, PROP_AUTOCONNECT); +} + +static gboolean +autoconnect_allowed_accumulator(GSignalInvocationHint *ihint, + GValue * return_accu, + const GValue * handler_return, + gpointer data) +{ + if (!g_value_get_boolean(handler_return)) + g_value_set_boolean(return_accu, FALSE); + return TRUE; +} + +/** + * nm_device_autoconnect_allowed: + * @self: the #NMDevice + * + * Returns: %TRUE if the device can be auto-connected immediately, taking + * transient conditions into account (like companion devices that may wish to + * block autoconnect for a time). + */ +gboolean +nm_device_autoconnect_allowed(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceClass * klass = NM_DEVICE_GET_CLASS(self); + GValue instance = G_VALUE_INIT; + GValue retval = G_VALUE_INIT; + + if (nm_device_autoconnect_blocked_get(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL)) + return FALSE; + + if (klass->get_autoconnect_allowed && !klass->get_autoconnect_allowed(self)) + return FALSE; + + if (!nm_device_get_enabled(self)) + return FALSE; + + if (nm_device_is_real(self)) { + if (priv->state < NM_DEVICE_STATE_DISCONNECTED) + return FALSE; + } else { + if (!nm_device_check_unrealized_device_managed(self)) + return FALSE; + } + + if (priv->delete_on_deactivate_data) + return FALSE; + + /* The 'autoconnect-allowed' signal is emitted on a device to allow + * other listeners to block autoconnect on the device if they wish. + * This is mainly used by the OLPC Mesh devices to block autoconnect + * on their companion Wi-Fi device as they share radio resources and + * cannot be connected at the same time. + */ + + g_value_init(&instance, G_TYPE_OBJECT); + g_value_set_object(&instance, self); + + g_value_init(&retval, G_TYPE_BOOLEAN); + g_value_set_boolean(&retval, TRUE); + + /* Use g_signal_emitv() rather than g_signal_emit() to avoid the return + * value being changed if no handlers are connected */ + g_signal_emitv(&instance, signals[AUTOCONNECT_ALLOWED], 0, &retval); + g_value_unset(&instance); + + return g_value_get_boolean(&retval); +} + +static gboolean +can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object) +{ + nm_assert(!specific_object || !*specific_object); + return TRUE; +} + +/** + * nm_device_can_auto_connect: + * @self: an #NMDevice + * @sett_conn: a #NMSettingsConnection + * @specific_object: (out) (transfer full): on output, the path of an + * object associated with the returned connection, to be passed to + * nm_manager_activate_connection(), or %NULL. + * + * Checks if @sett_conn can be auto-activated on @self right now. + * This requires, at a minimum, that the connection be compatible with + * @self, and that it have the #NMSettingConnection:autoconnect property + * set, and that the device allow auto connections. Some devices impose + * additional requirements. (Eg, a Wi-Fi connection can only be activated + * if its SSID was seen in the last scan.) + * + * Returns: %TRUE, if the @sett_conn can be auto-activated. + **/ +gboolean +nm_device_can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(sett_conn), FALSE); + g_return_val_if_fail(!specific_object || !*specific_object, FALSE); + + /* the caller must ensure that nm_device_autoconnect_allowed() returns + * TRUE as well. This is done, because nm_device_can_auto_connect() + * has only one caller, and it iterates over a list of available + * connections. + * + * Hence, we don't need to re-check nm_device_autoconnect_allowed() + * over and over again. The caller is supposed to do that. */ + nm_assert(nm_device_autoconnect_allowed(self)); + + if (!nm_device_check_connection_available(self, + nm_settings_connection_get_connection(sett_conn), + NM_DEVICE_CHECK_CON_AVAILABLE_NONE, + NULL, + NULL)) + return FALSE; + + if (!NM_DEVICE_GET_CLASS(self)->can_auto_connect(self, sett_conn, specific_object)) + return FALSE; + + return TRUE; +} + +static gboolean +device_has_config(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + /* Check for IP configuration. */ + if (priv->ip_config_4 && nm_ip4_config_get_num_addresses(priv->ip_config_4)) + return TRUE; + if (priv->ip_config_6 && nm_ip6_config_get_num_addresses(priv->ip_config_6)) + return TRUE; + + /* The existence of a software device is good enough. */ + if (nm_device_is_software(self) && nm_device_is_real(self)) + return TRUE; + + /* Master-slave relationship is also a configuration */ + if (!c_list_is_empty(&priv->slaves) + || nm_platform_link_get_master(nm_device_get_platform(self), priv->ifindex) > 0) + return TRUE; + + return FALSE; +} + +/** + * nm_device_master_update_slave_connection: + * @self: the master #NMDevice + * @slave: the slave #NMDevice + * @connection: the #NMConnection to update with the slave settings + * @GError: (out): error description + * + * Reads the slave configuration for @slave and updates @connection with those + * properties. This invokes a virtual function on the master device @self. + * + * Returns: %TRUE if the configuration was read and @connection updated, + * %FALSE on failure. + */ +gboolean +nm_device_master_update_slave_connection(NMDevice * self, + NMDevice * slave, + NMConnection *connection, + GError ** error) +{ + NMDeviceClass *klass; + gboolean success; + + g_return_val_if_fail(self, FALSE); + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(slave, FALSE); + g_return_val_if_fail(connection, FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + g_return_val_if_fail(nm_connection_get_setting_connection(connection), FALSE); + + g_return_val_if_fail(nm_device_get_iface(self), FALSE); + + klass = NM_DEVICE_GET_CLASS(self); + if (klass->master_update_slave_connection) { + success = klass->master_update_slave_connection(self, slave, connection, error); + + g_return_val_if_fail(!error || (success && !*error) || *error, success); + return success; + } + + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "master device '%s' cannot update a slave connection for slave device '%s' (master " + "type not supported?)", + nm_device_get_iface(self), + nm_device_get_iface(slave)); + return FALSE; +} + +static gboolean +_get_maybe_ipv6_disabled(NMDevice *self) +{ + NMPlatform *platform; + int ifindex; + const char *path; + char ifname[IFNAMSIZ]; + + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex <= 0) + return FALSE; + + platform = nm_device_get_platform(self); + if (!nm_platform_if_indextoname(platform, ifindex, ifname)) + return FALSE; + + path = nm_sprintf_bufa(128, "/proc/sys/net/ipv6/conf/%s/disable_ipv6", ifname); + return (nm_platform_sysctl_get_int32(platform, NMP_SYSCTL_PATHID_ABSOLUTE(path), 0) == 0); +} + +NMConnection * +nm_device_generate_connection(NMDevice *self, + NMDevice *master, + gboolean *out_maybe_later, + GError ** error) +{ + NMDeviceClass * klass = NM_DEVICE_GET_CLASS(self); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const char * ifname = nm_device_get_iface(self); + gs_unref_object NMConnection *connection = NULL; + NMSetting * s_con; + NMSetting * s_ip4; + NMSetting * s_ip6; + char uuid[37]; + const char * ip4_method, *ip6_method; + GError * local = NULL; + const NMPlatformLink * pllink; + + NM_SET_OUT(out_maybe_later, FALSE); + + /* If update_connection() is not implemented, just fail. */ + if (!klass->update_connection) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "device class %s does not support generating a connection", + G_OBJECT_TYPE_NAME(self)); + return NULL; + } + + /* Return NULL if device is unconfigured. */ + if (!device_has_config(self)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "device has no existing configuration"); + return NULL; + } + + connection = nm_simple_connection_new(); + s_con = nm_setting_connection_new(); + + g_object_set(s_con, + NM_SETTING_CONNECTION_UUID, + nm_utils_uuid_generate_buf(uuid), + NM_SETTING_CONNECTION_ID, + ifname, + NM_SETTING_CONNECTION_AUTOCONNECT, + FALSE, + NM_SETTING_CONNECTION_INTERFACE_NAME, + ifname, + NM_SETTING_CONNECTION_TIMESTAMP, + (guint64) time(NULL), + NULL); + + if (klass->connection_type_supported) + g_object_set(s_con, NM_SETTING_CONNECTION_TYPE, klass->connection_type_supported, NULL); + + nm_connection_add_setting(connection, s_con); + + /* If the device is a slave, update various slave settings */ + if (master) { + if (!nm_device_master_update_slave_connection(master, self, connection, &local)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "master device '%s' failed to update slave connection: %s", + nm_device_get_iface(master), + local->message); + g_error_free(local); + return NULL; + } + } else { + /* Only regular and master devices get IP configuration; slaves do not */ + s_ip4 = nm_ip4_config_create_setting(priv->ip_config_4); + nm_connection_add_setting(connection, s_ip4); + + s_ip6 = nm_ip6_config_create_setting(priv->ip_config_6, _get_maybe_ipv6_disabled(self)); + nm_connection_add_setting(connection, s_ip6); + + nm_connection_add_setting(connection, nm_setting_proxy_new()); + + pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ifindex); + if (pllink && pllink->inet6_token.id) { + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + g_object_set(s_ip6, + NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE, + NM_IN6_ADDR_GEN_MODE_EUI64, + NM_SETTING_IP6_CONFIG_TOKEN, + nm_utils_inet6_interface_identifier_to_token(pllink->inet6_token, sbuf), + NULL); + } + } + + klass->update_connection(self, connection); + + if (!nm_connection_normalize(connection, NULL, NULL, &local)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "generated connection does not verify: %s", + local->message); + g_error_free(local); + return NULL; + } + + /* Ignore the connection if it has no IP configuration, + * no slave configuration, and is not a master interface. + */ + ip4_method = nm_utils_get_ip_config_method(connection, AF_INET); + ip6_method = nm_utils_get_ip_config_method(connection, AF_INET6); + if (nm_streq0(ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) + && NM_IN_STRSET(ip6_method, + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED) + && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con)) + && c_list_is_empty(&priv->slaves)) { + NM_SET_OUT(out_maybe_later, TRUE); + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "ignoring generated connection (no IP and not in master-slave relationship)"); + return NULL; + } + + /* Ignore any IPv6LL-only, not master connections without slaves, + * unless they are in the assume-ipv6ll-only list. + */ + if (nm_streq0(ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) + && nm_streq0(ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL) + && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con)) + && c_list_is_empty(&priv->slaves) + && !nm_config_data_get_assume_ipv6ll_only(NM_CONFIG_GET_DATA, self)) { + _LOGD(LOGD_DEVICE, + "ignoring generated connection (IPv6LL-only and not in master-slave relationship)"); + NM_SET_OUT(out_maybe_later, TRUE); + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "ignoring generated connection (IPv6LL-only and not in master-slave relationship)"); + return NULL; + } + + return g_steal_pointer(&connection); +} + +/** + * nm_device_complete_connection: + * + * Complete the connection. This is solely used for AddAndActivate where the user + * may pass in an incomplete connection and a device, and the device tries to + * make sense of it and complete it for activation. Otherwise, this is not + * used. + * + * Returns: success or failure. + */ +gboolean +nm_device_complete_connection(NMDevice * self, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceClass *klass; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE); + + klass = NM_DEVICE_GET_CLASS(self); + + if (!klass->complete_connection) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "Device class %s had no complete_connection method", + G_OBJECT_TYPE_NAME(self)); + return FALSE; + } + + if (!klass->complete_connection(self, connection, specific_object, existing_connections, error)) + return FALSE; + + if (!nm_connection_normalize(connection, NULL, NULL, error)) + return FALSE; + + return nm_device_check_connection_compatible(self, connection, error); +} + +gboolean +nm_device_match_parent(NMDevice *self, const char *parent) +{ + NMDevice *parent_device; + + g_return_val_if_fail(parent, FALSE); + + parent_device = nm_device_parent_get_device(self); + if (!parent_device) + return FALSE; + + if (nm_utils_is_uuid(parent)) { + NMConnection *connection; + + /* If the parent is a UUID, the connection matches when there is + * no connection active on the device or when a connection with + * that UUID is active. + */ + connection = nm_device_get_applied_connection(parent_device); + if (connection && !nm_streq0(parent, nm_connection_get_uuid(connection))) + return FALSE; + } else { + /* Interface name */ + if (!nm_streq0(parent, nm_device_get_ip_iface(parent_device))) + return FALSE; + } + + return TRUE; +} + +gboolean +nm_device_match_parent_hwaddr(NMDevice * device, + NMConnection *connection, + gboolean fail_if_no_hwaddr) +{ + NMSettingWired *s_wired; + NMDevice * parent_device; + const char * setting_mac; + const char * parent_mac; + + s_wired = nm_connection_get_setting_wired(connection); + if (!s_wired) + return !fail_if_no_hwaddr; + + setting_mac = nm_setting_wired_get_mac_address(s_wired); + if (!setting_mac) + return !fail_if_no_hwaddr; + + parent_device = nm_device_parent_get_device(device); + if (!parent_device) + return !fail_if_no_hwaddr; + + parent_mac = nm_device_get_permanent_hw_address(parent_device); + return parent_mac && nm_utils_hwaddr_matches(setting_mac, -1, parent_mac, -1); +} + +static gboolean +check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const char * device_iface = nm_device_get_iface(self); + gs_free_error GError *local = NULL; + gs_free char * conn_iface = NULL; + NMDeviceClass * klass; + NMSettingMatch * s_match; + + klass = NM_DEVICE_GET_CLASS(self); + if (klass->connection_type_check_compatible) { + if (!_nm_connection_check_main_setting(connection, + klass->connection_type_check_compatible, + error)) + return FALSE; + } else if (klass->check_connection_compatible == check_connection_compatible) { + /* the device class does not implement check_connection_compatible nor set + * connection_type_check_compatible. That means, it is by default not compatible + * with any connection type. */ + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "device does not support any connections"); + return FALSE; + } + + conn_iface = nm_manager_get_connection_iface(NM_MANAGER_GET, connection, NULL, NULL, &local); + + /* We always need a interface name for virtual devices, but for + * physical ones a connection without interface name is fine for + * any device. */ + if (!conn_iface) { + if (nm_connection_is_virtual(connection)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "cannot get interface name due to %s", + local->message); + return FALSE; + } + } else if (!nm_streq0(conn_iface, device_iface)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "mismatching interface name"); + return FALSE; + } + + s_match = (NMSettingMatch *) nm_connection_get_setting(connection, NM_TYPE_SETTING_MATCH); + if (s_match) { + const char *const *patterns; + const char * device_driver; + guint num_patterns = 0; + + patterns = nm_setting_match_get_interface_names(s_match, &num_patterns); + if (!nm_wildcard_match_check(device_iface, patterns, num_patterns)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device does not satisfy match.interface-name property"); + return FALSE; + } + + patterns = nm_setting_match_get_kernel_command_lines(s_match, &num_patterns); + if (num_patterns > 0 + && !nm_utils_kernel_cmdline_match_check(nm_utils_proc_cmdline_split(), + patterns, + num_patterns, + error)) + return FALSE; + + device_driver = nm_device_get_driver(self); + patterns = nm_setting_match_get_drivers(s_match, &num_patterns); + if (!nm_wildcard_match_check(device_driver, patterns, num_patterns)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device does not satisfy match.driver property"); + return FALSE; + } + + patterns = nm_setting_match_get_paths(s_match, &num_patterns); + if (!nm_wildcard_match_check(priv->path, patterns, num_patterns)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "device does not satisfy match.path property"); + return FALSE; + } + } + + return TRUE; +} + +/** + * nm_device_check_connection_compatible: + * @self: an #NMDevice + * @connection: an #NMConnection + * @error: optional reason why it is incompatible. Note that the + * error code is set to %NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + * if the profile is fundamentally incompatible with the device + * (most commonly, because the device-type does not support the + * connection-type). + * + * Checks if @connection could potentially be activated on @self. + * This means only that @self has the proper capabilities, and that + * @connection is not locked to some other device. It does not + * necessarily mean that @connection could be activated on @self + * right now. (Eg, it might refer to a Wi-Fi network that is not + * currently available.) + * + * Returns: #TRUE if @connection could potentially be activated on + * @self. + */ +gboolean +nm_device_check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE); + + return NM_DEVICE_GET_CLASS(self)->check_connection_compatible(self, connection, error); +} + +gboolean +nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave) +{ + NMSettingConnection *s_con; + const char * connection_type, *slave_type; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(slave), FALSE); + + if (!nm_device_is_master(self)) + return FALSE; + + /* All masters should have connection type set */ + connection_type = NM_DEVICE_GET_CLASS(self)->connection_type_supported; + g_return_val_if_fail(connection_type, FALSE); + + s_con = nm_connection_get_setting_connection(slave); + g_assert(s_con); + slave_type = nm_setting_connection_get_slave_type(s_con); + if (!slave_type) + return FALSE; + + return nm_streq(connection_type, slave_type); +} + +/** + * nm_device_can_assume_connections: + * @self: #NMDevice instance + * + * This is a convenience function to determine whether connection assumption + * is available for this device. + * + * Returns: %TRUE if the device is capable of assuming connections, %FALSE if not + */ +static gboolean +nm_device_can_assume_connections(NMDevice *self) +{ + return !!NM_DEVICE_GET_CLASS(self)->update_connection; +} + +static gboolean +unmanaged_on_quit(NMDevice *self) +{ + NMConnection *connection; + + /* NMDeviceWifi overwrites this function to always unmanage wifi devices. + * + * For all other types, if the device type can assume connections, we leave + * it up on quit. + * + * Originally, we would only keep devices up that can be assumed afterwards. + * However, that meant we unmanged layer-2 only devices. So, this was step + * by step refined to unmanage less (commit 25aaaab3, rh#1311988, rh#1333983). + * But there are more scenarios where we also want to keep the device up + * (rh#1378418, rh#1371126). */ + if (!nm_device_can_assume_connections(self)) + return TRUE; + + /* the only exception are IPv4 shared connections. We unmanage them on quit. */ + connection = nm_device_get_applied_connection(self); + if (connection) { + if (NM_IN_STRSET(nm_utils_get_ip_config_method(connection, AF_INET), + NM_SETTING_IP4_CONFIG_METHOD_SHARED)) { + /* shared connections are to be unmangaed. */ + return TRUE; + } + } + + return FALSE; +} + +gboolean +nm_device_unmanage_on_quit(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + return NM_DEVICE_GET_CLASS(self)->unmanaged_on_quit(self); +} + +static gboolean +nm_device_emit_recheck_assume(gpointer user_data) +{ + NMDevice * self = user_data; + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + priv->recheck_assume_id = 0; + if (!nm_device_get_act_request(self)) + g_signal_emit(self, signals[RECHECK_ASSUME], 0); + + return G_SOURCE_REMOVE; +} + +void +nm_device_queue_recheck_assume(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->recheck_assume_id && nm_device_can_assume_connections(self)) + priv->recheck_assume_id = g_idle_add(nm_device_emit_recheck_assume, self); +} + +static gboolean +recheck_available(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean now_available; + NMDeviceState state = nm_device_get_state(self); + NMDeviceState new_state = NM_DEVICE_STATE_UNKNOWN; + + priv->recheck_available.call_id = 0; + + now_available = nm_device_is_available(self, NM_DEVICE_CHECK_DEV_AVAILABLE_NONE); + + if (state == NM_DEVICE_STATE_UNAVAILABLE && now_available) { + new_state = NM_DEVICE_STATE_DISCONNECTED; + nm_device_queue_state(self, new_state, priv->recheck_available.available_reason); + } else if (state >= NM_DEVICE_STATE_DISCONNECTED && !now_available) { + new_state = NM_DEVICE_STATE_UNAVAILABLE; + nm_device_queue_state(self, new_state, priv->recheck_available.unavailable_reason); + } + + if (new_state > NM_DEVICE_STATE_UNKNOWN) { + _LOGD(LOGD_DEVICE, + "is %savailable, %s %s", + now_available ? "" : "not ", + new_state == NM_DEVICE_STATE_UNAVAILABLE ? "no change required for" + : "will transition to", + nm_device_state_to_str(new_state == NM_DEVICE_STATE_UNAVAILABLE ? state : new_state)); + + priv->recheck_available.available_reason = NM_DEVICE_STATE_REASON_NONE; + priv->recheck_available.unavailable_reason = NM_DEVICE_STATE_REASON_NONE; + } + + if (priv->recheck_available.call_id == 0) + nm_device_remove_pending_action(self, NM_PENDING_ACTION_RECHECK_AVAILABLE, TRUE); + + return G_SOURCE_REMOVE; +} + +void +nm_device_queue_recheck_available(NMDevice * self, + NMDeviceStateReason available_reason, + NMDeviceStateReason unavailable_reason) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->recheck_available.available_reason = available_reason; + priv->recheck_available.unavailable_reason = unavailable_reason; + if (!priv->recheck_available.call_id) { + priv->recheck_available.call_id = g_idle_add(recheck_available, self); + nm_device_add_pending_action (self, NM_PENDING_ACTION_RECHECK_AVAILABLE, + FALSE /* cannot assert, because of how recheck_available() first clears + * the call-id and postpones removing the pending-action. */); + } +} + +void +nm_device_emit_recheck_auto_activate(NMDevice *self) +{ + g_signal_emit(self, signals[RECHECK_AUTO_ACTIVATE], 0); +} + +static void +dnsmasq_state_changed_cb(NMDnsMasqManager *manager, guint32 status, gpointer user_data) +{ + NMDevice *self = NM_DEVICE(user_data); + + switch (status) { + case NM_DNSMASQ_STATUS_DEAD: + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_SHARED_START_FAILED); + break; + default: + break; + } +} + +void +nm_device_auth_request(NMDevice * self, + GDBusMethodInvocation * context, + NMConnection * connection, + const char * permission, + gboolean allow_interaction, + GCancellable * cancellable, + NMManagerDeviceAuthRequestFunc callback, + gpointer user_data) +{ + nm_manager_device_auth_request(nm_device_get_manager(self), + self, + context, + connection, + permission, + allow_interaction, + cancellable, + callback, + user_data); +} + +/*****************************************************************************/ + +static void +activation_source_clear(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + if (priv->activation_source_id_x[IS_IPv4] != 0) { + _LOGD(LOGD_DEVICE, + "activation-stage: clear %s,v%c (id %u)", + _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]), + nm_utils_addr_family_to_char(addr_family), + priv->activation_source_id_x[IS_IPv4]); + nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]); + priv->activation_source_func_x[IS_IPv4] = NULL; + } +} + +static gboolean +activation_source_handle_cb(NMDevice *self, int addr_family) +{ + NMDevicePrivate * priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + ActivationHandleFunc activation_source_func; + guint activation_source_id; + + g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + activation_source_func = priv->activation_source_func_x[IS_IPv4]; + activation_source_id = priv->activation_source_id_x[IS_IPv4]; + + g_return_val_if_fail(activation_source_id != 0, G_SOURCE_REMOVE); + nm_assert(activation_source_func); + + priv->activation_source_func_x[IS_IPv4] = NULL; + priv->activation_source_id_x[IS_IPv4] = 0; + + _LOGD(LOGD_DEVICE, + "activation-stage: invoke %s,v%c (id %u)", + _activation_func_to_string(activation_source_func), + nm_utils_addr_family_to_char(addr_family), + activation_source_id); + + activation_source_func(self); + + _LOGT(LOGD_DEVICE, + "activation-stage: complete %s,v%c (id %u)", + _activation_func_to_string(activation_source_func), + nm_utils_addr_family_to_char(addr_family), + activation_source_id); + + return G_SOURCE_REMOVE; +} + +static gboolean +activation_source_handle_cb_4(gpointer user_data) +{ + return activation_source_handle_cb(user_data, AF_INET); +} + +static gboolean +activation_source_handle_cb_6(gpointer user_data) +{ + return activation_source_handle_cb(user_data, AF_INET6); +} + +static void +activation_source_schedule(NMDevice *self, ActivationHandleFunc func, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + guint new_id = 0; + + if (priv->activation_source_id_x[IS_IPv4] != 0 + && priv->activation_source_func_x[IS_IPv4] == func) { + /* Scheduling the same stage multiple times is fine. */ + _LOGT(LOGD_DEVICE, + "activation-stage: already scheduled %s,v%c (id %u)", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family), + priv->activation_source_id_x[IS_IPv4]); + return; + } + + new_id = + g_idle_add(IS_IPv4 ? activation_source_handle_cb_4 : activation_source_handle_cb_6, self); + + if (priv->activation_source_id_x[IS_IPv4] != 0) { + _LOGD(LOGD_DEVICE, + "activation-stage: schedule %s,v%c which replaces %s,v%c (id %u -> %u)", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family), + _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]), + nm_utils_addr_family_to_char(addr_family), + priv->activation_source_id_x[IS_IPv4], + new_id); + nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]); + } else { + _LOGD(LOGD_DEVICE, + "activation-stage: schedule %s,v%c (id %u)", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family), + new_id); + } + + priv->activation_source_func_x[IS_IPv4] = func; + priv->activation_source_id_x[IS_IPv4] = new_id; +} + +static void +activation_source_invoke_sync(NMDevice *self, ActivationHandleFunc func, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + if (priv->activation_source_id_x[IS_IPv4] == 0) { + _LOGD(LOGD_DEVICE, + "activation-stage: synchronously invoke %s,v%c", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family)); + } else if (priv->activation_source_func_x[IS_IPv4] == func) { + _LOGD(LOGD_DEVICE, + "activation-stage: synchronously invoke %s,v%c which was already scheduled (id %u)", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family), + priv->activation_source_id_x[IS_IPv4]); + } else { + _LOGD(LOGD_DEVICE, + "activation-stage: synchronously invoke %s,v%c which replaces %s,v%c (id %u)", + _activation_func_to_string(func), + nm_utils_addr_family_to_char(addr_family), + _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]), + nm_utils_addr_family_to_char(addr_family), + priv->activation_source_id_x[IS_IPv4]); + } + + nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]); + priv->activation_source_func_x[IS_IPv4] = NULL; + + func(self); +} + +/*****************************************************************************/ + +static void +master_ready(NMDevice *self, NMActiveConnection *active) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMActiveConnection *master_connection; + NMDevice * master; + + /* Notify a master device that it has a new slave */ + nm_assert(nm_active_connection_get_master_ready(active)); + + master_connection = nm_active_connection_get_master(active); + + master = nm_active_connection_get_device(master_connection); + + _LOGD(LOGD_DEVICE, "master connection ready; master device %s", nm_device_get_iface(master)); + + if (priv->master && priv->master != master) + nm_device_master_release_one_slave(priv->master, + self, + FALSE, + FALSE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + + /* If the master didn't change, add-slave only rechecks whether to assume a connection. */ + nm_device_master_add_slave(master, + self, + !nm_device_sys_iface_state_is_external_or_assume(self)); +} + +static void +master_ready_cb(NMActiveConnection *active, GParamSpec *pspec, NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(nm_active_connection_get_master_ready(active)); + + if (priv->state == NM_DEVICE_STATE_PREPARE) + nm_device_activate_schedule_stage1_device_prepare(self, FALSE); +} + +static void +lldp_neighbors_changed(NMLldpListener *lldp_listener, GParamSpec *pspec, gpointer user_data) +{ + NMDevice *self = NM_DEVICE(user_data); + + _notify(self, PROP_LLDP_NEIGHBORS); +} + +static NMPlatformVF * +sriov_vf_config_to_platform(NMDevice *self, NMSriovVF *vf, GError **error) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free NMPlatformVF *plat_vf = NULL; + const guint * vlan_ids; + GVariant * variant; + guint i, num_vlans; + gsize length; + + g_return_val_if_fail(!error || !*error, FALSE); + + vlan_ids = nm_sriov_vf_get_vlan_ids(vf, &num_vlans); + plat_vf = g_malloc0(sizeof(NMPlatformVF) + sizeof(NMPlatformVFVlan) * num_vlans); + + plat_vf->index = nm_sriov_vf_get_index(vf); + + variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_SPOOF_CHECK); + if (variant) + plat_vf->spoofchk = g_variant_get_boolean(variant); + else + plat_vf->spoofchk = -1; + + variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_TRUST); + if (variant) + plat_vf->trust = g_variant_get_boolean(variant); + else + plat_vf->trust = -1; + + variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MAC); + if (variant) { + if (!_nm_utils_hwaddr_aton(g_variant_get_string(variant, NULL), + plat_vf->mac.data, + sizeof(plat_vf->mac.data), + &length)) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "invalid MAC %s", + g_variant_get_string(variant, NULL)); + return NULL; + } + if (length != priv->hw_addr_len) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "wrong MAC length %" G_GSIZE_FORMAT ", should be %u", + length, + priv->hw_addr_len); + return NULL; + } + plat_vf->mac.len = length; + } + + variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MIN_TX_RATE); + if (variant) + plat_vf->min_tx_rate = g_variant_get_uint32(variant); + + variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MAX_TX_RATE); + if (variant) + plat_vf->max_tx_rate = g_variant_get_uint32(variant); + + plat_vf->num_vlans = num_vlans; + plat_vf->vlans = (NMPlatformVFVlan *) (&plat_vf[1]); + for (i = 0; i < num_vlans; i++) { + plat_vf->vlans[i].id = vlan_ids[i]; + plat_vf->vlans[i].qos = nm_sriov_vf_get_vlan_qos(vf, vlan_ids[i]); + plat_vf->vlans[i].proto_ad = + nm_sriov_vf_get_vlan_protocol(vf, vlan_ids[i]) == NM_SRIOV_VF_VLAN_PROTOCOL_802_1AD; + } + + return g_steal_pointer(&plat_vf); +} + +static void +sriov_params_cb(GError *error, gpointer user_data) +{ + NMDevice * self; + NMDevicePrivate *priv; + nm_auto_freev NMPlatformVF **plat_vfs = NULL; + + nm_utils_user_data_unpack(user_data, &self, &plat_vfs); + + if (nm_utils_error_is_cancelled_or_disposing(error)) + return; + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (error) { + _LOGE(LOGD_DEVICE, "failed to set SR-IOV parameters: %s", error->message); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED); + return; + } + + if (!nm_platform_link_set_sriov_vfs(nm_device_get_platform(self), + priv->ifindex, + (const NMPlatformVF *const *) plat_vfs)) { + _LOGE(LOGD_DEVICE, "failed to apply SR-IOV VFs"); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED); + return; + } + + priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_COMPLETED; + + nm_device_activate_schedule_stage1_device_prepare(self, FALSE); +} + +/* + * activate_stage1_device_prepare + * + * Prepare for device activation + * + */ +static void +activate_stage1_device_prepare(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMActStageReturn ret = NM_ACT_STAGE_RETURN_SUCCESS; + NMActiveConnection *active; + NMActiveConnection *master; + NMDeviceClass * klass; + + priv->v4_route_table_initialized = FALSE; + priv->v6_route_table_initialized = FALSE; + + _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_NONE); + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_NONE); + + /* Notify the new ActiveConnection along with the state change */ + nm_dbus_track_obj_path_set(&priv->act_request, priv->act_request.obj, TRUE); + + nm_device_state_changed(self, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_REASON_NONE); + + if (priv->stage1_sriov_state != NM_DEVICE_STAGE_STATE_COMPLETED) { + NMSettingSriov *s_sriov; + + if (nm_device_sys_iface_state_is_external_or_assume(self)) { + /* pass */ + } else if (priv->stage1_sriov_state == NM_DEVICE_STAGE_STATE_PENDING) + return; + else if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV) + && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) { + nm_auto_freev NMPlatformVF **plat_vfs = NULL; + gs_free_error GError *error = NULL; + NMSriovVF * vf; + NMTernary autoprobe; + guint num; + guint i; + + autoprobe = nm_setting_sriov_get_autoprobe_drivers(s_sriov); + if (autoprobe == NM_TERNARY_DEFAULT) { + autoprobe = nm_config_data_get_connection_default_int64( + NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("sriov.autoprobe-drivers"), + self, + NM_OPTION_BOOL_FALSE, + NM_OPTION_BOOL_TRUE, + NM_OPTION_BOOL_TRUE); + } + + num = nm_setting_sriov_get_num_vfs(s_sriov); + plat_vfs = g_new0(NMPlatformVF *, num + 1); + for (i = 0; i < num; i++) { + vf = nm_setting_sriov_get_vf(s_sriov, i); + plat_vfs[i] = sriov_vf_config_to_platform(self, vf, &error); + if (!plat_vfs[i]) { + _LOGE(LOGD_DEVICE, + "failed to apply SR-IOV VF '%s': %s", + nm_utils_sriov_vf_to_str(vf, FALSE, NULL), + error->message); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED); + return; + } + } + + /* When changing the number of VFs the kernel can block + * for very long time in the write to sysfs, especially + * if autoprobe-drivers is enabled. Do it asynchronously + * to avoid blocking the entire NM process. + */ + sriov_op_queue(self, + nm_setting_sriov_get_total_vfs(s_sriov), + NM_TERNARY_TO_OPTION_BOOL(autoprobe), + sriov_params_cb, + nm_utils_user_data_pack(self, g_steal_pointer(&plat_vfs))); + priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_PENDING; + return; + } + priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_COMPLETED; + } + + /* Assumed connections were already set up outside NetworkManager */ + klass = NM_DEVICE_GET_CLASS(self); + + if (klass->act_stage1_prepare_set_hwaddr_ethernet + && !nm_device_sys_iface_state_is_external_or_assume(self)) { + if (!nm_device_hw_addr_set_cloned(self, nm_device_get_applied_connection(self), FALSE)) { + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + } + + if (klass->act_stage1_prepare_also_for_external_or_assume + || !nm_device_sys_iface_state_is_external_or_assume(self)) { + nm_assert(!klass->act_stage1_prepare_also_for_external_or_assume + || klass->act_stage1_prepare); + if (klass->act_stage1_prepare) { + NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE; + + ret = klass->act_stage1_prepare(self, &failure_reason); + if (ret == NM_ACT_STAGE_RETURN_FAILURE) { + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason); + return; + } + if (ret == NM_ACT_STAGE_RETURN_POSTPONE) + return; + + nm_assert(ret == NM_ACT_STAGE_RETURN_SUCCESS); + } + } + + active = NM_ACTIVE_CONNECTION(priv->act_request.obj); + master = nm_active_connection_get_master(active); + if (master) { + if (nm_active_connection_get_state(master) >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) { + _LOGD(LOGD_DEVICE, "master connection is deactivating"); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED); + return; + } + /* If the master connection is ready for slaves, attach ourselves */ + if (!nm_active_connection_get_master_ready(active)) { + if (priv->master_ready_id == 0) { + _LOGD(LOGD_DEVICE, "waiting for master connection to become ready"); + priv->master_ready_id = + g_signal_connect(active, + "notify::" NM_ACTIVE_CONNECTION_INT_MASTER_READY, + (GCallback) master_ready_cb, + self); + } + return; + } + } + nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id); + if (master) + master_ready(self, active); + else if (priv->master) { + nm_device_master_release_one_slave(priv->master, + self, + TRUE, + TRUE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + } + + nm_device_activate_schedule_stage2_device_config(self, TRUE); +} + +void +nm_device_activate_schedule_stage1_device_prepare(NMDevice *self, gboolean do_sync) +{ + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_DEVICE_GET_PRIVATE(self)->act_request.obj); + + if (!do_sync) { + activation_source_schedule(self, activate_stage1_device_prepare, AF_INET); + return; + } + + activation_source_invoke_sync(self, activate_stage1_device_prepare, AF_INET); +} + +static NMActStageReturn +act_stage2_config(NMDevice *self, NMDeviceStateReason *out_failure_reason) +{ + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +lldp_init(NMDevice *self, gboolean restart) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->ifindex > 0 && _prop_get_connection_lldp(self)) { + gs_free_error GError *error = NULL; + + if (priv->lldp_listener) { + if (restart && nm_lldp_listener_is_running(priv->lldp_listener)) + nm_lldp_listener_stop(priv->lldp_listener); + } else { + priv->lldp_listener = nm_lldp_listener_new(); + g_signal_connect(priv->lldp_listener, + "notify::" NM_LLDP_LISTENER_NEIGHBORS, + G_CALLBACK(lldp_neighbors_changed), + self); + } + + if (!nm_lldp_listener_is_running(priv->lldp_listener)) { + if (nm_lldp_listener_start(priv->lldp_listener, nm_device_get_ifindex(self), &error)) + _LOGD(LOGD_DEVICE, "LLDP listener %p started", priv->lldp_listener); + else { + _LOGD(LOGD_DEVICE, + "LLDP listener %p could not be started: %s", + priv->lldp_listener, + error->message); + } + } + } else { + if (priv->lldp_listener) + nm_lldp_listener_stop(priv->lldp_listener); + } +} + +/* set-mode can be: + * - TRUE: sync with new rules. + * - FALSE: sync, but remove all rules (== flush) + * - DEFAULT: forget about all the rules that we previously tracked, + * but don't actually remove them. This is when quitting NM + * we want to keep the rules. + * The problem is, after restart of NM, the rule manager will + * no longer remember that NM added these rules and treat them + * as externally added ones. Don't restart NetworkManager if + * you care about that. + */ +static void +_routing_rules_sync(NMDevice *self, NMTernary set_mode) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMPRulesManager *rules_manager = nm_netns_get_rules_manager(nm_device_get_netns(self)); + NMDeviceClass * klass = NM_DEVICE_GET_CLASS(self); + gboolean untrack_only_dirty = FALSE; + gboolean keep_deleted_rules; + gpointer user_tag_1; + gpointer user_tag_2; + + /* take two arbitrary user-tag pointers that belong to @self. */ + user_tag_1 = &priv->v4_route_table; + user_tag_2 = &priv->v6_route_table; + + if (set_mode == NM_TERNARY_TRUE) { + NMConnection * applied_connection; + NMSettingIPConfig *s_ip; + guint i, num; + int is_ipv4; + + untrack_only_dirty = TRUE; + nmp_rules_manager_set_dirty(rules_manager, user_tag_1); + if (klass->get_extra_rules) + nmp_rules_manager_set_dirty(rules_manager, user_tag_2); + + applied_connection = nm_device_get_applied_connection(self); + + for (is_ipv4 = 0; applied_connection && is_ipv4 < 2; is_ipv4++) { + int addr_family = is_ipv4 ? AF_INET : AF_INET6; + + s_ip = nm_connection_get_setting_ip_config(applied_connection, addr_family); + if (!s_ip) + continue; + + num = nm_setting_ip_config_get_num_routing_rules(s_ip); + for (i = 0; i < num; i++) { + NMPlatformRoutingRule plrule; + NMIPRoutingRule * rule; + + rule = nm_setting_ip_config_get_routing_rule(s_ip, i); + nm_ip_routing_rule_to_platform(rule, &plrule); + + /* We track this rule, but we also make it explicitly not weakly-tracked + * (meaning to untrack NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG at + * the same time). */ + nmp_rules_manager_track(rules_manager, + &plrule, + 10, + user_tag_1, + NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG); + } + } + + if (klass->get_extra_rules) { + gs_unref_ptrarray GPtrArray *extra_rules = NULL; + + extra_rules = klass->get_extra_rules(self); + if (extra_rules) { + for (i = 0; i < extra_rules->len; i++) { + nmp_rules_manager_track(rules_manager, + NMP_OBJECT_CAST_ROUTING_RULE(extra_rules->pdata[i]), + 10, + user_tag_2, + NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG); + } + } + } + } + + nmp_rules_manager_untrack_all(rules_manager, user_tag_1, !untrack_only_dirty); + if (klass->get_extra_rules) + nmp_rules_manager_untrack_all(rules_manager, user_tag_2, !untrack_only_dirty); + + keep_deleted_rules = FALSE; + if (set_mode == NM_TERNARY_DEFAULT) { + /* when exiting NM, we leave the device up and the rules configured. + * We just all nmp_rules_manager_sync() to forget about the synced rules, + * but we don't actually delete them. + * + * FIXME: that is a problem after restart of NetworkManager, because these + * rules will look like externally added, and NM will no longer remove + * them. + * To fix that, we could during "assume" mark the rules of the profile + * as owned (and "added" by the device). The problem with that is that it + * wouldn't cover rules that devices add by internal decision (not because + * of a setting in the profile, e.g. WireGuard could setup policy routing). + * Maybe it would be better to remember these orphaned rules at exit in a + * file and track them after restart again. */ + keep_deleted_rules = TRUE; + } + nmp_rules_manager_sync(rules_manager, keep_deleted_rules); +} + +static gboolean +tc_commit(NMDevice *self) +{ + NMConnection * connection = NULL; + gs_unref_ptrarray GPtrArray *qdiscs = NULL; + gs_unref_ptrarray GPtrArray *tfilters = NULL; + NMSettingTCConfig * s_tc = NULL; + NMPlatform * platform; + int ip_ifindex; + + platform = nm_device_get_platform(self); + connection = nm_device_get_applied_connection(self); + if (connection) + s_tc = nm_connection_get_setting_tc_config(connection); + + ip_ifindex = nm_device_get_ip_ifindex(self); + if (!ip_ifindex) + return s_tc == NULL; + + if (s_tc) { + qdiscs = nm_utils_qdiscs_from_tc_setting(platform, s_tc, ip_ifindex); + tfilters = nm_utils_tfilters_from_tc_setting(platform, s_tc, ip_ifindex); + } + + if (!nm_platform_qdisc_sync(platform, ip_ifindex, qdiscs)) + return FALSE; + + if (!nm_platform_tfilter_sync(platform, ip_ifindex, tfilters)) + return FALSE; + + return TRUE; +} + +/* + * activate_stage2_device_config + * + * Determine device parameters and set those on the device, ie + * for wireless devices, set SSID, keys, etc. + * + */ +static void +activate_stage2_device_config(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceClass * klass; + NMActStageReturn ret; + gboolean no_firmware = FALSE; + CList * iter; + + nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + + if (!nm_device_sys_iface_state_is_external_or_assume(self)) + _ethtool_state_set(self); + + if (!nm_device_sys_iface_state_is_external_or_assume(self)) { + if (!tc_commit(self)) { + _LOGW(LOGD_IP6, "failed applying traffic control rules"); + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + } + + _routing_rules_sync(self, NM_TERNARY_TRUE); + + if (!nm_device_sys_iface_state_is_external_or_assume(self)) { + if (!nm_device_bring_up(self, FALSE, &no_firmware)) { + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + no_firmware ? NM_DEVICE_STATE_REASON_FIRMWARE_MISSING + : NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + } + + klass = NM_DEVICE_GET_CLASS(self); + if (klass->act_stage2_config_also_for_external_or_assume + || !nm_device_sys_iface_state_is_external_or_assume(self)) { + NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE; + + ret = klass->act_stage2_config(self, &failure_reason); + if (ret == NM_ACT_STAGE_RETURN_POSTPONE) + return; + if (ret != NM_ACT_STAGE_RETURN_SUCCESS) { + nm_assert(ret == NM_ACT_STAGE_RETURN_FAILURE); + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason); + return; + } + } + + /* If we have slaves that aren't yet enslaved, do that now */ + c_list_for_each (iter, &priv->slaves) { + SlaveInfo * info = c_list_entry(iter, SlaveInfo, lst_slave); + NMDeviceState slave_state = nm_device_get_state(info->slave); + + if (slave_state == NM_DEVICE_STATE_IP_CONFIG) + nm_device_master_enslave_slave(self, + info->slave, + nm_device_get_applied_connection(info->slave)); + else if (priv->act_request.obj && nm_device_sys_iface_state_is_external(self) + && slave_state <= NM_DEVICE_STATE_DISCONNECTED) + nm_device_queue_recheck_assume(info->slave); + } + + lldp_init(self, TRUE); + + nm_device_activate_schedule_stage3_ip_config_start(self); +} + +void +nm_device_activate_schedule_stage2_device_config(NMDevice *self, gboolean do_sync) +{ + g_return_if_fail(NM_IS_DEVICE(self)); + + if (!do_sync) { + activation_source_schedule(self, activate_stage2_device_config, AF_INET); + return; + } + + activation_source_invoke_sync(self, activate_stage2_device_config, AF_INET); +} + +void +nm_device_ip_method_failed(NMDevice *self, int addr_family, NMDeviceStateReason reason) +{ + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6)); + + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL); + + if (get_ip_config_may_fail(self, addr_family)) + check_ip_state(self, FALSE, (nm_device_get_state(self) == NM_DEVICE_STATE_IP_CONFIG)); + else + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, reason); +} + +/*****************************************************************************/ + +static void +acd_data_destroy(gpointer ptr) +{ + AcdData *data = ptr; + int i; + + for (i = 0; data->configs && data->configs[i]; i++) + g_object_unref(data->configs[i]); + g_free(data->configs); + g_slice_free(AcdData, data); +} + +static void +ipv4_manual_method_apply(NMDevice *self, NMIP4Config **configs, gboolean success) +{ + NMConnection *connection; + const char * method; + + connection = nm_device_get_applied_connection(self); + nm_assert(connection); + method = nm_utils_get_ip_config_method(connection, AF_INET); + nm_assert(NM_IN_STRSET(method, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL, + NM_SETTING_IP4_CONFIG_METHOD_AUTO)); + + if (!success) { + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE); + return; + } + + if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) + nm_device_activate_schedule_ip_config_result(self, AF_INET, NULL); + else { + if (NM_DEVICE_GET_PRIVATE(self)->ip_state_4 != NM_DEVICE_IP_STATE_DONE) + ip_config_merge_and_apply(self, AF_INET, TRUE); + } +} + +static void +acd_manager_probe_terminated(NMAcdManager *acd_manager, gpointer user_data) +{ + AcdData * data = user_data; + NMDevice * self; + NMDevicePrivate * priv; + NMDedupMultiIter ipconf_iter; + const NMPlatformIP4Address *address; + gboolean result, success = TRUE; + int i; + + g_assert(data); + self = data->device; + priv = NM_DEVICE_GET_PRIVATE(self); + + for (i = 0; data->configs && data->configs[i]; i++) { + nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, data->configs[i], &address) { + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + result = nm_acd_manager_check_address(acd_manager, address->address); + success &= result; + + _NMLOG(result ? LOGL_DEBUG : LOGL_WARN, + LOGD_DEVICE, + "IPv4 DAD result: address %s is %s", + _nm_utils_inet4_ntop(address->address, sbuf), + result ? "unique" : "duplicate"); + } + } + + data->callback(self, data->configs, success); + + priv->acd.dad_list = g_slist_remove(priv->acd.dad_list, acd_manager); + nm_acd_manager_free(acd_manager); +} + +/** + * ipv4_dad_start: + * @self: device instance + * @configs: NULL-terminated array of IPv4 configurations + * @cb: callback function + * + * Start IPv4 DAD on device @self, check addresses in @configs and call @cb + * when the procedure ends. @cb will be called in any case, even if DAD can't + * be started. @configs will be unreferenced after @cb has been called. + */ +static void +ipv4_dad_start(NMDevice *self, NMIP4Config **configs, AcdCallback cb) +{ + static const NMAcdCallbacks acd_callbacks = { + .probe_terminated_callback = acd_manager_probe_terminated, + .user_data_destroy = acd_data_destroy, + }; + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMAcdManager * acd_manager; + const NMPlatformIP4Address *address; + NMDedupMultiIter ipconf_iter; + AcdData * data; + guint timeout; + gboolean addr_found; + int r; + const guint8 * hwaddr_arr; + size_t length; + guint i; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(configs); + g_return_if_fail(cb); + + for (i = 0, addr_found = FALSE; configs[i]; i++) { + if (nm_ip4_config_get_num_addresses(configs[i]) > 0) { + addr_found = TRUE; + break; + } + } + + timeout = _prop_get_ipv4_dad_timeout(self); + hwaddr_arr = nm_platform_link_get_address(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + &length); + + if (!timeout || !hwaddr_arr || !addr_found || length != ETH_ALEN + || nm_device_sys_iface_state_is_external_or_assume(self)) { + /* DAD not needed, signal success */ + cb(self, configs, TRUE); + + for (i = 0; configs[i]; i++) + g_object_unref(configs[i]); + g_free(configs); + + return; + } + + data = g_slice_new0(AcdData); + data->configs = configs; + data->callback = cb; + data->device = self; + + acd_manager = nm_acd_manager_new(nm_device_get_ip_ifindex(self), + hwaddr_arr, + length, + &acd_callbacks, + data); + priv->acd.dad_list = g_slist_append(priv->acd.dad_list, acd_manager); + + for (i = 0; configs[i]; i++) { + nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, configs[i], &address) + nm_acd_manager_add_address(acd_manager, address->address); + } + + r = nm_acd_manager_start_probe(acd_manager, timeout); + if (r < 0) { + _LOGW(LOGD_DEVICE, "acd probe failed"); + + /* DAD could not be started, signal success */ + cb(self, configs, TRUE); + + priv->acd.dad_list = g_slist_remove(priv->acd.dad_list, acd_manager); + nm_acd_manager_free(acd_manager); + } +} + +/*****************************************************************************/ +/* IPv4LL stuff */ + +static void +ipv4ll_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->ipv4ll) { + sd_ipv4ll_set_callback(priv->ipv4ll, NULL, NULL); + sd_ipv4ll_stop(priv->ipv4ll); + priv->ipv4ll = sd_ipv4ll_unref(priv->ipv4ll); + } + + nm_clear_g_source(&priv->ipv4ll_timeout); +} + +static NMIP4Config * +ipv4ll_get_ip4_config(NMDevice *self, guint32 lla) +{ + NMIP4Config * config = NULL; + NMPlatformIP4Address address; + NMPlatformIP4Route route; + + config = nm_device_ip4_config_new(self); + g_assert(config); + + memset(&address, 0, sizeof(address)); + nm_platform_ip4_address_set_addr(&address, lla, 16); + address.addr_source = NM_IP_CONFIG_SOURCE_IP4LL; + nm_ip4_config_add_address(config, &address); + + /* Add a multicast route for link-local connections: destination= 224.0.0.0, netmask=240.0.0.0 */ + memset(&route, 0, sizeof(route)); + route.network = htonl(0xE0000000L); + route.plen = 4; + route.rt_source = NM_IP_CONFIG_SOURCE_IP4LL; + route.table_coerced = nm_platform_route_table_coerce(nm_device_get_route_table(self, AF_INET)); + route.metric = nm_device_get_route_metric(self, AF_INET); + nm_ip4_config_add_route(config, &route, NULL); + + return config; +} + +static void +nm_device_handle_ipv4ll_event(sd_ipv4ll *ll, int event, void *data) +{ + NMDevice * self = data; + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + struct in_addr address; + NMIP4Config * config; + int r; + + if (priv->act_request.obj == NULL) + return; + + nm_assert(nm_streq(nm_device_get_effective_ip_config_method(self, AF_INET), + NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)); + + switch (event) { + case SD_IPV4LL_EVENT_BIND: + r = sd_ipv4ll_get_address(ll, &address); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "invalid IPv4 link-local address received, error %d.", r); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED); + return; + } + + if (!nm_utils_ip4_address_is_link_local(address.s_addr)) { + _LOGE(LOGD_AUTOIP4, "invalid address %08x received (not link-local).", address.s_addr); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_ERROR); + return; + } + + config = ipv4ll_get_ip4_config(self, address.s_addr); + if (config == NULL) { + _LOGE(LOGD_AUTOIP4, "failed to get IPv4LL config"); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED); + return; + } + + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) { + nm_clear_g_source(&priv->ipv4ll_timeout); + nm_device_activate_schedule_ip_config_result(self, AF_INET, NM_IP_CONFIG_CAST(config)); + } else if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) { + applied_config_init(&priv->dev_ip_config_4, config); + if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) { + _LOGE(LOGD_AUTOIP4, "failed to update IP4 config for autoip change."); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED); + } + } else + g_assert_not_reached(); + + g_object_unref(config); + break; + default: + _LOGW(LOGD_AUTOIP4, "IPv4LL address no longer valid after event %d.", event); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED); + } +} + +static gboolean +ipv4ll_timeout_cb(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->ipv4ll_timeout) { + _LOGI(LOGD_AUTOIP4, "IPv4LL configuration timed out."); + priv->ipv4ll_timeout = 0; + ipv4ll_cleanup(self); + + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) + nm_device_activate_schedule_ip_config_timeout(self, AF_INET); + } + + return FALSE; +} + +static NMActStageReturn +ipv4ll_start(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const struct ether_addr *addr; + int ifindex, r; + size_t addr_len; + + ipv4ll_cleanup(self); + + r = sd_ipv4ll_new(&priv->ipv4ll); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: new() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + r = sd_ipv4ll_attach_event(priv->ipv4ll, NULL, 0); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: attach_event() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + ifindex = nm_device_get_ip_ifindex(self); + addr = nm_platform_link_get_address(nm_device_get_platform(self), ifindex, &addr_len); + if (!addr || addr_len != ETH_ALEN) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: can't retrieve hardware address"); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + r = sd_ipv4ll_set_mac(priv->ipv4ll, addr); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: set_mac() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + r = sd_ipv4ll_set_ifindex(priv->ipv4ll, ifindex); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: set_ifindex() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + r = sd_ipv4ll_set_callback(priv->ipv4ll, nm_device_handle_ipv4ll_event, self); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: set_callback() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + r = sd_ipv4ll_start(priv->ipv4ll); + if (r < 0) { + _LOGE(LOGD_AUTOIP4, "IPv4LL: start() failed with error %d", r); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + _LOGI(LOGD_DEVICE | LOGD_AUTOIP4, "IPv4LL: started"); + + /* Start a timeout to bound the address attempt */ + priv->ipv4ll_timeout = g_timeout_add_seconds(20, ipv4ll_timeout_cb, self); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static void +ensure_con_ip_config(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMIPConfig * con_ip_config; + + if (priv->con_ip_config_x[IS_IPv4]) + return; + + connection = nm_device_get_applied_connection(self); + if (!connection) + return; + + con_ip_config = nm_device_ip_config_new(self, addr_family); + + if (IS_IPv4) { + nm_ip4_config_merge_setting(NM_IP4_CONFIG(con_ip_config), + nm_connection_get_setting_ip4_config(connection), + _prop_get_connection_mdns(self), + _prop_get_connection_llmnr(self), + nm_device_get_route_table(self, addr_family), + nm_device_get_route_metric(self, addr_family)); + } else { + nm_ip6_config_merge_setting(NM_IP6_CONFIG(con_ip_config), + nm_connection_get_setting_ip6_config(connection), + nm_device_get_route_table(self, addr_family), + nm_device_get_route_metric(self, addr_family)); + } + + if (nm_device_sys_iface_state_is_external_or_assume(self)) { + /* For assumed connections ignore all addresses and routes. */ + nm_ip_config_reset_addresses(con_ip_config); + nm_ip_config_reset_routes(con_ip_config); + } + + priv->con_ip_config_x[IS_IPv4] = con_ip_config; +} + +/*****************************************************************************/ + +static void +dhcp4_cleanup(NMDevice *self, CleanupType cleanup_type, gboolean release) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->dhcp_data_4.was_active = FALSE; + nm_clear_g_source(&priv->dhcp_data_4.grace_id); + priv->dhcp_data_4.grace_pending = FALSE; + nm_clear_g_free(&priv->dhcp4.pac_url); + + if (priv->dhcp_data_4.client) { + /* Stop any ongoing DHCP transaction on this device */ + nm_clear_g_signal_handler(priv->dhcp_data_4.client, &priv->dhcp_data_4.state_sigid); + + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE || cleanup_type == CLEANUP_TYPE_REMOVED) + nm_dhcp_client_stop(priv->dhcp_data_4.client, release); + + g_clear_object(&priv->dhcp_data_4.client); + } + + if (priv->dhcp_data_4.config) { + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config); + _notify(self, PROP_DHCP4_CONFIG); + } +} + +static gboolean +ip_config_merge_and_apply(NMDevice *self, int addr_family, gboolean commit) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean success; + gs_unref_object NMIPConfig *composite = NULL; + NMIPConfig * config; + gs_unref_ptrarray GPtrArray *ip4_dev_route_blacklist = NULL; + NMConnection * connection; + gboolean ignore_auto_routes = FALSE; + gboolean ignore_auto_dns = FALSE; + gboolean ignore_default_routes = FALSE; + GSList * iter; + const char * ip6_addr_gen_token = NULL; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + if (nm_device_sys_iface_state_is_external(self)) + commit = FALSE; + + connection = nm_device_get_applied_connection(self); + + /* Apply ignore-auto-routes and ignore-auto-dns settings */ + if (connection) { + NMSettingIPConfig *s_ip; + + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + if (s_ip) { + ignore_auto_routes = nm_setting_ip_config_get_ignore_auto_routes(s_ip); + ignore_auto_dns = nm_setting_ip_config_get_ignore_auto_dns(s_ip); + + /* if the connection has an explicit gateway, we also ignore + * the default routes from other sources. */ + ignore_default_routes = nm_setting_ip_config_get_never_default(s_ip) + || nm_setting_ip_config_get_gateway(s_ip); + + if (!IS_IPv4) { + NMSettingIP6Config *s_ip6 = NM_SETTING_IP6_CONFIG(s_ip); + + if (nm_setting_ip6_config_get_addr_gen_mode(s_ip6) + == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64) + ip6_addr_gen_token = nm_setting_ip6_config_get_token(s_ip6); + } + } + } + + composite = nm_device_ip_config_new(self, addr_family); + + if (!IS_IPv4) { + nm_ip6_config_set_privacy(NM_IP6_CONFIG(composite), + priv->ndisc ? priv->ndisc_use_tempaddr + : NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + } + + init_ip_config_dns_priority(self, composite); + + if (commit) { + if (priv->queued_ip_config_id_x[IS_IPv4]) + update_ext_ip_config(self, addr_family, FALSE); + ensure_con_ip_config(self, addr_family); + } + + if (!IS_IPv4) { + if (commit && priv->ipv6ll_has) { + const NMPlatformIP6Address ll_a = { + .address = priv->ipv6ll_addr, + .plen = 64, + .addr_source = NM_IP_CONFIG_SOURCE_IP6LL, + }; + const NMPlatformIP6Route ll_r = { + .network.s6_addr16[0] = htons(0xfe80u), + .plen = 64, + .metric = nm_device_get_route_metric(self, addr_family), + .rt_source = NM_IP_CONFIG_SOURCE_IP6LL, + }; + + nm_assert(IN6_IS_ADDR_LINKLOCAL(&priv->ipv6ll_addr)); + + nm_ip6_config_add_address(NM_IP6_CONFIG(composite), &ll_a); + nm_ip6_config_add_route(NM_IP6_CONFIG(composite), &ll_r, NULL); + } + } + + if (commit) { + gboolean v; + + v = default_route_metric_penalty_detect(self, addr_family); + if (IS_IPv4) + priv->default_route_metric_penalty_ip4_has = v; + else + priv->default_route_metric_penalty_ip6_has = v; + } + + /* Merge all the IP configs into the composite config */ + + if (IS_IPv4) { + config = applied_config_get_current(&priv->dev_ip_config_4); + if (config) { + nm_ip4_config_merge( + NM_IP4_CONFIG(composite), + NM_IP4_CONFIG(config), + (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0) + | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0) + | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0), + default_route_metric_penalty_get(self, addr_family)); + } + } + + if (!IS_IPv4) { + config = applied_config_get_current(&priv->ac_ip6_config); + if (config) { + nm_ip6_config_merge( + NM_IP6_CONFIG(composite), + NM_IP6_CONFIG(config), + (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0) + | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0) + | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0), + default_route_metric_penalty_get(self, addr_family)); + } + } + + if (!IS_IPv4) { + config = applied_config_get_current(&priv->dhcp6.ip6_config); + if (config) { + nm_ip6_config_merge( + NM_IP6_CONFIG(composite), + NM_IP6_CONFIG(config), + (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0) + | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0) + | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0), + default_route_metric_penalty_get(self, addr_family)); + } + } + + for (iter = priv->vpn_configs_x[IS_IPv4]; iter; iter = iter->next) + nm_ip_config_merge(composite, iter->data, NM_IP_CONFIG_MERGE_DEFAULT, 0); + + if (priv->ext_ip_config_x[IS_IPv4]) + nm_ip_config_merge(composite, + priv->ext_ip_config_x[IS_IPv4], + NM_IP_CONFIG_MERGE_EXTERNAL, + 0); + + /* Merge WWAN config *last* to ensure modem-given settings overwrite + * any external stuff set by pppd or other scripts. + */ + config = applied_config_get_current(&priv->dev2_ip_config_x[IS_IPv4]); + if (config) { + nm_ip_config_merge(composite, + config, + (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0) + | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0) + | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0), + default_route_metric_penalty_get(self, addr_family)); + } + + if (!IS_IPv4) { + if (priv->rt6_temporary_not_available) { + const NMPObject *o; + GHashTableIter hiter; + + g_hash_table_iter_init(&hiter, priv->rt6_temporary_not_available); + while (g_hash_table_iter_next(&hiter, (gpointer *) &o, NULL)) { + nm_ip6_config_add_route(NM_IP6_CONFIG(composite), + NMP_OBJECT_CAST_IP6_ROUTE(o), + NULL); + } + } + } + + /* Merge user overrides into the composite config. For assumed connections, + * con_ip_config_x is empty. */ + if (priv->con_ip_config_x[IS_IPv4]) { + nm_ip_config_merge(composite, + priv->con_ip_config_x[IS_IPv4], + NM_IP_CONFIG_MERGE_DEFAULT, + default_route_metric_penalty_get(self, addr_family)); + } + + if (commit) { + gboolean is_vrf; + + is_vrf = priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF; + + if (IS_IPv4) { + nm_ip4_config_add_dependent_routes(NM_IP4_CONFIG(composite), + nm_device_get_route_table(self, addr_family), + nm_device_get_route_metric(self, addr_family), + is_vrf, + &ip4_dev_route_blacklist); + } else { + nm_ip6_config_add_dependent_routes(NM_IP6_CONFIG(composite), + nm_device_get_route_table(self, addr_family), + nm_device_get_route_metric(self, addr_family), + is_vrf); + } + } + + if (IS_IPv4) { + if (commit) { + if (NM_DEVICE_GET_CLASS(self)->ip4_config_pre_commit) + NM_DEVICE_GET_CLASS(self)->ip4_config_pre_commit(self, NM_IP4_CONFIG(composite)); + } + } + + if (!IS_IPv4) { + NMUtilsIPv6IfaceId iid; + + if (commit && priv->ndisc_started && ip6_addr_gen_token + && nm_utils_ipv6_interface_identifier_get_from_token(&iid, ip6_addr_gen_token)) { + set_ipv6_token(self, iid, ip6_addr_gen_token); + } + } + + success = + nm_device_set_ip_config(self, addr_family, composite, commit, ip4_dev_route_blacklist); + if (commit) { + if (IS_IPv4) + priv->v4_commit_first_time = FALSE; + else + priv->v6_commit_first_time = FALSE; + } + + return success; +} + +static gboolean +dhcp4_lease_change(NMDevice *self, NMIP4Config *config, gboolean bound) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free_error GError *error = NULL; + + g_return_val_if_fail(config, FALSE); + + applied_config_init(&priv->dev_ip_config_4, config); + + if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) { + _LOGW(LOGD_DHCP4, "failed to update IPv4 config for DHCP change."); + return FALSE; + } + + /* TODO: we should perform DAD again whenever we obtain a + * new lease after an expiry. But what should we do if + * a duplicate address is detected? Fail the connection; + * restart DHCP; continue without an address? */ + if (bound && !nm_dhcp_client_accept(priv->dhcp_data_4.client, &error)) { + _LOGW(LOGD_DHCP4, "error accepting lease: %s", error->message); + return FALSE; + } + + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP4_CHANGE, self, NULL, NULL, NULL, NULL); + + return TRUE; +} + +static gboolean +dhcp_grace_period_expired(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + priv->dhcp_data_x[IS_IPv4].grace_id = 0; + priv->dhcp_data_x[IS_IPv4].grace_pending = FALSE; + + _LOGI(LOGD_DHCPX(IS_IPv4), + "DHCPv%c: grace period expired", + nm_utils_addr_family_to_char(addr_family)); + + nm_device_ip_method_failed(self, addr_family, NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED); + /* If the device didn't fail, the DHCP client will continue */ + + return G_SOURCE_REMOVE; +} + +static gboolean +dhcp_grace_period_expired_4(gpointer user_data) +{ + return dhcp_grace_period_expired(user_data, AF_INET); +} + +static gboolean +dhcp_grace_period_expired_6(gpointer user_data) +{ + return dhcp_grace_period_expired(user_data, AF_INET6); +} + +static gboolean +dhcp_grace_period_start(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + guint32 timeout; + + /* In any other case (expired lease, assumed connection, etc.), + * wait for some time before failing the IP method. + */ + if (priv->dhcp_data_x[IS_IPv4].grace_pending) { + /* already pending. */ + return FALSE; + } + + /* Start a grace period equal to the DHCP timeout multiplied + * by a constant factor. */ + timeout = _prop_get_ipvx_dhcp_timeout(self, addr_family); + if (timeout == NM_DHCP_TIMEOUT_INFINITY) + _LOGI(LOGD_DHCPX(IS_IPv4), + "DHCPv%c: trying to acquire a new lease", + nm_utils_addr_family_to_char(addr_family)); + else { + timeout = dhcp_grace_period_from_timeout(timeout); + _LOGI(LOGD_DHCPX(IS_IPv4), + "DHCPv%c: trying to acquire a new lease within %u seconds", + nm_utils_addr_family_to_char(addr_family), + timeout); + nm_assert(!priv->dhcp_data_x[IS_IPv4].grace_id); + priv->dhcp_data_x[IS_IPv4].grace_id = g_timeout_add_seconds( + timeout, + IS_IPv4 ? dhcp_grace_period_expired_4 : dhcp_grace_period_expired_6, + self); + } + + priv->dhcp_data_x[IS_IPv4].grace_pending = TRUE; + + return TRUE; +} +static void +dhcp4_fail(NMDevice *self, NMDhcpState dhcp_state) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + _LOGD(LOGD_DHCP4, + "DHCPv4 failed (ip_state %s, was_active %d)", + _ip_state_to_string(priv->ip_state_4), + priv->dhcp_data_4.was_active); + + /* The client is always left running after a failure. */ + + /* Nothing to do if we failed before... */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL) + goto clear_config; + + /* ... and also if there are static addresses configured + * on the interface. + */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && priv->con_ip_config_4 + && nm_ip4_config_get_num_addresses(priv->con_ip_config_4) > 0) + goto clear_config; + + /* Fail the method when one of the following is true: + * 1) the DHCP client terminated: it does not make sense to start a grace + * period without a client running; + * 2) we failed to get an initial lease AND the client was + * not active before. + */ + if (dhcp_state == NM_DHCP_STATE_TERMINATED + || (!priv->dhcp_data_4.was_active && priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF)) { + nm_device_activate_schedule_ip_config_timeout(self, AF_INET); + return; + } + + if (dhcp_grace_period_start(self, AF_INET)) + goto clear_config; + + return; + +clear_config: + /* The previous configuration is no longer valid */ + if (priv->dhcp_data_4.config) { + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config); + priv->dhcp_data_4.config = nm_dhcp_config_new(AF_INET); + _notify(self, PROP_DHCP4_CONFIG); + } +} + +static void +dhcp4_dad_cb(NMDevice *self, NMIP4Config **configs, gboolean success) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (success) { + nm_device_activate_schedule_ip_config_result(self, AF_INET, NM_IP_CONFIG_CAST(configs[1])); + } else { + nm_dhcp_client_decline(priv->dhcp_data_4.client, "Address conflict detected", NULL); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE); + } +} + +static void +dhcp4_state_changed(NMDhcpClient *client, + NMDhcpState state, + NMIP4Config * ip4_config, + GHashTable * options, + gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMIP4Config * manual, **configs; + NMConnection * connection; + + g_return_if_fail(nm_dhcp_client_get_addr_family(client) == AF_INET); + g_return_if_fail(!ip4_config || NM_IS_IP4_CONFIG(ip4_config)); + + _LOGD(LOGD_DHCP4, "new DHCPv4 client state %d", state); + + switch (state) { + case NM_DHCP_STATE_BOUND: + case NM_DHCP_STATE_EXTENDED: + if (!ip4_config) { + _LOGW(LOGD_DHCP4, "failed to get IPv4 config in response to DHCP event."); + dhcp4_fail(self, state); + break; + } + + nm_clear_g_source(&priv->dhcp_data_4.grace_id); + priv->dhcp_data_4.grace_pending = FALSE; + + /* After some failures, we have been able to renew the lease: + * update the ip state + */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL) + _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_CONF); + + g_free(priv->dhcp4.pac_url); + priv->dhcp4.pac_url = g_strdup(g_hash_table_lookup(options, "wpad")); + nm_device_set_proxy_config(self, priv->dhcp4.pac_url); + + nm_dhcp_config_set_options(priv->dhcp_data_4.config, options); + _notify(self, PROP_DHCP4_CONFIG); + + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) { + connection = nm_device_get_applied_connection(self); + g_assert(connection); + + manual = nm_device_ip4_config_new(self); + nm_ip4_config_merge_setting(manual, + nm_connection_get_setting_ip4_config(connection), + NM_SETTING_CONNECTION_MDNS_DEFAULT, + NM_SETTING_CONNECTION_LLMNR_DEFAULT, + nm_device_get_route_table(self, AF_INET), + nm_device_get_route_metric(self, AF_INET)); + + configs = g_new0(NMIP4Config *, 3); + configs[0] = manual; + configs[1] = g_object_ref(ip4_config); + + ipv4_dad_start(self, configs, dhcp4_dad_cb); + } else if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) { + if (dhcp4_lease_change(self, ip4_config, state == NM_DHCP_STATE_BOUND)) + nm_device_update_metered(self); + else + dhcp4_fail(self, state); + } + break; + case NM_DHCP_STATE_TIMEOUT: + dhcp4_fail(self, state); + break; + case NM_DHCP_STATE_EXPIRE: + /* Ignore expiry before we even have a lease (NAK, old lease, etc) */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) + break; + /* fall-through */ + case NM_DHCP_STATE_DONE: + case NM_DHCP_STATE_FAIL: + case NM_DHCP_STATE_TERMINATED: + dhcp4_fail(self, state); + break; + default: + break; + } +} + +static NMActStageReturn +dhcp4_start(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingIPConfig *s_ip4; + gs_unref_bytes GBytes *vendor_class_identifier = NULL; + gs_unref_bytes GBytes *hwaddr = NULL; + gs_unref_bytes GBytes *bcast_hwaddr = NULL; + gs_unref_bytes GBytes *client_id = NULL; + gs_free char * mud_url_free = NULL; + NMConnection * connection; + NMSettingConnection * s_con; + GError * error = NULL; + const NMPlatformLink * pllink; + const char *const * reject_servers; + + connection = nm_device_get_applied_connection(self); + g_return_val_if_fail(connection, FALSE); + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + + s_con = nm_connection_get_setting_connection(connection); + nm_assert(s_con); + + /* Clear old exported DHCP options */ + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config); + priv->dhcp_data_4.config = nm_dhcp_config_new(AF_INET); + + pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self)); + if (pllink) { + hwaddr = nmp_link_address_get_as_bytes(&pllink->l_address); + bcast_hwaddr = nmp_link_address_get_as_bytes(&pllink->l_broadcast); + } + + client_id = _prop_get_ipv4_dhcp_client_id(self, connection, hwaddr); + vendor_class_identifier = + _prop_get_ipv4_dhcp_vendor_class_identifier(self, NM_SETTING_IP4_CONFIG(s_ip4)); + reject_servers = nm_setting_ip_config_get_dhcp_reject_servers(s_ip4, NULL); + + g_warn_if_fail(priv->dhcp_data_4.client == NULL); + priv->dhcp_data_4.client = + nm_dhcp_manager_start_ip4(nm_dhcp_manager_get(), + nm_netns_get_multi_idx(nm_device_get_netns(self)), + nm_device_get_ip_iface(self), + nm_device_get_ip_ifindex(self), + hwaddr, + bcast_hwaddr, + nm_connection_get_uuid(connection), + nm_device_get_route_table(self, AF_INET), + nm_device_get_route_metric(self, AF_INET), + nm_setting_ip_config_get_dhcp_send_hostname(s_ip4), + nm_setting_ip_config_get_dhcp_hostname(s_ip4), + nm_setting_ip4_config_get_dhcp_fqdn(NM_SETTING_IP4_CONFIG(s_ip4)), + _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET), + _prop_get_connection_mud_url(self, s_con, &mud_url_free), + client_id, + _prop_get_ipvx_dhcp_timeout(self, AF_INET), + priv->dhcp_anycast_address, + NULL, + vendor_class_identifier, + reject_servers, + &error); + if (!priv->dhcp_data_4.client) { + _LOGW(LOGD_DHCP4, "failure to start DHCP: %s", error->message); + g_clear_error(&error); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + priv->dhcp_data_4.state_sigid = g_signal_connect(priv->dhcp_data_4.client, + NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED, + G_CALLBACK(dhcp4_state_changed), + self); + + if (nm_device_sys_iface_state_is_external_or_assume(self)) + priv->dhcp_data_4.was_active = TRUE; + + /* DHCP devices will be notified by the DHCP manager when stuff happens */ + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +gboolean +nm_device_dhcp4_renew(NMDevice *self, gboolean release) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_val_if_fail(priv->dhcp_data_4.client != NULL, FALSE); + + _LOGI(LOGD_DHCP4, "DHCPv4 lease renewal requested"); + + /* Terminate old DHCP instance and release the old lease */ + dhcp4_cleanup(self, CLEANUP_TYPE_DECONFIGURE, release); + + /* Start DHCP again on the interface */ + return dhcp4_start(self) != NM_ACT_STAGE_RETURN_FAILURE; +} + +/*****************************************************************************/ + +static NMIP4Config * +shared4_new_config(NMDevice *self, NMConnection *connection) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMIP4Config * config; + NMSettingIPConfig * s_ip4; + NMPlatformIP4Address address = { + .addr_source = NM_IP_CONFIG_SOURCE_SHARED, + }; + + g_return_val_if_fail(self, NULL); + g_return_val_if_fail(connection, NULL); + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + if (s_ip4 && nm_setting_ip_config_get_num_addresses(s_ip4) > 0) { + /* Use the first user-supplied address */ + NMIPAddress *user = nm_setting_ip_config_get_address(s_ip4, 0); + in_addr_t a; + + nm_ip_address_get_address_binary(user, &a); + nm_platform_ip4_address_set_addr(&address, a, nm_ip_address_get_prefix(user)); + nm_clear_pointer(&priv->shared_ip_handle, nm_netns_shared_ip_release); + } else { + if (!priv->shared_ip_handle) + priv->shared_ip_handle = nm_netns_shared_ip_reserve(nm_device_get_netns(self)); + nm_platform_ip4_address_set_addr(&address, priv->shared_ip_handle->addr, 24); + } + + config = nm_device_ip4_config_new(self); + nm_ip4_config_add_address(config, &address); + + return config; +} + +/*****************************************************************************/ + +static gboolean +connection_ip_method_requires_carrier(NMConnection *connection, + int addr_family, + gboolean * out_ip_enabled) +{ + const char *method; + + method = nm_utils_get_ip_config_method(connection, addr_family); + + if (NM_IS_IPv4(addr_family)) { + NM_SET_OUT(out_ip_enabled, !nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)); + return NM_IN_STRSET(method, + NM_SETTING_IP4_CONFIG_METHOD_AUTO, + NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL); + } + + NM_SET_OUT(out_ip_enabled, + !NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED)); + return NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_DHCP, + NM_SETTING_IP6_CONFIG_METHOD_SHARED, + NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL); +} + +static gboolean +connection_requires_carrier(NMConnection *connection) +{ + NMSettingIPConfig * s_ip4, *s_ip6; + NMSettingConnection *s_con; + gboolean ip4_carrier_wanted, ip6_carrier_wanted; + gboolean ip4_used = FALSE, ip6_used = FALSE; + + /* We can progress to IP_CONFIG now, so that we're enslaved. + * That may actually cause carrier to go up and thus continue activation. */ + s_con = nm_connection_get_setting_connection(connection); + if (nm_setting_connection_get_master(s_con)) + return FALSE; + + ip4_carrier_wanted = connection_ip_method_requires_carrier(connection, AF_INET, &ip4_used); + if (ip4_carrier_wanted) { + /* If IPv4 wants a carrier and cannot fail, the whole connection + * requires a carrier regardless of the IPv6 method. + */ + s_ip4 = nm_connection_get_setting_ip4_config(connection); + if (s_ip4 && !nm_setting_ip_config_get_may_fail(s_ip4)) + return TRUE; + } + + ip6_carrier_wanted = connection_ip_method_requires_carrier(connection, AF_INET6, &ip6_used); + if (ip6_carrier_wanted) { + /* If IPv6 wants a carrier and cannot fail, the whole connection + * requires a carrier regardless of the IPv4 method. + */ + s_ip6 = nm_connection_get_setting_ip6_config(connection); + if (s_ip6 && !nm_setting_ip_config_get_may_fail(s_ip6)) + return TRUE; + } + + /* If an IP version wants a carrier and the other IP version isn't + * used, the connection requires carrier since it will just fail without one. + */ + if (ip4_carrier_wanted && !ip6_used) + return TRUE; + if (ip6_carrier_wanted && !ip4_used) + return TRUE; + + /* If both want a carrier, the whole connection wants a carrier */ + return ip4_carrier_wanted && ip6_carrier_wanted; +} + +static gboolean +have_any_ready_slaves(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + SlaveInfo * info; + CList * iter; + + /* Any enslaved slave is "ready" in the generic case as it's + * at least >= NM_DEVCIE_STATE_IP_CONFIG and has had Layer 2 + * properties set up. + */ + c_list_for_each (iter, &priv->slaves) { + info = c_list_entry(iter, SlaveInfo, lst_slave); + if (NM_DEVICE_GET_PRIVATE(info->slave)->is_enslaved) + return TRUE; + } + return FALSE; +} + +/*****************************************************************************/ +/* DHCPv6 stuff */ + +static void +dhcp6_cleanup(NMDevice *self, CleanupType cleanup_type, gboolean release) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->dhcp_data_6.was_active = FALSE; + priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_NONE; + applied_config_clear(&priv->dhcp6.ip6_config); + nm_clear_g_free(&priv->dhcp6.event_id); + nm_clear_g_source(&priv->dhcp_data_6.grace_id); + priv->dhcp_data_6.grace_pending = FALSE; + + if (priv->dhcp_data_6.client) { + nm_clear_g_signal_handler(priv->dhcp_data_6.client, &priv->dhcp_data_6.state_sigid); + nm_clear_g_signal_handler(priv->dhcp_data_6.client, &priv->dhcp6.prefix_sigid); + + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE || cleanup_type == CLEANUP_TYPE_REMOVED) + nm_dhcp_client_stop(priv->dhcp_data_6.client, release); + + g_clear_object(&priv->dhcp_data_6.client); + } + + if (priv->dhcp_data_6.config) { + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config); + _notify(self, PROP_DHCP6_CONFIG); + } +} + +static gboolean +dhcp6_lease_change(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingsConnection *settings_connection; + + if (!applied_config_get_current(&priv->dhcp6.ip6_config)) { + _LOGW(LOGD_DHCP6, "failed to get DHCPv6 config for rebind"); + return FALSE; + } + + g_assert(priv->dhcp_data_6.client); /* sanity check */ + + settings_connection = nm_device_get_settings_connection(self); + g_assert(settings_connection); + + /* Apply the updated config */ + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) { + _LOGW(LOGD_DHCP6, "failed to update IPv6 config in response to DHCP event"); + return FALSE; + } + + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP6_CHANGE, self, NULL, NULL, NULL, NULL); + + return TRUE; +} + +static void +dhcp6_fail(NMDevice *self, NMDhcpState dhcp_state) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean is_dhcp_managed; + + _LOGD(LOGD_DHCP6, + "DHCPv6 failed (ip_state %s, was_active %d)", + _ip_state_to_string(priv->ip_state_6), + priv->dhcp_data_6.was_active); + + /* The client is always left running after a failure. */ + + /* Nothing to do if we failed before... */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL) + goto clear_config; + + is_dhcp_managed = (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED); + + if (is_dhcp_managed) { + /* ... and also if there are static addresses configured + * on the interface. + */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE && priv->con_ip_config_6 + && nm_ip6_config_get_num_addresses(priv->con_ip_config_6)) + goto clear_config; + + /* Fail the method when one of the following is true: + * 1) the DHCP client terminated: it does not make sense to start a grace + * period without a client running; + * 2) we failed to get an initial lease AND the client was + * not active before. + */ + if (dhcp_state == NM_DHCP_STATE_TERMINATED + || (!priv->dhcp_data_6.was_active && priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF)) { + nm_device_activate_schedule_ip_config_timeout(self, AF_INET6); + return; + } + + if (dhcp_grace_period_start(self, AF_INET6)) + goto clear_config; + } else { + /* not a hard failure; just live with the RA info */ + dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, FALSE); + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + } + return; + +clear_config: + /* The previous configuration is no longer valid */ + if (priv->dhcp_data_6.config) { + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config); + priv->dhcp_data_6.config = nm_dhcp_config_new(AF_INET6); + _notify(self, PROP_DHCP6_CONFIG); + } +} + +static void +dhcp6_state_changed(NMDhcpClient *client, + NMDhcpState state, + NMIP6Config * ip6_config, + GHashTable * options, + gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free char * event_id = NULL; + + g_return_if_fail(nm_dhcp_client_get_addr_family(client) == AF_INET6); + g_return_if_fail(!ip6_config || NM_IS_IP6_CONFIG(ip6_config)); + + _LOGD(LOGD_DHCP6, "new DHCPv6 client state %d", state); + + switch (state) { + case NM_DHCP_STATE_BOUND: + case NM_DHCP_STATE_EXTENDED: + nm_clear_g_source(&priv->dhcp_data_6.grace_id); + priv->dhcp_data_6.grace_pending = FALSE; + /* If the server sends multiple IPv6 addresses, we receive a state + * changed event for each of them. Use the event ID to merge IPv6 + * addresses from the same transaction into a single configuration. + */ + + event_id = nm_dhcp_utils_get_dhcp6_event_id(options); + + if (ip6_config && event_id && priv->dhcp6.event_id + && nm_streq(event_id, priv->dhcp6.event_id)) { + NMDedupMultiIter ipconf_iter; + const NMPlatformIP6Address *a; + + nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, ip6_config, &a) + applied_config_add_address(&priv->dhcp6.ip6_config, NM_PLATFORM_IP_ADDRESS_CAST(a)); + } else { + nm_clear_g_free(&priv->dhcp6.event_id); + if (ip6_config) { + applied_config_init(&priv->dhcp6.ip6_config, ip6_config); + priv->dhcp6.event_id = g_strdup(event_id); + nm_dhcp_config_set_options(priv->dhcp_data_6.config, options); + _notify(self, PROP_DHCP6_CONFIG); + } else + applied_config_clear(&priv->dhcp6.ip6_config); + } + + /* After long time we have been able to renew the lease: + * update the ip state + */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL) + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_CONF); + + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) { + if (!applied_config_get_current(&priv->dhcp6.ip6_config)) { + nm_device_ip_method_failed(self, AF_INET6, NM_DEVICE_STATE_REASON_DHCP_FAILED); + break; + } + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + } else if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) + if (!dhcp6_lease_change(self)) + dhcp6_fail(self, state); + break; + case NM_DHCP_STATE_TIMEOUT: + if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED) + dhcp6_fail(self, state); + else { + /* not a hard failure; just live with the RA info */ + dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, FALSE); + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + } + break; + case NM_DHCP_STATE_EXPIRE: + /* Ignore expiry before we even have a lease (NAK, old lease, etc) */ + if (priv->ip_state_6 != NM_DEVICE_IP_STATE_CONF) + dhcp6_fail(self, state); + break; + case NM_DHCP_STATE_TERMINATED: + /* In IPv6 info-only mode, the client doesn't handle leases so it + * may exit right after getting a response from the server. That's + * normal. In that case we just ignore the exit. + */ + if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF) + break; + /* fall-through */ + case NM_DHCP_STATE_DONE: + case NM_DHCP_STATE_FAIL: + dhcp6_fail(self, state); + break; + default: + break; + } +} + +static void +dhcp6_prefix_delegated(NMDhcpClient *client, NMPlatformIP6Address *prefix, gpointer user_data) +{ + NMDevice *self = NM_DEVICE(user_data); + + /* Just re-emit. The device just contributes the prefix to the + * pool in NMPolicy, which decides about subnet allocation + * on the shared devices. */ + g_signal_emit(self, signals[IP6_PREFIX_DELEGATED], 0, prefix); +} + +/*****************************************************************************/ + +static gboolean +dhcp6_start_with_link_ready(NMDevice *self, NMConnection *connection) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingIPConfig *s_ip6; + gs_unref_bytes GBytes *hwaddr = NULL; + gs_unref_bytes GBytes * duid = NULL; + gboolean enforce_duid = FALSE; + const NMPlatformLink * pllink; + gs_free char * mud_url_free = NULL; + GError * error = NULL; + guint32 iaid; + gboolean iaid_explicit; + NMSettingConnection * s_con; + const NMPlatformIP6Address *ll_addr = NULL; + + g_return_val_if_fail(connection, FALSE); + + s_ip6 = nm_connection_get_setting_ip6_config(connection); + nm_assert(s_ip6); + s_con = nm_connection_get_setting_connection(connection); + nm_assert(s_con); + + if (priv->ext_ip6_config_captured) { + ll_addr = nm_ip6_config_find_first_address(priv->ext_ip6_config_captured, + NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL); + } + + if (!ll_addr) { + _LOGW(LOGD_DHCP6, "can't start DHCPv6: no link-local address"); + return FALSE; + } + + pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self)); + if (pllink) + hwaddr = nmp_link_address_get_as_bytes(&pllink->l_address); + + iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET6, connection, TRUE, &iaid_explicit); + duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, &enforce_duid); + + priv->dhcp_data_6.client = nm_dhcp_manager_start_ip6( + nm_dhcp_manager_get(), + nm_device_get_multi_index(self), + nm_device_get_ip_iface(self), + nm_device_get_ip_ifindex(self), + &ll_addr->address, + nm_connection_get_uuid(connection), + nm_device_get_route_table(self, AF_INET6), + nm_device_get_route_metric(self, AF_INET6), + nm_setting_ip_config_get_dhcp_send_hostname(s_ip6), + nm_setting_ip_config_get_dhcp_hostname(s_ip6), + _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET6), + _prop_get_connection_mud_url(self, s_con, &mud_url_free), + duid, + enforce_duid, + iaid, + iaid_explicit, + _prop_get_ipvx_dhcp_timeout(self, AF_INET6), + priv->dhcp_anycast_address, + (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF) ? TRUE : FALSE, + nm_setting_ip6_config_get_ip6_privacy(NM_SETTING_IP6_CONFIG(s_ip6)), + priv->dhcp6.needed_prefixes, + &error); + if (!priv->dhcp_data_6.client) { + _LOGW(LOGD_DHCP6, "failure to start DHCPv6: %s", error->message); + g_clear_error(&error); + if (nm_device_sys_iface_state_is_external_or_assume(self)) + priv->dhcp_data_6.was_active = TRUE; + return FALSE; + } + + priv->dhcp_data_6.state_sigid = g_signal_connect(priv->dhcp_data_6.client, + NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED, + G_CALLBACK(dhcp6_state_changed), + self); + priv->dhcp6.prefix_sigid = g_signal_connect(priv->dhcp_data_6.client, + NM_DHCP_CLIENT_SIGNAL_PREFIX_DELEGATED, + G_CALLBACK(dhcp6_prefix_delegated), + self); + + if (nm_device_sys_iface_state_is_external_or_assume(self)) + priv->dhcp_data_6.was_active = TRUE; + + return TRUE; +} + +static gboolean +dhcp6_start(NMDevice *self, gboolean wait_for_ll) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + + nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config); + priv->dhcp_data_6.config = nm_dhcp_config_new(AF_INET6); + + nm_assert(!applied_config_get_current(&priv->dhcp6.ip6_config)); + applied_config_clear(&priv->dhcp6.ip6_config); + nm_clear_g_free(&priv->dhcp6.event_id); + + connection = nm_device_get_applied_connection(self); + g_return_val_if_fail(connection, FALSE); + + if (wait_for_ll) { + /* ensure link local is ready... */ + if (!linklocal6_start(self)) { + /* wait for the LL address to show up */ + return TRUE; + } + /* already have the LL address; kick off DHCP */ + } + + if (!dhcp6_start_with_link_ready(self, connection)) + return FALSE; + + return TRUE; +} + +gboolean +nm_device_dhcp6_renew(NMDevice *self, gboolean release) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMNDiscDHCPLevel mode; + + g_return_val_if_fail(priv->dhcp_data_6.client != NULL, FALSE); + + _LOGI(LOGD_DHCP6, "DHCPv6 lease renewal requested"); + + /* Terminate old DHCP instance and release the old lease */ + mode = priv->dhcp6.mode; + dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, release); + priv->dhcp6.mode = mode; + + /* Start DHCP again on the interface */ + return dhcp6_start(self, FALSE); +} + +/*****************************************************************************/ + +/* + * Called on the requesting interface when a subnet can't be obtained + * from known prefixes for a newly active shared connection. + */ +void +nm_device_request_ip6_prefixes(NMDevice *self, int needed_prefixes) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->dhcp6.needed_prefixes = needed_prefixes; + + if (priv->dhcp_data_6.client) { + _LOGD(LOGD_IP6, "ipv6-pd: asking DHCPv6 for %d prefixes", needed_prefixes); + nm_device_dhcp6_renew(self, FALSE); + } else { + _LOGI(LOGD_IP6, "ipv6-pd: device doesn't use DHCPv6, can't request prefixes"); + } +} + +gboolean +nm_device_needs_ip6_subnet(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->needs_ip6_subnet; +} + +/* + * Called on the ipv6.method=shared interface when a new subnet is allocated + * or the prefix from which it is allocated is renewed. + */ +void +nm_device_use_ip6_subnet(NMDevice *self, const NMPlatformIP6Address *subnet) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatformIP6Address address = *subnet; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + if (!applied_config_get_current(&priv->ac_ip6_config)) + applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6); + + /* Assign a ::1 address in the subnet for us. */ + address.address.s6_addr32[3] |= htonl(1); + applied_config_add_address(&priv->ac_ip6_config, NM_PLATFORM_IP_ADDRESS_CAST(&address)); + + _LOGD(LOGD_IP6, + "ipv6-pd: using %s address (preferred for %u seconds)", + _nm_utils_inet6_ntop(&address.address, sbuf), + subnet->preferred); + + /* This also updates the ndisc if there are actual changes. */ + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "ipv6-pd: failed applying IP6 config for connection sharing"); +} + +/* + * Called whenever the policy picks a default IPv6 device. + * The ipv6.method=shared devices just reuse its DNS configuration. + */ +void +nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMIP6Config * from_config = NULL; + guint i, len; + + if (applied_config_get_current(&priv->ac_ip6_config)) { + applied_config_reset_nameservers(&priv->ac_ip6_config); + applied_config_reset_searches(&priv->ac_ip6_config); + } else + applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6); + + if (from_device) + from_config = nm_device_get_ip6_config(from_device); + if (!from_config) + return; + + len = nm_ip6_config_get_num_nameservers(from_config); + for (i = 0; i < len; i++) { + applied_config_add_nameserver( + &priv->ac_ip6_config, + (const NMIPAddr *) nm_ip6_config_get_nameserver(from_config, i)); + } + + len = nm_ip6_config_get_num_searches(from_config); + for (i = 0; i < len; i++) { + applied_config_add_search(&priv->ac_ip6_config, nm_ip6_config_get_search(from_config, i)); + } + + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "ipv6-pd: failed applying DNS config for connection sharing"); +} + +/*****************************************************************************/ + +static void +linklocal6_failed(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_clear_g_source(&priv->linklocal6_timeout_id); + nm_device_activate_schedule_ip_config_timeout(self, AF_INET6); +} + +static gboolean +linklocal6_timeout_cb(gpointer user_data) +{ + NMDevice *self = user_data; + + _LOGD(LOGD_DEVICE, "linklocal6: waiting for link-local addresses failed due to timeout"); + linklocal6_failed(self); + return G_SOURCE_REMOVE; +} + +static void +linklocal6_check_complete(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + const char * method; + + if (!priv->linklocal6_timeout_id) { + /* we are not waiting for linklocal to complete. Nothing to do. */ + return; + } + + if (!priv->ext_ip6_config_captured + || !nm_ip6_config_find_first_address(priv->ext_ip6_config_captured, + NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL)) { + /* we don't have a non-tentative link local address yet. Wait longer. */ + return; + } + + nm_clear_g_source(&priv->linklocal6_timeout_id); + + connection = nm_device_get_applied_connection(self); + g_assert(connection); + + method = nm_device_get_effective_ip_config_method(self, AF_INET6); + + _LOGD(LOGD_DEVICE, + "linklocal6: waiting for link-local addresses successful, continue with method %s", + method); + + if (NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_SHARED)) + addrconf6_start_with_link_ready(self); + else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DHCP)) { + if (!dhcp6_start_with_link_ready(self, connection)) { + /* Time out IPv6 instead of failing the entire activation */ + nm_device_activate_schedule_ip_config_timeout(self, AF_INET6); + } + } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)) + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + else + g_return_if_fail(FALSE); +} + +static void +check_and_add_ipv6ll_addr(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + struct in6_addr lladdr; + NMConnection * connection; + NMSettingIP6Config *s_ip6 = NULL; + GError * error = NULL; + const char * addr_type; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + if (!priv->ipv6ll_handle) + return; + + if (priv->ext_ip6_config_captured + && nm_ip6_config_find_first_address(priv->ext_ip6_config_captured, + NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE)) { + /* Already have an LL address, nothing to do */ + return; + } + + priv->ipv6ll_has = FALSE; + memset(&priv->ipv6ll_addr, 0, sizeof(priv->ipv6ll_addr)); + + memset(&lladdr, 0, sizeof(lladdr)); + lladdr.s6_addr16[0] = htons(0xfe80); + + connection = nm_device_get_applied_connection(self); + if (connection) + s_ip6 = NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection)); + + if (s_ip6 + && nm_setting_ip6_config_get_addr_gen_mode(s_ip6) + == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY) { + NMUtilsStableType stable_type; + const char * stable_id; + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + if (!nm_utils_ipv6_addr_set_stable_privacy(stable_type, + &lladdr, + nm_device_get_iface(self), + stable_id, + priv->linklocal6_dad_counter++, + &error)) { + _LOGW(LOGD_IP6, "linklocal6: failed to generate an address: %s", error->message); + g_clear_error(&error); + linklocal6_failed(self); + return; + } + addr_type = "stable-privacy"; + } else { + NMUtilsIPv6IfaceId iid; + + if (priv->linklocal6_timeout_id) { + /* We already started and attempt to add a LL address. For the EUI-64 + * mode we can't pick a new one, we'll just fail. */ + _LOGW(LOGD_IP6, "linklocal6: DAD failed for an EUI-64 address"); + linklocal6_failed(self); + return; + } + + if (!nm_device_get_ip_iface_identifier(self, &iid, TRUE)) { + _LOGW(LOGD_IP6, "linklocal6: failed to get interface identifier; IPv6 cannot continue"); + return; + } + nm_utils_ipv6_addr_set_interface_identifier(&lladdr, iid); + addr_type = "EUI-64"; + } + + _LOGD(LOGD_IP6, + "linklocal6: generated %s IPv6LL address %s", + addr_type, + _nm_utils_inet6_ntop(&lladdr, sbuf)); + priv->ipv6ll_has = TRUE; + priv->ipv6ll_addr = lladdr; + ip_config_merge_and_apply(self, AF_INET6, TRUE); +} + +static gboolean +linklocal6_start(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_clear_g_source(&priv->linklocal6_timeout_id); + + if (priv->ext_ip6_config_captured + && nm_ip6_config_find_first_address(priv->ext_ip6_config_captured, + NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL)) + return TRUE; + + _LOGD(LOGD_DEVICE, + "linklocal6: starting IPv6 with method '%s', but the device has no link-local addresses " + "configured. Wait.", + nm_device_get_effective_ip_config_method(self, AF_INET6)); + + check_and_add_ipv6ll_addr(self); + + /* Depending on the network and what the 'dad_transmits' and 'retrans_time_ms' + * sysctl values are, DAD for the IPv6LL address may take quite a while. + * FIXME: use dad/retrans sysctl values if they are higher than a minimum time. + * (rh #1101809) + */ + priv->linklocal6_timeout_id = g_timeout_add_seconds(15, linklocal6_timeout_cb, self); + return FALSE; +} + +/*****************************************************************************/ + +gint64 +nm_device_get_configured_mtu_from_connection_default(NMDevice * self, + const char *property_name, + guint32 max_mtu) +{ + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + property_name, + self, + 0, + max_mtu, + -1); +} + +guint32 +nm_device_get_configured_mtu_from_connection(NMDevice * self, + GType setting_type, + NMDeviceMtuSource *out_source) +{ + const char * global_property_name; + NMConnection *connection; + NMSetting * setting; + gint64 mtu_default; + guint32 mtu = 0; + guint32 max_mtu = G_MAXUINT32; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(out_source); + + connection = nm_device_get_applied_connection(self); + if (!connection) + g_return_val_if_reached(0); + + setting = nm_connection_get_setting(connection, setting_type); + + if (setting_type == NM_TYPE_SETTING_WIRED) { + if (setting) + mtu = nm_setting_wired_get_mtu(NM_SETTING_WIRED(setting)); + global_property_name = NM_CON_DEFAULT("ethernet.mtu"); + } else if (setting_type == NM_TYPE_SETTING_WIRELESS) { + if (setting) + mtu = nm_setting_wireless_get_mtu(NM_SETTING_WIRELESS(setting)); + global_property_name = NM_CON_DEFAULT("wifi.mtu"); + } else if (setting_type == NM_TYPE_SETTING_INFINIBAND) { + if (setting) + mtu = nm_setting_infiniband_get_mtu(NM_SETTING_INFINIBAND(setting)); + global_property_name = NM_CON_DEFAULT("infiniband.mtu"); + max_mtu = NM_INFINIBAND_MAX_MTU; + } else if (setting_type == NM_TYPE_SETTING_IP_TUNNEL) { + if (setting) + mtu = nm_setting_ip_tunnel_get_mtu(NM_SETTING_IP_TUNNEL(setting)); + global_property_name = NM_CON_DEFAULT("ip-tunnel.mtu"); + } else if (setting_type == NM_TYPE_SETTING_WIREGUARD) { + if (setting) + mtu = nm_setting_wireguard_get_mtu(NM_SETTING_WIREGUARD(setting)); + global_property_name = NM_CON_DEFAULT("wireguard.mtu"); + } else + g_return_val_if_reached(0); + + if (mtu) { + *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION; + return mtu; + } + + mtu_default = + nm_device_get_configured_mtu_from_connection_default(self, global_property_name, max_mtu); + if (mtu_default >= 0) { + *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION; + return (guint32) mtu_default; + } + + *out_source = NM_DEVICE_MTU_SOURCE_NONE; + return 0; +} + +guint32 +nm_device_get_configured_mtu_for_wired(NMDevice * self, + NMDeviceMtuSource *out_source, + gboolean * out_force) +{ + return nm_device_get_configured_mtu_from_connection(self, NM_TYPE_SETTING_WIRED, out_source); +} + +guint32 +nm_device_get_configured_mtu_wired_parent(NMDevice * self, + NMDeviceMtuSource *out_source, + gboolean * out_force) +{ + guint32 mtu = 0; + guint32 parent_mtu = 0; + int ifindex; + + ifindex = nm_device_parent_get_ifindex(self); + if (ifindex > 0) { + parent_mtu = nm_platform_link_get_mtu(nm_device_get_platform(self), ifindex); + if (parent_mtu >= NM_DEVICE_GET_CLASS(self)->mtu_parent_delta) + parent_mtu -= NM_DEVICE_GET_CLASS(self)->mtu_parent_delta; + else + parent_mtu = 0; + } + + mtu = nm_device_get_configured_mtu_for_wired(self, out_source, NULL); + + if (parent_mtu && mtu > parent_mtu) { + /* Trying to set a MTU that is out of range from configuration: + * fall back to the parent MTU and set force flag so that it + * overrides an MTU with higher priority already configured. + */ + *out_source = NM_DEVICE_MTU_SOURCE_PARENT; + *out_force = TRUE; + return parent_mtu; + } + + if (*out_source != NM_DEVICE_MTU_SOURCE_NONE) { + nm_assert(mtu > 0); + return mtu; + } + + /* Inherit the MTU from parent device, if any */ + if (parent_mtu) { + mtu = parent_mtu; + *out_source = NM_DEVICE_MTU_SOURCE_PARENT; + } + + return mtu; +} + +/*****************************************************************************/ + +static void +_set_mtu(NMDevice *self, guint32 mtu) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->mtu == mtu) + return; + + priv->mtu = mtu; + _notify(self, PROP_MTU); + + if (priv->master) { + /* changing the MTU of a slave, might require the master to reset + * its MTU. Note that the master usually cannot set a MTU larger + * then the slave's. Hence, when the slave increases the MTU, + * master might want to retry setting the MTU. */ + nm_device_commit_mtu(priv->master); + } +} + +static gboolean +set_platform_mtu(NMDevice *self, guint32 mtu) +{ + int r; + + r = nm_platform_link_set_mtu(nm_device_get_platform(self), nm_device_get_ip_ifindex(self), mtu); + return (r != -NME_PL_CANT_SET_MTU); +} + +static void +_commit_mtu(NMDevice *self, const NMIP4Config *config) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceMtuSource source = NM_DEVICE_MTU_SOURCE_NONE; + guint32 ip6_mtu, ip6_mtu_orig; + guint32 mtu_desired, mtu_desired_orig; + guint32 mtu_plat; + struct { + gboolean initialized; + guint32 value; + } ip6_mtu_sysctl = { + 0, + }; + int ifindex; + char sbuf[64], sbuf1[64], sbuf2[64]; + gboolean success = TRUE; + + ifindex = nm_device_get_ip_ifindex(self); + if (ifindex <= 0) + return; + + if (!nm_device_get_applied_connection(self) + || nm_device_sys_iface_state_is_external_or_assume(self)) { + /* we don't tamper with the MTU of disconnected and + * external/assumed devices. */ + return; + } + + { + guint32 mtu = 0; + gboolean force = FALSE; + + /* We take the MTU from various sources: (in order of increasing + * priority) parent link, IP configuration (which contains the + * MTU from DHCP/PPP), connection profile. + * + * We could just compare it with the platform MTU and apply it + * when different, but this would revert at random times manual + * changes done by the user with the MTU from the connection. + * + * Instead, we remember the source of the currently configured + * MTU and apply the new one only when the new source has a + * higher priority, so that we don't set a MTU from same source + * multiple times. An exception to this is for the PARENT + * source, since we need to keep tracking the parent MTU when it + * changes. + * + * The subclass can set the @force argument to TRUE to signal that the + * returned MTU should be applied even if it has a lower priority. This + * is useful when the value from a lower source should + * preempt the one from higher ones. + */ + + if (NM_DEVICE_GET_CLASS(self)->get_configured_mtu) + mtu = NM_DEVICE_GET_CLASS(self)->get_configured_mtu(self, &source, &force); + + if (config && !force && source < NM_DEVICE_MTU_SOURCE_IP_CONFIG + && nm_ip4_config_get_mtu(config)) { + mtu = nm_ip4_config_get_mtu(config); + source = NM_DEVICE_MTU_SOURCE_IP_CONFIG; + } + + if (mtu != 0) { + _LOGT(LOGD_DEVICE, + "mtu: value %u from source '%s' (%u), current source '%s' (%u)%s", + (guint) mtu, + mtu_source_to_str(source), + (guint) source, + mtu_source_to_str(priv->mtu_source), + (guint) priv->mtu_source, + force ? " (forced)" : ""); + } + + if (mtu != 0 + && (force || source > priv->mtu_source + || (priv->mtu_source == NM_DEVICE_MTU_SOURCE_PARENT && source == priv->mtu_source))) + mtu_desired = mtu; + else { + mtu_desired = 0; + source = NM_DEVICE_MTU_SOURCE_NONE; + } + } + + if (mtu_desired && mtu_desired < 1280) { + NMSettingIPConfig *s_ip6; + + s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG); + if (s_ip6 + && !NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip6), + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) { + /* the interface has IPv6 enabled. The MTU with IPv6 cannot be smaller + * then 1280. + * + * For slave-devices (that don't have @s_ip6 we) don't do this fixup because + * it's anyway an unsolved problem when the slave configures a conflicting + * MTU. */ + mtu_desired = 1280; + } + } + + ip6_mtu = priv->ip6_mtu; + if (!ip6_mtu && priv->mtu_source == NM_DEVICE_MTU_SOURCE_NONE) { + /* initially, if the IPv6 MTU is not specified, grow it as large as the + * link MTU @mtu_desired. Only exception is, if @mtu_desired is so small + * to disable IPv6. */ + if (mtu_desired >= 1280) + ip6_mtu = mtu_desired; + } + + if (!ip6_mtu && !mtu_desired) + return; + + mtu_desired_orig = mtu_desired; + ip6_mtu_orig = ip6_mtu; + + mtu_plat = nm_platform_link_get_mtu(nm_device_get_platform(self), ifindex); + + if (ip6_mtu) { + ip6_mtu = NM_MAX(1280, ip6_mtu); + + if (!mtu_desired) + mtu_desired = mtu_plat; + + if (mtu_desired) { + mtu_desired = NM_MAX(1280, mtu_desired); + + if (mtu_desired < ip6_mtu) + ip6_mtu = mtu_desired; + } + } + + if (mtu_desired && NM_DEVICE_GET_CLASS(self)->mtu_force_set && !priv->mtu_force_set_done) { + priv->mtu_force_set_done = TRUE; + + if (mtu_desired == mtu_plat) { + mtu_plat--; + if (NM_DEVICE_GET_CLASS(self)->set_platform_mtu(self, mtu_desired - 1)) { + _LOGD(LOGD_DEVICE, "mtu: force-set MTU to %u", mtu_desired - 1); + } else + _LOGW(LOGD_DEVICE, "mtu: failure to force-set MTU to %u", mtu_desired - 1); + } + } + + _LOGT(LOGD_DEVICE, + "mtu: device-mtu: %u%s, ipv6-mtu: %u%s, ifindex: %d", + (guint) mtu_desired, + mtu_desired == mtu_desired_orig + ? "" + : nm_sprintf_buf(sbuf1, " (was %u)", (guint) mtu_desired_orig), + (guint) ip6_mtu, + ip6_mtu == ip6_mtu_orig ? "" : nm_sprintf_buf(sbuf2, " (was %u)", (guint) ip6_mtu_orig), + ifindex); + +#define _IP6_MTU_SYS() \ + ({ \ + if (!ip6_mtu_sysctl.initialized) { \ + ip6_mtu_sysctl.value = nm_device_sysctl_ip_conf_get_int_checked(self, \ + AF_INET6, \ + "mtu", \ + 10, \ + 0, \ + G_MAXUINT32, \ + 0); \ + ip6_mtu_sysctl.initialized = TRUE; \ + } \ + ip6_mtu_sysctl.value; \ + }) + if ((mtu_desired && mtu_desired != mtu_plat) || (ip6_mtu && ip6_mtu != _IP6_MTU_SYS())) { + gboolean anticipated_failure = FALSE; + + if (!priv->mtu_initial && !priv->ip6_mtu_initial) { + /* before touching any of the MTU parameters, record the + * original setting to restore on deactivation. */ + priv->mtu_initial = mtu_plat; + priv->ip6_mtu_initial = _IP6_MTU_SYS(); + } + + if (mtu_desired && mtu_desired != mtu_plat) { + if (!NM_DEVICE_GET_CLASS(self)->set_platform_mtu(self, mtu_desired)) { + anticipated_failure = TRUE; + success = FALSE; + _LOGW(LOGD_DEVICE, + "mtu: failure to set MTU. %s", + NM_IS_DEVICE_VLAN(self) + ? "Is the parent's MTU size large enough?" + : (!c_list_is_empty(&priv->slaves) + ? "Are the MTU sizes of the slaves large enough?" + : "Did you configure the MTU correctly?")); + } + priv->carrier_wait_until_ms = + nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS; + } + + if (ip6_mtu && ip6_mtu != _IP6_MTU_SYS()) { + if (!nm_device_sysctl_ip_conf_set(self, + AF_INET6, + "mtu", + nm_sprintf_buf(sbuf, "%u", (unsigned) ip6_mtu))) { + int errsv = errno; + NMLogLevel level = LOGL_WARN; + const char *msg = NULL; + + success = FALSE; + + if (anticipated_failure && errsv == EINVAL) { + level = LOGL_DEBUG; + msg = "Is the underlying MTU value successfully set?"; + } else if (!g_file_test("/proc/sys/net/ipv6", G_FILE_TEST_IS_DIR)) { + level = LOGL_DEBUG; + msg = "IPv6 is disabled"; + success = TRUE; + } + + _NMLOG(level, + LOGD_DEVICE, + "mtu: failure to set IPv6 MTU%s%s", + msg ? ": " : "", + msg ?: ""); + } + priv->carrier_wait_until_ms = + nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS; + } + } + + if (success && source != NM_DEVICE_MTU_SOURCE_NONE) + priv->mtu_source = source; + +#undef _IP6_MTU_SYS +} + +void +nm_device_commit_mtu(NMDevice *self) +{ + NMDeviceState state; + + g_return_if_fail(NM_IS_DEVICE(self)); + + state = nm_device_get_state(self); + if (state >= NM_DEVICE_STATE_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) { + _LOGT(LOGD_DEVICE, "mtu: commit-mtu..."); + _commit_mtu(self, NM_DEVICE_GET_PRIVATE(self)->ip_config_4); + } else + _LOGT(LOGD_DEVICE, + "mtu: commit-mtu... skip due to state %s", + nm_device_state_to_str(state)); +} + +static void +ndisc_config_changed(NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, NMDevice *self) +{ + NMNDiscConfigMap changed = changed_int; + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + guint i; + + g_return_if_fail(priv->act_request.obj); + + if (!applied_config_get_current(&priv->ac_ip6_config)) + applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6); + + if (changed & NM_NDISC_CONFIG_ADDRESSES) { + guint8 plen; + guint32 ifa_flags; + + /* Check, whether kernel is recent enough to help user space handling RA. + * If it's not supported, we have no ipv6-privacy and must add autoconf + * addresses as /128. The reason for the /128 is to prevent the kernel + * from adding a prefix route for this address. */ + ifa_flags = 0; + if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)) { + ifa_flags |= IFA_F_NOPREFIXROUTE; + if (NM_IN_SET(priv->ndisc_use_tempaddr, + NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR, + NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR)) + ifa_flags |= IFA_F_MANAGETEMPADDR; + plen = 64; + } else + plen = 128; + + nm_ip6_config_reset_addresses_ndisc((NMIP6Config *) priv->ac_ip6_config.orig, + rdata->addresses, + rdata->addresses_n, + plen, + ifa_flags); + if (priv->ac_ip6_config.current) { + nm_ip6_config_reset_addresses_ndisc((NMIP6Config *) priv->ac_ip6_config.current, + rdata->addresses, + rdata->addresses_n, + plen, + ifa_flags); + } + } + + if (NM_FLAGS_ANY(changed, NM_NDISC_CONFIG_ROUTES | NM_NDISC_CONFIG_GATEWAYS)) { + nm_ip6_config_reset_routes_ndisc( + (NMIP6Config *) priv->ac_ip6_config.orig, + rdata->gateways, + rdata->gateways_n, + rdata->routes, + rdata->routes_n, + nm_device_get_route_table(self, AF_INET6), + nm_device_get_route_metric(self, AF_INET6), + nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF)); + if (priv->ac_ip6_config.current) { + nm_ip6_config_reset_routes_ndisc( + (NMIP6Config *) priv->ac_ip6_config.current, + rdata->gateways, + rdata->gateways_n, + rdata->routes, + rdata->routes_n, + nm_device_get_route_table(self, AF_INET6), + nm_device_get_route_metric(self, AF_INET6), + nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF)); + } + } + + if (changed & NM_NDISC_CONFIG_DNS_SERVERS) { + /* Rebuild DNS server list from neighbor discovery cache. */ + applied_config_reset_nameservers(&priv->ac_ip6_config); + + for (i = 0; i < rdata->dns_servers_n; i++) + applied_config_add_nameserver(&priv->ac_ip6_config, + (const NMIPAddr *) &rdata->dns_servers[i].address); + } + + if (changed & NM_NDISC_CONFIG_DNS_DOMAINS) { + /* Rebuild domain list from neighbor discovery cache. */ + applied_config_reset_searches(&priv->ac_ip6_config); + + for (i = 0; i < rdata->dns_domains_n; i++) + applied_config_add_search(&priv->ac_ip6_config, rdata->dns_domains[i].domain); + } + + if (changed & NM_NDISC_CONFIG_DHCP_LEVEL) { + dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, TRUE); + + priv->dhcp6.mode = rdata->dhcp_level; + if (priv->dhcp6.mode != NM_NDISC_DHCP_LEVEL_NONE) { + _LOGD(LOGD_DEVICE | LOGD_DHCP6, + "Activation: Stage 3 of 5 (IP Configure Start) starting DHCPv6" + " as requested by IPv6 router..."); + if (!dhcp6_start(self, FALSE)) { + if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED) { + nm_device_state_changed(self, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_DHCP_START_FAILED); + return; + } + } + } + } + + if (changed & NM_NDISC_CONFIG_HOP_LIMIT) + nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe(nm_device_get_platform(self), + nm_device_get_ip_iface(self), + rdata->hop_limit); + + if (changed & NM_NDISC_CONFIG_REACHABLE_TIME) { + nm_platform_sysctl_ip_neigh_set_ipv6_reachable_time(nm_device_get_platform(self), + nm_device_get_ip_iface(self), + rdata->reachable_time_ms); + } + + if (changed & NM_NDISC_CONFIG_RETRANS_TIMER) { + nm_platform_sysctl_ip_neigh_set_ipv6_retrans_time(nm_device_get_platform(self), + nm_device_get_ip_iface(self), + rdata->retrans_timer_ms); + } + + if (changed & NM_NDISC_CONFIG_MTU) { + if (priv->ip6_mtu != rdata->mtu) { + _LOGD(LOGD_DEVICE, "mtu: set IPv6 MTU to %u", (guint) rdata->mtu); + priv->ip6_mtu = rdata->mtu; + } + } + + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); +} + +static void +ndisc_ra_timeout(NMNDisc *ndisc, NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + /* We don't want to stop listening for router advertisements completely, + * but instead let device activation continue activating. If an RA + * shows up later, we'll use it as long as the device is not disconnected. + */ + + _LOGD(LOGD_IP6, "timed out waiting for IPv6 router advertisement"); + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) { + /* If RA is our only source of addressing information and we don't + * ever receive one, then time out IPv6. But if there is other + * IPv6 configuration, like manual IPv6 addresses or external IPv6 + * config, consider that sufficient for IPv6 success. + * + * FIXME: it doesn't seem correct to determine this based on which + * addresses we find inside priv->ip_config_6. + */ + if (priv->ip_config_6 + && nm_ip6_config_find_first_address(priv->ip_config_6, + NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL + | NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY)) + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + else + nm_device_activate_schedule_ip_config_timeout(self, AF_INET6); + } +} + +static void +addrconf6_start_with_link_ready(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMUtilsIPv6IfaceId iid; + + g_assert(priv->ndisc); + + if (nm_device_get_ip_iface_identifier(self, &iid, FALSE)) { + _LOGD(LOGD_IP6, "addrconf6: using the device EUI-64 identifier"); + nm_ndisc_set_iid(priv->ndisc, iid); + } else { + /* Don't abort the addrconf at this point -- if ndisc needs the iid + * it will notice this itself. */ + _LOGI(LOGD_IP6, "addrconf6: no interface identifier; IPv6 address creation may fail"); + } + + /* Apply any manual configuration before starting RA */ + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "failed to apply manual IPv6 configuration"); + + if (nm_ndisc_get_node_type(priv->ndisc) == NM_NDISC_NODE_TYPE_ROUTER) { + nm_device_sysctl_ip_conf_set(self, AF_INET6, "forwarding", "1"); + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + priv->needs_ip6_subnet = TRUE; + g_signal_emit(self, signals[IP6_SUBNET_NEEDED], 0); + } + + priv->ndisc_changed_id = g_signal_connect(priv->ndisc, + NM_NDISC_CONFIG_RECEIVED, + G_CALLBACK(ndisc_config_changed), + self); + priv->ndisc_timeout_id = g_signal_connect(priv->ndisc, + NM_NDISC_RA_TIMEOUT_SIGNAL, + G_CALLBACK(ndisc_ra_timeout), + self); + + ndisc_set_router_config(priv->ndisc, self); + nm_ndisc_start(priv->ndisc); + priv->ndisc_started = TRUE; + return; +} + +static gboolean +addrconf6_start(NMDevice *self, NMSettingIP6ConfigPrivacy use_tempaddr) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + NMSettingIP6Config *s_ip6 = NULL; + GError * error = NULL; + NMUtilsStableType stable_type; + const char * stable_id; + NMNDiscNodeType node_type; + int max_addresses; + int router_solicitations; + int router_solicitation_interval; + guint32 ra_timeout; + guint32 default_ra_timeout; + + connection = nm_device_get_applied_connection(self); + g_assert(connection); + + nm_assert(!applied_config_get_current(&priv->ac_ip6_config)); + applied_config_clear(&priv->ac_ip6_config); + + nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref); + nm_clear_g_source(&priv->rt6_temporary_not_available_id); + + s_ip6 = NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection)); + g_assert(s_ip6); + + if (nm_streq(nm_device_get_effective_ip_config_method(self, AF_INET6), + NM_SETTING_IP4_CONFIG_METHOD_SHARED)) + node_type = NM_NDISC_NODE_TYPE_ROUTER; + else + node_type = NM_NDISC_NODE_TYPE_HOST; + + nm_lndp_ndisc_get_sysctl(nm_device_get_platform(self), + nm_device_get_ip_iface(self), + &max_addresses, + &router_solicitations, + &router_solicitation_interval, + &default_ra_timeout); + + if (node_type == NM_NDISC_NODE_TYPE_ROUTER) + ra_timeout = 0u; + else { + ra_timeout = _prop_get_ipv6_ra_timeout(self); + if (ra_timeout == 0u) + ra_timeout = default_ra_timeout; + } + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + priv->ndisc = nm_lndp_ndisc_new(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + nm_device_get_ip_iface(self), + stable_type, + stable_id, + nm_setting_ip6_config_get_addr_gen_mode(s_ip6), + node_type, + max_addresses, + router_solicitations, + router_solicitation_interval, + ra_timeout, + &error); + if (!priv->ndisc) { + _LOGE(LOGD_IP6, "addrconf6: failed to start neighbor discovery: %s", error->message); + g_error_free(error); + return FALSE; + } + + priv->ndisc_use_tempaddr = use_tempaddr; + + if (NM_IN_SET(use_tempaddr, + NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR, + NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR) + && !nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)) { + _LOGW(LOGD_IP6, + "The kernel does not support extended IFA_FLAGS needed by NM for " + "IPv6 private addresses. This feature is not available"); + } + + /* ensure link local is ready... */ + if (!linklocal6_start(self)) { + /* wait for the LL address to show up */ + return TRUE; + } + + /* already have the LL address; kick off neighbor discovery */ + addrconf6_start_with_link_ready(self); + return TRUE; +} + +static void +addrconf6_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->ndisc_started = FALSE; + nm_clear_g_signal_handler(priv->ndisc, &priv->ndisc_changed_id); + nm_clear_g_signal_handler(priv->ndisc, &priv->ndisc_timeout_id); + + applied_config_clear(&priv->ac_ip6_config); + nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref); + nm_clear_g_source(&priv->rt6_temporary_not_available_id); + if (priv->ndisc) { + nm_ndisc_stop(priv->ndisc); + g_clear_object(&priv->ndisc); + } +} + +/*****************************************************************************/ + +static void +save_ip6_properties(NMDevice *self) +{ + static const char *const ip6_properties_to_save[] = { + "accept_ra", + "forwarding", + "disable_ipv6", + "hop_limit", + "use_tempaddr", + }; + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatform * platform = nm_device_get_platform(self); + const char * ifname; + char * value; + int i; + + g_hash_table_remove_all(priv->ip6_saved_properties); + + ifname = nm_device_get_ip_iface_from_platform(self); + if (!ifname) + return; + + for (i = 0; i < G_N_ELEMENTS(ip6_properties_to_save); i++) { + value = + nm_platform_sysctl_ip_conf_get(platform, AF_INET6, ifname, ip6_properties_to_save[i]); + if (value) { + g_hash_table_insert(priv->ip6_saved_properties, + (char *) ip6_properties_to_save[i], + value); + } + } +} + +static void +restore_ip6_properties(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, priv->ip6_saved_properties); + while (g_hash_table_iter_next(&iter, &key, &value)) { + /* Don't touch "disable_ipv6" if we're doing userland IPv6LL */ + if (priv->ipv6ll_handle && nm_streq(key, "disable_ipv6")) + continue; + nm_device_sysctl_ip_conf_set(self, AF_INET6, key, value); + } +} + +static void +set_disable_ipv6(NMDevice *self, const char *value) +{ + /* We only touch disable_ipv6 when NM is not managing the IPv6LL address */ + if (!NM_DEVICE_GET_PRIVATE(self)->ipv6ll_handle) + nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", value); +} + +static void +set_nm_ipv6ll(NMDevice *self, gboolean enable) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + int ifindex = nm_device_get_ip_ifindex(self); + + if (!nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) + return; + + priv->ipv6ll_handle = enable; + if (ifindex > 0) { + const char *detail = enable ? "enable" : "disable"; + int r; + + _LOGD(LOGD_IP6, "will %s userland IPv6LL", detail); + r = nm_platform_link_set_user_ipv6ll_enabled(nm_device_get_platform(self), ifindex, enable); + if (r < 0) { + _NMLOG(NM_IN_SET(r, -NME_PL_NOT_FOUND, -NME_PL_OPNOTSUPP) ? LOGL_DEBUG : LOGL_WARN, + LOGD_IP6, + "failed to %s userspace IPv6LL address handling (%s)", + detail, + nm_strerror(r)); + } + + if (enable) { + gs_free char *value = NULL; + + /* Bounce IPv6 to ensure the kernel stops IPv6LL address generation */ + value = nm_device_sysctl_ip_conf_get(self, AF_INET6, "disable_ipv6"); + if (nm_streq0(value, "0")) + nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1"); + + /* Ensure IPv6 is enabled */ + nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "0"); + } + } +} + +/*****************************************************************************/ + +static gboolean +ip_requires_slaves(NMDevice *self, int addr_family) +{ + const char *method; + + method = nm_device_get_effective_ip_config_method(self, addr_family); + + if (NM_IS_IPv4(addr_family)) + return nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO); + + /* SLAAC, DHCP, and Link-Local depend on connectivity (and thus slaves) + * to complete addressing. SLAAC and DHCP need a peer to provide a prefix. + */ + return NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_DHCP); +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * self, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + NMActStageReturn ret = NM_ACT_STAGE_RETURN_FAILURE; + const char * method; + + nm_assert_addr_family(addr_family); + + connection = nm_device_get_applied_connection(self); + + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + if (connection_ip_method_requires_carrier(connection, addr_family, NULL) + && nm_device_is_master(self) && !priv->carrier) { + _LOGI(LOGD_IP | LOGD_DEVICE, + "IPv%c config waiting until carrier is on", + nm_utils_addr_family_to_char(addr_family)); + return NM_ACT_STAGE_RETURN_IP_WAIT; + } + + if (nm_device_is_master(self) && ip_requires_slaves(self, addr_family)) { + /* If the master has no ready slaves, and depends on slaves for + * a successful IP configuration attempt, then postpone IP addressing. + */ + if (!have_any_ready_slaves(self)) { + _LOGI(LOGD_DEVICE | LOGD_IP, + "IPv%c config waiting until slaves are ready", + nm_utils_addr_family_to_char(addr_family)); + return NM_ACT_STAGE_RETURN_IP_WAIT; + } + } + + if (!IS_IPv4) + priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_NONE; + + method = nm_device_get_effective_ip_config_method(self, addr_family); + + _LOGD(LOGD_IP | LOGD_DEVICE, + "IPv%c config method is %s", + nm_utils_addr_family_to_char(addr_family), + method); + + if (IS_IPv4) { + if (NM_IN_STRSET(method, + NM_SETTING_IP4_CONFIG_METHOD_AUTO, + NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) { + NMSettingIPConfig *s_ip4; + NMIP4Config ** configs, *config; + guint num_addresses; + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + g_return_val_if_fail(s_ip4, NM_ACT_STAGE_RETURN_FAILURE); + num_addresses = nm_setting_ip_config_get_num_addresses(s_ip4); + + if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) { + ret = dhcp4_start(self); + if (ret == NM_ACT_STAGE_RETURN_FAILURE) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_DHCP_START_FAILED); + return ret; + } + } else { + g_return_val_if_fail(num_addresses != 0, NM_ACT_STAGE_RETURN_FAILURE); + ret = NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (num_addresses) { + config = nm_device_ip4_config_new(self); + nm_ip4_config_merge_setting(config, + nm_connection_get_setting_ip4_config(connection), + NM_SETTING_CONNECTION_MDNS_DEFAULT, + NM_SETTING_CONNECTION_LLMNR_DEFAULT, + nm_device_get_route_table(self, AF_INET), + nm_device_get_route_metric(self, AF_INET)); + configs = g_new0(NMIP4Config *, 2); + configs[0] = config; + ipv4_dad_start(self, configs, ipv4_manual_method_apply); + } + } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)) { + ret = ipv4ll_start(self); + if (ret == NM_ACT_STAGE_RETURN_FAILURE) + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED); + } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED)) { + if (out_config) { + *out_config = shared4_new_config(self, connection); + if (*out_config) { + priv->dnsmasq_manager = nm_dnsmasq_manager_new(nm_device_get_ip_iface(self)); + ret = NM_ACT_STAGE_RETURN_SUCCESS; + } else { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + ret = NM_ACT_STAGE_RETURN_FAILURE; + } + } else + g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE); + } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) + ret = NM_ACT_STAGE_RETURN_SUCCESS; + else + _LOGW(LOGD_IP4, "unhandled IPv4 config method '%s'; will fail", method); + + return ret; + } else { + NMSettingIP6ConfigPrivacy ip6_privacy = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN; + const char * ip6_privacy_str = "0"; + NMPlatform * platform; + int ifindex; + + if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) { + nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1"); + return NM_ACT_STAGE_RETURN_IP_DONE; + } + + if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) { + if (!nm_device_sys_iface_state_is_external(self)) { + if (priv->master) { + /* If a device only has an IPv6 link-local address, + * we don't generate an assumed connection. Therefore, + * when a new slave connection (without IP configuration) + * is activated on the device, the link-local address + * remains configured. The IP configuration of an activated + * slave should not depend on the previous state. Flush + * addresses and routes on activation. + */ + ifindex = nm_device_get_ip_ifindex(self); + platform = nm_device_get_platform(self); + + if (ifindex > 0) { + gs_unref_object NMIP6Config *config = nm_device_ip6_config_new(self); + + nm_platform_ip_route_flush(platform, AF_INET6, ifindex); + nm_platform_ip_address_flush(platform, AF_INET6, ifindex); + nm_device_set_ip_config(self, AF_INET6, (NMIPConfig *) config, FALSE, NULL); + } + } else { + gboolean ipv6ll_handle_old = priv->ipv6ll_handle; + + /* When activating an IPv6 'ignore' connection we need to revert back + * to kernel IPv6LL, but the kernel won't actually assign an address + * to the interface until disable_ipv6 is bounced. + */ + set_nm_ipv6ll(self, FALSE); + if (ipv6ll_handle_old) + nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1"); + restore_ip6_properties(self); + } + } + return NM_ACT_STAGE_RETURN_IP_DONE; + } + + /* Ensure the MTU makes sense. If it was below 1280 the kernel would not + * expose any ipv6 sysctls or allow presence of any addresses on the interface, + * including LL, which * would make it impossible to autoconfigure MTU to a + * correct value. */ + _commit_mtu(self, priv->ip_config_4); + + /* Any method past this point requires an IPv6LL address. Use NM-controlled + * IPv6LL if this is not an assumed connection, since assumed connections + * will already have IPv6 set up. + */ + if (!nm_device_sys_iface_state_is_external_or_assume(self)) + set_nm_ipv6ll(self, TRUE); + + /* Re-enable IPv6 on the interface */ + nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "0"); + set_disable_ipv6(self, "0"); + + /* Synchronize external IPv6 configuration with kernel, since + * linklocal6_start() uses the information there to determine if we can + * proceed with the selected method (SLAAC, DHCP, link-local). + */ + nm_platform_process_events(nm_device_get_platform(self)); + g_clear_object(&priv->ext_ip6_config_captured); + priv->ext_ip6_config_captured = + nm_ip6_config_capture(nm_device_get_multi_index(self), + nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + + ip6_privacy = _prop_get_ipv6_ip6_privacy(self); + + if (NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_SHARED)) { + if (!addrconf6_start(self, ip6_privacy)) { + /* IPv6 might be disabled; allow IPv4 to proceed */ + ret = NM_ACT_STAGE_RETURN_IP_FAIL; + } else + ret = NM_ACT_STAGE_RETURN_POSTPONE; + } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)) { + ret = + linklocal6_start(self) ? NM_ACT_STAGE_RETURN_SUCCESS : NM_ACT_STAGE_RETURN_POSTPONE; + } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DHCP)) { + priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_MANAGED; + if (!dhcp6_start(self, TRUE)) { + /* IPv6 might be disabled; allow IPv4 to proceed */ + ret = NM_ACT_STAGE_RETURN_IP_FAIL; + } else + ret = NM_ACT_STAGE_RETURN_POSTPONE; + } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_MANUAL)) + ret = NM_ACT_STAGE_RETURN_SUCCESS; + else + _LOGW(LOGD_IP6, "unhandled IPv6 config method '%s'; will fail", method); + + if (ret != NM_ACT_STAGE_RETURN_FAILURE + && !nm_device_sys_iface_state_is_external_or_assume(self)) { + switch (ip6_privacy) { + case NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN: + case NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED: + ip6_privacy_str = "0"; + break; + case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR: + ip6_privacy_str = "1"; + break; + case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR: + ip6_privacy_str = "2"; + break; + } + nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", ip6_privacy_str); + } + + return ret; + } +} + +gboolean +nm_device_activate_stage3_ip_start(NMDevice *self, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMActStageReturn ret; + NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE; + gs_unref_object NMIPConfig *ip_config = NULL; + + g_assert(priv->ip_state_x[IS_IPv4] == NM_DEVICE_IP_STATE_WAIT); + + if (nm_device_sys_iface_state_is_external(self)) { + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE); + check_ip_state(self, FALSE, TRUE); + return TRUE; + } + + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_CONF); + + ret = NM_DEVICE_GET_CLASS(self)->act_stage3_ip_config_start(self, + addr_family, + (gpointer *) &ip_config, + &failure_reason); + + switch (ret) { + case NM_ACT_STAGE_RETURN_SUCCESS: + if (!IS_IPv4) { + /* Here we get a static IPv6 config, like for Shared where it's + * autogenerated or from modems where it comes from ModemManager. + */ + if (!ip_config) + ip_config = nm_device_ip_config_new(self, addr_family); + nm_assert(!applied_config_get_current(&priv->ac_ip6_config)); + applied_config_init(&priv->ac_ip6_config, ip_config); + ip_config = NULL; + } + nm_device_activate_schedule_ip_config_result(self, addr_family, ip_config); + break; + case NM_ACT_STAGE_RETURN_IP_DONE: + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE); + check_ip_state(self, FALSE, TRUE); + break; + case NM_ACT_STAGE_RETURN_FAILURE: + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason); + return FALSE; + case NM_ACT_STAGE_RETURN_IP_FAIL: + /* Activation not wanted */ + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL); + break; + case NM_ACT_STAGE_RETURN_IP_WAIT: + /* Wait for something to try IP config again */ + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_WAIT); + break; + default: + g_assert(ret == NM_ACT_STAGE_RETURN_POSTPONE); + } + + return TRUE; +} + +/* + * activate_stage3_ip_config_start + * + * Begin automatic/manual IP configuration + * + */ +static void +activate_stage3_ip_config_start(NMDevice *self) +{ + int ifindex; + + _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_WAIT); + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_WAIT); + + _active_connection_set_state_flags(self, NM_ACTIVATION_STATE_FLAG_LAYER2_READY); + + nm_device_state_changed(self, NM_DEVICE_STATE_IP_CONFIG, NM_DEVICE_STATE_REASON_NONE); + + /* Device should be up before we can do anything with it */ + if ((ifindex = nm_device_get_ip_ifindex(self)) > 0 + && !nm_platform_link_is_up(nm_device_get_platform(self), ifindex)) + _LOGW(LOGD_DEVICE, + "interface %s not up for IP configuration", + nm_device_get_ip_iface(self)); + + if (nm_device_activate_ip4_state_in_wait(self) + && !nm_device_activate_stage3_ip_start(self, AF_INET)) + return; + + if (nm_device_activate_ip6_state_in_wait(self) + && !nm_device_activate_stage3_ip_start(self, AF_INET6)) + return; + + /* Proxy */ + nm_device_set_proxy_config(self, NULL); + + check_ip_state(self, TRUE, TRUE); +} + +static void +fw_change_zone_cb(NMFirewallManager * firewall_manager, + NMFirewallManagerCallId *call_id, + GError * error, + gpointer user_data) +{ + NMDevice * self = user_data; + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->fw_call != call_id) + g_return_if_reached(); + + priv->fw_call = NULL; + + if (nm_utils_error_is_cancelled(error)) + return; + + switch (priv->fw_state) { + case FIREWALL_STATE_WAIT_STAGE_3: + priv->fw_state = FIREWALL_STATE_INITIALIZED; + nm_device_activate_schedule_stage3_ip_config_start(self); + break; + case FIREWALL_STATE_WAIT_IP_CONFIG: + priv->fw_state = FIREWALL_STATE_INITIALIZED; + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE + || priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) + nm_device_start_ip_check(self); + break; + case FIREWALL_STATE_INITIALIZED: + break; + default: + g_return_if_reached(); + } +} + +static void +fw_change_zone(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * applied_connection; + NMSettingConnection *s_con; + const char * zone; + + nm_assert(priv->fw_state >= FIREWALL_STATE_INITIALIZED); + + applied_connection = nm_device_get_applied_connection(self); + nm_assert(applied_connection); + + s_con = nm_connection_get_setting_connection(applied_connection); + nm_assert(s_con); + + if (priv->fw_call) { + nm_firewall_manager_cancel_call(priv->fw_call); + nm_assert(!priv->fw_call); + } + + if (G_UNLIKELY(!priv->fw_mgr)) + priv->fw_mgr = g_object_ref(nm_firewall_manager_get()); + + zone = nm_setting_connection_get_zone(s_con); +#if WITH_FIREWALLD_ZONE + if (!zone || zone[0] == '\0') { + if (nm_streq0(nm_device_get_effective_ip_config_method(self, AF_INET), + NM_SETTING_IP4_CONFIG_METHOD_SHARED) + || nm_streq0(nm_device_get_effective_ip_config_method(self, AF_INET6), + NM_SETTING_IP6_CONFIG_METHOD_SHARED)) + zone = "nm-shared"; + } +#endif + priv->fw_call = nm_firewall_manager_add_or_change_zone(priv->fw_mgr, + nm_device_get_ip_iface(self), + zone, + FALSE, /* change zone */ + fw_change_zone_cb, + self); +} + +/* + * nm_device_activate_schedule_stage3_ip_config_start + * + * Schedule IP configuration start + */ +void +nm_device_activate_schedule_stage3_ip_config_start(NMDevice *self) +{ + NMDevicePrivate *priv; + int ifindex; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + g_return_if_fail(priv->act_request.obj); + ifindex = nm_device_get_ip_ifindex(self); + + /* Add the interface to the specified firewall zone */ + if (priv->fw_state == FIREWALL_STATE_UNMANAGED) { + if (nm_device_sys_iface_state_is_external(self)) { + /* fake success */ + priv->fw_state = FIREWALL_STATE_INITIALIZED; + } else if (ifindex > 0) { + priv->fw_state = FIREWALL_STATE_WAIT_STAGE_3; + fw_change_zone(self); + return; + } + /* no ifindex, nothing to do for now */ + } else if (priv->fw_state == FIREWALL_STATE_WAIT_STAGE_3) { + /* a firewall call for stage3 is pending. Return and wait. */ + return; + } + + nm_assert(ifindex <= 0 || priv->fw_state == FIREWALL_STATE_INITIALIZED); + + activation_source_schedule(self, activate_stage3_ip_config_start, AF_INET); +} + +static NMActStageReturn +act_stage4_ip_config_timeout(NMDevice * self, + int addr_family, + NMDeviceStateReason *out_failure_reason) +{ + nm_assert_addr_family(addr_family); + + if (!get_ip_config_may_fail(self, addr_family)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +activate_stage4_ip_config_timeout_x(NMDevice *self, int addr_family) +{ + NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE; + NMActStageReturn ret; + + ret = + NM_DEVICE_GET_CLASS(self)->act_stage4_ip_config_timeout(self, addr_family, &failure_reason); + + if (ret == NM_ACT_STAGE_RETURN_POSTPONE) + return; + + if (ret == NM_ACT_STAGE_RETURN_FAILURE) { + nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason); + return; + } + g_assert(ret == NM_ACT_STAGE_RETURN_SUCCESS); + + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL); + check_ip_state(self, FALSE, TRUE); +} + +static void +activate_stage4_ip_config_timeout_4(NMDevice *self) +{ + activate_stage4_ip_config_timeout_x(self, AF_INET); +} + +static void +activate_stage4_ip_config_timeout_6(NMDevice *self) +{ + activate_stage4_ip_config_timeout_x(self, AF_INET6); +} + +void +nm_device_activate_schedule_ip_config_timeout(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_if_fail(priv->act_request.obj); + + activation_source_schedule(self, + IS_IPv4 ? activate_stage4_ip_config_timeout_4 + : activate_stage4_ip_config_timeout_6, + addr_family); +} + +static gboolean +share_init(NMDevice *self, GError **error) +{ + const char *const modules[] = {"ip_tables", + "iptable_nat", + "nf_nat_ftp", + "nf_nat_irc", + "nf_nat_sip", + "nf_nat_tftp", + "nf_nat_pptp", + "nf_nat_h323"}; + guint i; + int errsv; + + if (nm_platform_sysctl_get_int32(nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"), + -1) + == 1) { + /* nothing to do. */ + } else if (!nm_platform_sysctl_set(nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"), + "1")) { + errsv = errno; + _LOGD(LOGD_SHARING, + "share: error enabling IPv4 forwarding: (%d) %s", + errsv, + nm_strerror_native(errsv)); + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "cannot set ipv4/ip_forward: %s", + nm_strerror_native(errsv)); + return FALSE; + } + + if (nm_platform_sysctl_get_int32(nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_dynaddr"), + -1) + == 1) { + /* nothing to do. */ + } else if (!nm_platform_sysctl_set(nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_dynaddr"), + "1")) { + errsv = errno; + _LOGD(LOGD_SHARING, + "share: error enabling dynamic addresses: (%d) %s", + errsv, + nm_strerror_native(errsv)); + } + + for (i = 0; i < G_N_ELEMENTS(modules); i++) + nm_utils_modprobe(NULL, FALSE, modules[i], NULL); + + return TRUE; +} + +static gboolean +start_sharing(NMDevice *self, NMIP4Config *config, GError **error) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMActRequest * req; + const NMPlatformIP4Address *ip4_addr = NULL; + const char * ip_iface; + GError * local = NULL; + NMConnection * conn; + NMSettingConnection * s_con; + gboolean announce_android_metered; + NMUtilsShareRules * share_rules; + + g_return_val_if_fail(config, FALSE); + + ip_iface = nm_device_get_ip_iface(self); + if (!ip_iface) { + g_set_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "device has no ip interface"); + return FALSE; + } + + ip4_addr = nm_ip4_config_get_first_address(config); + if (!ip4_addr || !ip4_addr->address) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "could not determine IPv4 address"); + return FALSE; + } + + if (!share_init(self, error)) + return FALSE; + + req = nm_device_get_act_request(self); + g_return_val_if_fail(req, FALSE); + + share_rules = nm_utils_share_rules_new(); + + nm_utils_share_rules_add_all_rules(share_rules, ip_iface, ip4_addr->address, ip4_addr->plen); + + nm_utils_share_rules_apply(share_rules, TRUE); + + nm_act_request_set_shared(req, share_rules); + + conn = nm_act_request_get_applied_connection(req); + s_con = nm_connection_get_setting_connection(conn); + + switch (nm_setting_connection_get_metered(s_con)) { + case NM_METERED_YES: + /* honor the metered flag. Note that reapply on the device does not affect + * the metered setting. This is different from other profiles, where the + * metered flag of an activated profile can be changed (reapplied). */ + announce_android_metered = TRUE; + break; + case NM_METERED_UNKNOWN: + /* we pick up the current value and announce it. But again, we cannot update + * the announced setting without restarting dnsmasq. That means, if the default + * route changes w.r.t. being metered, then the shared connection does not get + * updated before reactivating. */ + announce_android_metered = + NM_IN_SET(nm_manager_get_metered(NM_MANAGER_GET), NM_METERED_YES, NM_METERED_GUESS_YES); + break; + default: + announce_android_metered = FALSE; + break; + } + + if (!nm_dnsmasq_manager_start(priv->dnsmasq_manager, + config, + announce_android_metered, + &local)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "could not start dnsmasq due to %s", + local->message); + g_error_free(local); + nm_act_request_set_shared(req, NULL); + return FALSE; + } + + priv->dnsmasq_state_id = g_signal_connect(priv->dnsmasq_manager, + NM_DNS_MASQ_MANAGER_STATE_CHANGED, + G_CALLBACK(dnsmasq_state_changed_cb), + self); + return TRUE; +} + +static void +arp_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_clear_pointer(&priv->acd.announcing, nm_acd_manager_free); +} + +void +nm_device_arp_announce(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + NMSettingIPConfig *s_ip4; + guint num, i; + const guint8 * hw_addr; + size_t hw_addr_len = 0; + + arp_cleanup(self); + + hw_addr = nm_platform_link_get_address(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + &hw_addr_len); + + if (!hw_addr || hw_addr_len != ETH_ALEN) + return; + + /* We only care about manually-configured addresses; DHCP- and autoip-configured + * ones should already have been seen on the network at this point. + */ + connection = nm_device_get_applied_connection(self); + if (!connection) + return; + s_ip4 = nm_connection_get_setting_ip4_config(connection); + if (!s_ip4) + return; + num = nm_setting_ip_config_get_num_addresses(s_ip4); + if (num == 0) + return; + + priv->acd.announcing = + nm_acd_manager_new(nm_device_get_ip_ifindex(self), hw_addr, hw_addr_len, NULL, NULL); + + for (i = 0; i < num; i++) { + NMIPAddress *ip = nm_setting_ip_config_get_address(s_ip4, i); + in_addr_t addr; + + if (inet_pton(AF_INET, nm_ip_address_get_address(ip), &addr) == 1) + nm_acd_manager_add_address(priv->acd.announcing, addr); + else + g_warn_if_reached(); + } + + nm_acd_manager_announce_addresses(priv->acd.announcing); +} + +static void +activate_stage5_ip_config_result_x(NMDevice *self, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMActRequest * req; + const char * method; + int ip_ifindex; + int errsv; + gboolean do_announce = FALSE; + + req = nm_device_get_act_request(self); + g_assert(req); + + /* Interface must be IFF_UP before IP config can be applied */ + ip_ifindex = nm_device_get_ip_ifindex(self); + g_return_if_fail(ip_ifindex); + + if (!nm_platform_link_is_up(nm_device_get_platform(self), ip_ifindex) + && !nm_device_sys_iface_state_is_external_or_assume(self)) { + nm_platform_link_set_up(nm_device_get_platform(self), ip_ifindex, NULL); + if (!nm_platform_link_is_up(nm_device_get_platform(self), ip_ifindex)) + _LOGW(LOGD_DEVICE, + "interface %s not up for IP configuration", + nm_device_get_ip_iface(self)); + } + + if (!ip_config_merge_and_apply(self, addr_family, TRUE)) { + _LOGD(LOGD_DEVICE | LOGD_IPX(IS_IPv4), + "Activation: Stage 5 of 5 (IPv%c Commit) failed", + nm_utils_addr_family_to_char(addr_family)); + nm_device_ip_method_failed(self, addr_family, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + + if (!IS_IPv4) { + if (priv->dhcp6.mode != NM_NDISC_DHCP_LEVEL_NONE + && priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) { + if (applied_config_get_current(&priv->dhcp6.ip6_config)) { + /* If IPv6 wasn't the first IP to complete, and DHCP was used, + * then ensure dispatcher scripts get the DHCP lease information. + */ + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP6_CHANGE, + self, + NULL, + NULL, + NULL, + NULL); + } else { + /* still waiting for first dhcp6 lease. */ + return; + } + } + } + + /* Start IPv4 sharing/IPv6 forwarding if we need it */ + method = nm_device_get_effective_ip_config_method(self, addr_family); + if (IS_IPv4) { + if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED)) { + gs_free_error GError *error = NULL; + + if (!start_sharing(self, priv->ip_config_4, &error)) { + _LOGW(LOGD_SHARING, + "Activation: Stage 5 of 5 (IPv4 Commit) start sharing failed: %s", + error->message); + nm_device_ip_method_failed(self, + AF_INET, + NM_DEVICE_STATE_REASON_SHARED_START_FAILED); + return; + } + } + } else { + if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_SHARED)) { + if (!nm_platform_sysctl_set( + nm_device_get_platform(self), + NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv6/conf/all/forwarding"), + "1")) { + errsv = errno; + _LOGE(LOGD_SHARING, + "share: error enabling IPv6 forwarding: (%d) %s", + errsv, + nm_strerror_native(errsv)); + nm_device_ip_method_failed(self, + AF_INET6, + NM_DEVICE_STATE_REASON_SHARED_START_FAILED); + return; + } + } + } + + if (IS_IPv4) { + if (priv->dhcp_data_4.client) { + gs_free_error GError *error = NULL; + + if (!nm_dhcp_client_accept(priv->dhcp_data_4.client, &error)) { + _LOGW(LOGD_DHCP4, + "Activation: Stage 5 of 5 (IPv4 Commit) error accepting lease: %s", + error->message); + nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_DHCP_ERROR); + return; + } + } + + /* If IPv4 wasn't the first to complete, and DHCP was used, then ensure + * dispatcher scripts get the DHCP lease information. + */ + if (priv->dhcp_data_4.client && nm_device_activate_ip4_state_in_conf(self) + && (nm_device_get_state(self) > NM_DEVICE_STATE_IP_CONFIG)) { + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP4_CHANGE, + self, + NULL, + NULL, + NULL, + NULL); + } + } + + if (!IS_IPv4) { + /* Check if we have to wait for DAD */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF && !priv->dad6_ip6_config) { + if (!priv->carrier && priv->ignore_carrier && get_ip_config_may_fail(self, AF_INET6)) + _LOGI(LOGD_DEVICE | LOGD_IP6, + "IPv6 DAD: carrier missing and ignored, not delaying activation"); + else + priv->dad6_ip6_config = dad6_get_pending_addresses(self); + + if (priv->dad6_ip6_config) { + _LOGD(LOGD_DEVICE | LOGD_IP6, "IPv6 DAD: awaiting termination"); + } else { + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_DONE); + check_ip_state(self, FALSE, TRUE); + } + } + } + + if (IS_IPv4) { + /* Send ARP announcements */ + + if (nm_device_is_master(self)) { + CList * iter; + SlaveInfo *info; + + /* Skip announcement if there are no device enslaved, for two reasons: + * 1) the master has a temporary MAC address until the first slave comes + * 2) announcements are going to be dropped anyway without slaves + */ + do_announce = FALSE; + + c_list_for_each (iter, &priv->slaves) { + info = c_list_entry(iter, SlaveInfo, lst_slave); + if (info->slave_is_enslaved) { + do_announce = TRUE; + break; + } + } + } else + do_announce = TRUE; + + if (do_announce) + nm_device_arp_announce(self); + } + + if (IS_IPv4) { + /* Enter the IP_CHECK state if this is the first method to complete */ + _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_DONE); + check_ip_state(self, FALSE, TRUE); + } +} + +static void +activate_stage5_ip_config_result_4(NMDevice *self) +{ + activate_stage5_ip_config_result_x(self, AF_INET); +} + +static void +activate_stage5_ip_config_result_6(NMDevice *self) +{ + activate_stage5_ip_config_result_x(self, AF_INET6); +} + +#define activate_stage5_ip_config_result_x_fcn(addr_family) \ + (NM_IS_IPv4(addr_family) ? activate_stage5_ip_config_result_4 \ + : activate_stage5_ip_config_result_6) + +void +nm_device_activate_schedule_ip_config_result(NMDevice *self, int addr_family, NMIPConfig *config) +{ + NMDevicePrivate *priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(!config || (IS_IPv4 && nm_ip_config_get_addr_family(config) == AF_INET)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (IS_IPv4) { + applied_config_init(&priv->dev_ip_config_4, config); + } else { + /* If IP had previously failed, move it back to NM_DEVICE_IP_STATE_CONF since we + * clearly now have configuration. + */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL) + _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_CONF); + } + + activation_source_schedule(self, + activate_stage5_ip_config_result_x_fcn(addr_family), + addr_family); +} + +NMDeviceIPState +nm_device_activate_get_ip_state(NMDevice *self, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_IP_STATE_NONE); + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), NM_DEVICE_IP_STATE_NONE); + + return NM_DEVICE_GET_PRIVATE(self)->ip_state_x[IS_IPv4]; +} + +static void +dad6_add_pending_address(NMDevice * self, + NMPlatform * platform, + int ifindex, + const struct in6_addr *address, + NMIP6Config ** dad6_config) +{ + const NMPlatformIP6Address *pl_addr; + + pl_addr = nm_platform_ip6_address_get(platform, ifindex, address); + if (pl_addr && NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_TENTATIVE) + && !NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_DADFAILED) + && !NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_OPTIMISTIC)) { + _LOGt(LOGD_DEVICE, + "IPv6 DAD: pending address %s", + nm_platform_ip6_address_to_string(pl_addr, NULL, 0)); + + if (!*dad6_config) + *dad6_config = nm_device_ip6_config_new(self); + + nm_ip6_config_add_address(*dad6_config, pl_addr); + } +} + +/* + * Returns a NMIP6Config containing NM-configured addresses which + * have the tentative flag, or NULL if none is present. + */ +static NMIP6Config * +dad6_get_pending_addresses(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMIP6Config * confs[] = {(NMIP6Config *) applied_config_get_current(&priv->ac_ip6_config), + (NMIP6Config *) applied_config_get_current(&priv->dhcp6.ip6_config), + priv->con_ip_config_6, + (NMIP6Config *) applied_config_get_current(&priv->dev2_ip_config_6)}; + const NMPlatformIP6Address *addr; + NMIP6Config * dad6_config = NULL; + NMDedupMultiIter ipconf_iter; + guint i; + int ifindex; + NMPlatform * platform; + + ifindex = nm_device_get_ip_ifindex(self); + g_return_val_if_fail(ifindex > 0, NULL); + + platform = nm_device_get_platform(self); + + if (priv->ipv6ll_has) { + dad6_add_pending_address(self, platform, ifindex, &priv->ipv6ll_addr, &dad6_config); + } + + /* We are interested only in addresses that we have explicitly configured, + * not in externally added ones. + */ + for (i = 0; i < G_N_ELEMENTS(confs); i++) { + if (!confs[i]) + continue; + + nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, confs[i], &addr) { + dad6_add_pending_address(self, platform, ifindex, &addr->address, &dad6_config); + } + } + + return dad6_config; +} + +/*****************************************************************************/ + +static void +act_request_set(NMDevice *self, NMActRequest *act_request) +{ + NMDevicePrivate *priv; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(!act_request || NM_IS_ACT_REQUEST(act_request)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->act_request.visible && priv->act_request.obj == act_request) + return; + + /* always clear the public flag. The few callers that set a new @act_request + * don't want that the property is public yet. */ + nm_dbus_track_obj_path_set(&priv->act_request, act_request, FALSE); + + if (act_request) { + switch (nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(act_request))) { + case NM_ACTIVATION_TYPE_EXTERNAL: + break; + case NM_ACTIVATION_TYPE_ASSUME: + if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_ASSUME); + break; + case NM_ACTIVATION_TYPE_MANAGED: + if (NM_IN_SET_TYPED(NMDeviceSysIfaceState, + priv->sys_iface_state, + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_ASSUME)) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED); + break; + } + } +} + +static void +dnsmasq_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->dnsmasq_manager) + return; + + nm_clear_g_signal_handler(priv->dnsmasq_manager, &priv->dnsmasq_state_id); + + nm_dnsmasq_manager_stop(priv->dnsmasq_manager); + g_object_unref(priv->dnsmasq_manager); + priv->dnsmasq_manager = NULL; +} + +gboolean +nm_device_is_nm_owned(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->nm_owned; +} + +/* + * delete_on_deactivate_link_delete + * + * Function will be queued with g_idle_add to call + * nm_platform_link_delete for the underlying resources + * of the device. + */ +static gboolean +delete_on_deactivate_link_delete(gpointer user_data) +{ + DeleteOnDeactivateData *data = user_data; + NMDevice * self = data->device; + + _LOGD(LOGD_DEVICE, + "delete_on_deactivate: cleanup and delete virtual link #%d (id=%u)", + data->ifindex, + data->idle_add_id); + + if (data->device) { + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(data->device); + gs_free_error GError *error = NULL; + + g_object_remove_weak_pointer(G_OBJECT(data->device), (void **) &data->device); + priv->delete_on_deactivate_data = NULL; + + if (!nm_device_unrealize(data->device, TRUE, &error)) + _LOGD(LOGD_DEVICE, + "delete_on_deactivate: unrealizing %d failed (%s)", + data->ifindex, + error->message); + } else if (data->ifindex > 0) + nm_platform_link_delete(nm_device_get_platform(self), data->ifindex); + + nm_device_emit_recheck_auto_activate(self); + + g_free(data); + return FALSE; +} + +static void +delete_on_deactivate_unschedule(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->delete_on_deactivate_data) { + DeleteOnDeactivateData *data = priv->delete_on_deactivate_data; + + priv->delete_on_deactivate_data = NULL; + + g_source_remove(data->idle_add_id); + g_object_remove_weak_pointer(G_OBJECT(self), (void **) &data->device); + _LOGD(LOGD_DEVICE, + "delete_on_deactivate: cancel cleanup and delete virtual link #%d (id=%u)", + data->ifindex, + data->idle_add_id); + g_free(data); + } +} + +static void +delete_on_deactivate_check_and_schedule(NMDevice *self, int ifindex) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + DeleteOnDeactivateData *data; + + if (!priv->nm_owned) + return; + if (priv->queued_act_request) + return; + if (!nm_device_is_software(self) || !nm_device_is_real(self)) + return; + if (nm_device_get_state(self) == NM_DEVICE_STATE_UNMANAGED) + return; + if (nm_device_get_state(self) == NM_DEVICE_STATE_UNAVAILABLE) + return; + delete_on_deactivate_unschedule(self); /* always cancel and reschedule */ + + data = g_new(DeleteOnDeactivateData, 1); + g_object_add_weak_pointer(G_OBJECT(self), (void **) &data->device); + data->device = self; + data->ifindex = ifindex; + data->idle_add_id = g_idle_add(delete_on_deactivate_link_delete, data); + priv->delete_on_deactivate_data = data; + + _LOGD(LOGD_DEVICE, + "delete_on_deactivate: schedule cleanup and delete virtual link #%d (id=%u)", + ifindex, + data->idle_add_id); +} + +static void +_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_NONE); + + if (nm_clear_g_source(&priv->queued_ip_config_id_x[IS_IPv4])) { + _LOGD(LOGD_DEVICE, + "clearing queued IP%c config change", + nm_utils_addr_family_to_char(addr_family)); + } + + if (IS_IPv4) { + dhcp4_cleanup(self, cleanup_type, FALSE); + arp_cleanup(self); + dnsmasq_cleanup(self); + ipv4ll_cleanup(self); + g_slist_free_full(priv->acd.dad_list, (GDestroyNotify) nm_acd_manager_free); + priv->acd.dad_list = NULL; + } else { + g_slist_free_full(priv->dad6_failed_addrs, (GDestroyNotify) nmp_object_unref); + priv->dad6_failed_addrs = NULL; + g_clear_object(&priv->dad6_ip6_config); + dhcp6_cleanup(self, cleanup_type, FALSE); + nm_clear_g_source(&priv->linklocal6_timeout_id); + addrconf6_cleanup(self); + } +} + +gboolean +_nm_device_hash_check_invalid_keys(GHashTable * hash, + const char * setting_name, + GError ** error, + const char *const *whitelist) +{ + guint found_whitelisted_keys = 0; + guint i; + + nm_assert(hash && g_hash_table_size(hash) > 0); + nm_assert(whitelist && whitelist[0]); + +#if NM_MORE_ASSERTS > 10 + /* Require whitelist to only contain unique keys. */ + { + gs_unref_hashtable GHashTable *check_dups = + g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, NULL); + + for (i = 0; whitelist[i]; i++) { + if (!g_hash_table_add(check_dups, (char *) whitelist[i])) + nm_assert(FALSE); + } + nm_assert(g_hash_table_size(check_dups) > 0); + } +#endif + + for (i = 0; whitelist[i]; i++) { + if (g_hash_table_contains(hash, whitelist[i])) + found_whitelisted_keys++; + } + + if (found_whitelisted_keys == g_hash_table_size(hash)) { + /* Good, there are only whitelisted keys in the hash. */ + return TRUE; + } + + if (error) { + GHashTableIter iter; + const char * k = NULL; + const char * first_invalid_key = NULL; + + g_hash_table_iter_init(&iter, hash); + while (g_hash_table_iter_next(&iter, (gpointer *) &k, NULL)) { + if (nm_utils_strv_find_first((char **) whitelist, -1, k) < 0) { + first_invalid_key = k; + break; + } + } + if (setting_name) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Can't reapply changes to '%s.%s' setting", + setting_name, + first_invalid_key); + } else { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Can't reapply any changes to '%s' setting", + first_invalid_key); + } + g_return_val_if_fail(first_invalid_key, FALSE); + } + + return FALSE; +} + +void +nm_device_reactivate_ip_config(NMDevice * self, + int addr_family, + NMSettingIPConfig *s_ip_old, + NMSettingIPConfig *s_ip_new) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMDevicePrivate *priv; + const char * method_old; + const char * method_new; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->ip_state_x[IS_IPv4] == NM_DEVICE_IP_STATE_NONE) + return; + + g_clear_object(&priv->con_ip_config_x[IS_IPv4]); + g_clear_object(&priv->ext_ip_config_x[IS_IPv4]); + if (IS_IPv4) { + g_clear_object(&priv->dev_ip_config_4.current); + } else { + g_clear_object(&priv->ac_ip6_config.current); + g_clear_object(&priv->dhcp6.ip6_config.current); + } + g_clear_object(&priv->dev2_ip_config_x[IS_IPv4].current); + + if (!IS_IPv4) { + if (priv->ipv6ll_handle && !IN6_IS_ADDR_UNSPECIFIED(&priv->ipv6ll_addr)) + priv->ipv6ll_has = TRUE; + } + + priv->con_ip_config_x[IS_IPv4] = nm_device_ip_config_new(self, addr_family); + + if (IS_IPv4) { + nm_ip4_config_merge_setting(priv->con_ip_config_4, + s_ip_new, + _prop_get_connection_mdns(self), + _prop_get_connection_llmnr(self), + nm_device_get_route_table(self, AF_INET), + nm_device_get_route_metric(self, AF_INET)); + } else { + nm_ip6_config_merge_setting(priv->con_ip_config_6, + s_ip_new, + nm_device_get_route_table(self, AF_INET6), + nm_device_get_route_metric(self, AF_INET6)); + } + + method_old = (s_ip_old ? nm_setting_ip_config_get_method(s_ip_old) : NULL) + ?: (IS_IPv4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED + : NM_SETTING_IP6_CONFIG_METHOD_IGNORE); + method_new = (s_ip_new ? nm_setting_ip_config_get_method(s_ip_new) : NULL) + ?: (IS_IPv4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED + : NM_SETTING_IP6_CONFIG_METHOD_IGNORE); + + if (!nm_streq0(method_old, method_new)) { + _cleanup_ip_pre(self, addr_family, CLEANUP_TYPE_DECONFIGURE); + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_WAIT); + if (!nm_device_activate_stage3_ip_start(self, addr_family)) { + _LOGW(LOGD_IP4, + "Failed to apply IPv%c configuration", + nm_utils_addr_family_to_char(addr_family)); + } + return; + } + + if (s_ip_old && s_ip_new) { + gint64 metric_old, metric_new; + + /* For dynamic IP methods (DHCP, IPv4LL, WWAN) the route metric is + * set at activation/renewal time using the value from static + * configuration. To support runtime change we need to update the + * dynamic configuration in place and tell the DHCP client the new + * value to use for future renewals. + */ + metric_old = nm_setting_ip_config_get_route_metric(s_ip_old); + metric_new = nm_setting_ip_config_get_route_metric(s_ip_new); + + if (metric_old != metric_new) { + if (IS_IPv4) { + if (priv->dev_ip_config_4.orig) { + nm_ip4_config_update_routes_metric((NMIP4Config *) priv->dev_ip_config_4.orig, + nm_device_get_route_metric(self, AF_INET)); + } + if (priv->dev2_ip_config_4.orig) { + nm_ip4_config_update_routes_metric((NMIP4Config *) priv->dev2_ip_config_4.orig, + nm_device_get_route_metric(self, AF_INET)); + } + if (priv->dhcp_data_4.client) { + nm_dhcp_client_set_route_metric(priv->dhcp_data_4.client, + nm_device_get_route_metric(self, AF_INET)); + } + } else { + if (priv->ac_ip6_config.orig) { + nm_ip6_config_update_routes_metric((NMIP6Config *) priv->ac_ip6_config.orig, + nm_device_get_route_metric(self, AF_INET6)); + } + if (priv->dhcp6.ip6_config.orig) { + nm_ip6_config_update_routes_metric((NMIP6Config *) priv->dhcp6.ip6_config.orig, + nm_device_get_route_metric(self, AF_INET6)); + } + if (priv->dev2_ip_config_6.orig) { + nm_ip6_config_update_routes_metric((NMIP6Config *) priv->dev2_ip_config_6.orig, + nm_device_get_route_metric(self, AF_INET6)); + } + if (priv->dhcp_data_6.client) { + nm_dhcp_client_set_route_metric(priv->dhcp_data_6.client, + nm_device_get_route_metric(self, AF_INET6)); + } + } + } + } + + if (nm_device_get_ip_ifindex(self) > 0 && !ip_config_merge_and_apply(self, addr_family, TRUE)) { + _LOGW(LOGD_IPX(IS_IPv4), + "Failed to reapply IPv%c configuration", + nm_utils_addr_family_to_char(addr_family)); + } +} + +static void +_pacrunner_manager_add(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id); + + priv->pacrunner_conf_id = nm_pacrunner_manager_add(nm_pacrunner_manager_get(), + priv->proxy_config, + nm_device_get_ip_iface(self), + NULL, + NULL); +} + +static void +reactivate_proxy_config(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->pacrunner_conf_id) + return; + nm_device_set_proxy_config(self, priv->dhcp4.pac_url); + _pacrunner_manager_add(self); +} + +static gboolean +can_reapply_change(NMDevice * self, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + if (nm_streq(setting_name, NM_SETTING_CONNECTION_SETTING_NAME)) { + /* Whitelist allowed properties from "connection" setting which are + * allowed to differ. + * + * This includes UUID, there is no principal problem with reapplying a + * connection and changing its UUID. In fact, disallowing it makes it + * cumbersome for the user to reapply any connection but the original + * settings-connection. */ + return nm_device_hash_check_invalid_keys(diffs, + NM_SETTING_CONNECTION_SETTING_NAME, + error, + NM_SETTING_CONNECTION_ID, + NM_SETTING_CONNECTION_UUID, + NM_SETTING_CONNECTION_STABLE_ID, + NM_SETTING_CONNECTION_AUTOCONNECT, + NM_SETTING_CONNECTION_ZONE, + NM_SETTING_CONNECTION_METERED, + NM_SETTING_CONNECTION_LLDP, + NM_SETTING_CONNECTION_MDNS, + NM_SETTING_CONNECTION_LLMNR); + } + + if (NM_IN_STRSET(setting_name, + NM_SETTING_USER_SETTING_NAME, + NM_SETTING_PROXY_SETTING_NAME, + NM_SETTING_IP4_CONFIG_SETTING_NAME, + NM_SETTING_IP6_CONFIG_SETTING_NAME)) + return TRUE; + + if (nm_streq(setting_name, NM_SETTING_WIRED_SETTING_NAME)) { + if (NM_IN_SET(NM_DEVICE_GET_CLASS(self)->get_configured_mtu, + nm_device_get_configured_mtu_wired_parent, + nm_device_get_configured_mtu_for_wired)) { + return nm_device_hash_check_invalid_keys(diffs, + NM_SETTING_WIRED_SETTING_NAME, + error, + NM_SETTING_WIRED_MTU); + } + goto out_fail; + } + + if (nm_streq(setting_name, NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME) + && NM_DEVICE_GET_CLASS(self)->can_reapply_change_ovs_external_ids) { + /* TODO: this means, you cannot reapply changes to the external-ids for + * OVS system interfaces. */ + return TRUE; + } + +out_fail: + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Can't reapply any changes to '%s' setting", + setting_name); + return FALSE; +} + +static void +reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new) +{} + +/* check_and_reapply_connection: + * @connection: the new connection settings to be applied or %NULL to reapply + * the current settings connection + * @version_id: either zero, or the current version id for the applied + * connection. + * @audit_args: on return, a string representing the changes + * @error: the error if %FALSE is returned + * + * Change configuration of an already configured device if possible. + * Updates the device's applied connection upon success. + * + * Return: %FALSE if the new configuration can not be reapplied. + */ +static gboolean +check_and_reapply_connection(NMDevice * self, + NMConnection *connection, + guint64 version_id, + char ** audit_args, + GError ** error) +{ + NMDeviceClass * klass = NM_DEVICE_GET_CLASS(self); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * applied = nm_device_get_applied_connection(self); + gs_unref_object NMConnection *applied_clone = NULL; + gs_unref_hashtable GHashTable *diffs = NULL; + NMConnection * con_old, *con_new; + NMSettingIPConfig * s_ip4_old, *s_ip4_new; + NMSettingIPConfig * s_ip6_old, *s_ip6_new; + GHashTableIter iter; + + if (priv->state < NM_DEVICE_STATE_PREPARE || priv->state > NM_DEVICE_STATE_ACTIVATED) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "Device is not activated"); + return FALSE; + } + + nm_connection_diff(connection, + applied, + NM_SETTING_COMPARE_FLAG_IGNORE_TIMESTAMP + | NM_SETTING_COMPARE_FLAG_IGNORE_SECRETS, + &diffs); + + if (audit_args) { + if (diffs && nm_audit_manager_audit_enabled(nm_audit_manager_get())) + *audit_args = nm_utils_format_con_diff_for_audit(diffs); + else + *audit_args = NULL; + } + + /************************************************************************** + * check for unsupported changes and reject to reapply + *************************************************************************/ + if (diffs) { + char * setting_name; + GHashTable *setting_diff; + + g_hash_table_iter_init(&iter, diffs); + while ( + g_hash_table_iter_next(&iter, (gpointer *) &setting_name, (gpointer *) &setting_diff)) { + if (!klass->can_reapply_change( + self, + setting_name, + nm_connection_get_setting_by_name(applied, setting_name), + nm_connection_get_setting_by_name(connection, setting_name), + setting_diff, + error)) + return FALSE; + } + } + + if (version_id != 0 + && version_id + != nm_active_connection_version_id_get( + (NMActiveConnection *) priv->act_request.obj)) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_VERSION_ID_MISMATCH, + "Reapply failed because device changed in the meantime and the version-id mismatches"); + return FALSE; + } + + /************************************************************************** + * Update applied connection + *************************************************************************/ + + if (diffs) + nm_active_connection_version_id_bump((NMActiveConnection *) priv->act_request.obj); + + _LOGD(LOGD_DEVICE, + "reapply (version-id %llu%s)", + (unsigned long long) nm_active_connection_version_id_get( + ((NMActiveConnection *) priv->act_request.obj)), + diffs ? "" : " (unmodified)"); + + if (diffs) { + NMConnection * connection_clean = connection; + gs_unref_object NMConnection *connection_clean_free = NULL; + + { + NMSettingConnection *s_con_a, *s_con_n; + + /* we allow re-applying a connection with differing ID, UUID, STABLE_ID and AUTOCONNECT. + * This is for convenience but these values are not actually changeable. So, check + * if they changed, and if the did revert to the original values. */ + s_con_a = nm_connection_get_setting_connection(applied); + s_con_n = nm_connection_get_setting_connection(connection); + + if (!nm_streq(nm_setting_connection_get_id(s_con_a), + nm_setting_connection_get_id(s_con_n)) + || !nm_streq(nm_setting_connection_get_uuid(s_con_a), + nm_setting_connection_get_uuid(s_con_n)) + || nm_setting_connection_get_autoconnect(s_con_a) + != nm_setting_connection_get_autoconnect(s_con_n) + || !nm_streq0(nm_setting_connection_get_stable_id(s_con_a), + nm_setting_connection_get_stable_id(s_con_n))) { + connection_clean_free = nm_simple_connection_new_clone(connection); + connection_clean = connection_clean_free; + s_con_n = nm_connection_get_setting_connection(connection_clean); + g_object_set(s_con_n, + NM_SETTING_CONNECTION_ID, + nm_setting_connection_get_id(s_con_a), + NM_SETTING_CONNECTION_UUID, + nm_setting_connection_get_uuid(s_con_a), + NM_SETTING_CONNECTION_AUTOCONNECT, + nm_setting_connection_get_autoconnect(s_con_a), + NM_SETTING_CONNECTION_STABLE_ID, + nm_setting_connection_get_stable_id(s_con_a), + NULL); + } + } + + con_old = applied_clone = nm_simple_connection_new_clone(applied); + con_new = applied; + /* FIXME(applied-connection-immutable): we should not modify the applied + * connection but replace it with a new (immutable) instance. */ + nm_connection_replace_settings_from_connection(applied, connection_clean); + nm_connection_clear_secrets(applied); + } else + con_old = con_new = applied; + + priv->v4_commit_first_time = TRUE; + priv->v6_commit_first_time = TRUE; + + priv->v4_route_table_initialized = FALSE; + priv->v6_route_table_initialized = FALSE; + + /************************************************************************** + * Reapply changes + * + * Note that reapply_connection() is called as very first. This is for example + * important for NMDeviceWireGuard, which implements coerce_route_table() + * and get_extra_rules(). + * That is because NMDeviceWireGuard caches settings, so during reapply that + * cache must be updated *first*. + *************************************************************************/ + klass->reapply_connection(self, con_old, con_new); + + if (priv->state >= NM_DEVICE_STATE_CONFIG) + lldp_init(self, FALSE); + + if (priv->state >= NM_DEVICE_STATE_IP_CONFIG) { + s_ip4_old = nm_connection_get_setting_ip4_config(con_old); + s_ip4_new = nm_connection_get_setting_ip4_config(con_new); + s_ip6_old = nm_connection_get_setting_ip6_config(con_old); + s_ip6_new = nm_connection_get_setting_ip6_config(con_new); + + /* Allow reapply of MTU */ + priv->mtu_source = NM_DEVICE_MTU_SOURCE_NONE; + + nm_device_reactivate_ip_config(self, AF_INET, s_ip4_old, s_ip4_new); + nm_device_reactivate_ip_config(self, AF_INET6, s_ip6_old, s_ip6_new); + + _routing_rules_sync(self, NM_TERNARY_TRUE); + + reactivate_proxy_config(self); + } + + if (priv->state >= NM_DEVICE_STATE_IP_CHECK) + nm_device_update_firewall_zone(self); + + if (priv->state >= NM_DEVICE_STATE_ACTIVATED) + nm_device_update_metered(self); + + return TRUE; +} + +gboolean +nm_device_reapply(NMDevice *self, NMConnection *connection, GError **error) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + return check_and_reapply_connection(self, connection, 0, NULL, error); +} + +typedef struct { + NMConnection *connection; + guint64 version_id; +} ReapplyData; + +static void +reapply_cb(NMDevice * self, + GDBusMethodInvocation *context, + NMAuthSubject * subject, + GError * error, + gpointer user_data) +{ + ReapplyData * reapply_data = user_data; + guint64 version_id = 0; + gs_unref_object NMConnection *connection = NULL; + GError * local = NULL; + gs_free char * audit_args = NULL; + + if (reapply_data) { + connection = reapply_data->connection; + version_id = reapply_data->version_id; + g_slice_free(ReapplyData, reapply_data); + } + + if (error) { + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, + self, + FALSE, + NULL, + subject, + error->message); + g_dbus_method_invocation_return_gerror(context, error); + return; + } + + if (nm_device_sys_iface_state_is_external(self)) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED); + + if (!check_and_reapply_connection(self, + connection + ?: nm_device_get_settings_connection_get_connection(self), + version_id, + &audit_args, + &local)) { + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, + self, + FALSE, + audit_args, + subject, + local->message); + g_dbus_method_invocation_take_error(context, local); + local = NULL; + } else { + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, self, TRUE, audit_args, subject, NULL); + g_dbus_method_invocation_return_value(context, NULL); + } +} + +static void +impl_device_reapply(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * dbus_connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDevice * self = NM_DEVICE(obj); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingsConnection *settings_connection; + NMConnection * connection = NULL; + GError * error = NULL; + ReapplyData * reapply_data; + gs_unref_variant GVariant *settings = NULL; + guint64 version_id; + guint32 flags; + + g_variant_get(parameters, "(@a{sa{sv}}tu)", &settings, &version_id, &flags); + + /* No flags supported as of now. */ + if (flags != 0) { + error = + g_error_new_literal(NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "Invalid flags specified"); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, + self, + FALSE, + NULL, + invocation, + error->message); + g_dbus_method_invocation_take_error(invocation, error); + return; + } + + if (priv->state < NM_DEVICE_STATE_PREPARE || priv->state > NM_DEVICE_STATE_ACTIVATED) { + error = g_error_new_literal(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "Device is not activated"); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, + self, + FALSE, + NULL, + invocation, + error->message); + g_dbus_method_invocation_take_error(invocation, error); + return; + } + + settings_connection = nm_device_get_settings_connection(self); + g_return_if_fail(settings_connection); + + if (settings && g_variant_n_children(settings)) { + /* New settings specified inline. */ + connection = _nm_simple_connection_new_from_dbus(settings, + NM_SETTING_PARSE_FLAGS_STRICT + | NM_SETTING_PARSE_FLAGS_NORMALIZE, + &error); + if (!connection) { + g_prefix_error(&error, "The settings specified are invalid: "); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, + self, + FALSE, + NULL, + invocation, + error->message); + g_dbus_method_invocation_take_error(invocation, error); + return; + } + nm_connection_clear_secrets(connection); + } + + if (connection || version_id) { + reapply_data = g_slice_new(ReapplyData); + reapply_data->connection = connection; + reapply_data->version_id = version_id; + } else + reapply_data = NULL; + + nm_device_auth_request(self, + invocation, + nm_device_get_applied_connection(self), + NM_AUTH_PERMISSION_NETWORK_CONTROL, + TRUE, + NULL, + reapply_cb, + reapply_data); +} + +/*****************************************************************************/ + +static void +impl_device_get_applied_connection(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDevice * self = NM_DEVICE(obj); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free_error GError *error = NULL; + NMConnection * applied_connection; + guint32 flags; + GVariant * var_settings; + + g_variant_get(parameters, "(u)", &flags); + + /* No flags supported as of now. */ + if (flags != 0) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "Invalid flags specified"); + return; + } + + applied_connection = nm_device_get_applied_connection(self); + if (!applied_connection) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "Device is not activated"); + return; + } + + if (!nm_auth_is_invocation_in_acl_set_error(applied_connection, + invocation, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_PERMISSION_DENIED, + NULL, + &error)) { + g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error)); + return; + } + + var_settings = nm_connection_to_dbus(applied_connection, NM_CONNECTION_SERIALIZE_NO_SECRETS); + if (!var_settings) + var_settings = g_variant_new_array(G_VARIANT_TYPE("{sa{sv}}"), NULL, 0); + + g_dbus_method_invocation_return_value( + invocation, + g_variant_new( + "(@a{sa{sv}}t)", + var_settings, + nm_active_connection_version_id_get((NMActiveConnection *) priv->act_request.obj))); +} + +/*****************************************************************************/ + +typedef struct { + gint64 timestamp_ms; + bool dirty; +} IP6RoutesTemporaryNotAvailableData; + +static gboolean +_rt6_temporary_not_available_timeout(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->rt6_temporary_not_available_id = 0; + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + + return G_SOURCE_REMOVE; +} + +static gboolean +_rt6_temporary_not_available_set(NMDevice *self, GPtrArray *temporary_not_available) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + IP6RoutesTemporaryNotAvailableData *data; + GHashTableIter iter; + gint64 now_ms, oldest_ms; + const gint64 MAX_AGE_MS = 20000; + guint i; + gboolean success = TRUE; + + if (!temporary_not_available || !temporary_not_available->len) { + /* nothing outstanding. Clear tracking the routes. */ + nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref); + nm_clear_g_source(&priv->rt6_temporary_not_available_id); + return success; + } + + if (priv->rt6_temporary_not_available) { + g_hash_table_iter_init(&iter, priv->rt6_temporary_not_available); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &data)) + data->dirty = TRUE; + } else { + priv->rt6_temporary_not_available = + g_hash_table_new_full((GHashFunc) nmp_object_id_hash, + (GEqualFunc) nmp_object_id_equal, + (GDestroyNotify) nmp_object_unref, + nm_g_slice_free_fcn(IP6RoutesTemporaryNotAvailableData)); + } + + now_ms = nm_utils_get_monotonic_timestamp_msec(); + oldest_ms = now_ms; + + for (i = 0; i < temporary_not_available->len; i++) { + const NMPObject *o = temporary_not_available->pdata[i]; + + data = g_hash_table_lookup(priv->rt6_temporary_not_available, o); + if (data) { + if (!data->dirty) + continue; + data->dirty = FALSE; + nm_assert(data->timestamp_ms > 0 && data->timestamp_ms <= now_ms); + if (now_ms > data->timestamp_ms + MAX_AGE_MS) { + /* timeout. Could not add this address. */ + _LOGW(LOGD_DEVICE, + "failure to add IPv6 route: %s", + nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0)); + success = FALSE; + } else + oldest_ms = MIN(data->timestamp_ms, oldest_ms); + continue; + } + + data = g_slice_new0(IP6RoutesTemporaryNotAvailableData); + data->timestamp_ms = now_ms; + g_hash_table_insert(priv->rt6_temporary_not_available, (gpointer) nmp_object_ref(o), data); + } + + g_hash_table_iter_init(&iter, priv->rt6_temporary_not_available); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &data)) { + if (data->dirty) + g_hash_table_iter_remove(&iter); + } + + nm_clear_g_source(&priv->rt6_temporary_not_available_id); + priv->rt6_temporary_not_available_id = + g_timeout_add(oldest_ms + MAX_AGE_MS - now_ms, _rt6_temporary_not_available_timeout, self); + + return success; +} + +/*****************************************************************************/ + +static void +disconnect_cb(NMDevice * self, + GDBusMethodInvocation *context, + NMAuthSubject * subject, + GError * error, + gpointer user_data) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + GError * local = NULL; + + if (error) { + g_dbus_method_invocation_return_gerror(context, error); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT, + self, + FALSE, + NULL, + subject, + error->message); + return; + } + + /* Authorized */ + if (priv->state <= NM_DEVICE_STATE_DISCONNECTED) { + local = g_error_new_literal(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "Device is not active"); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT, + self, + FALSE, + NULL, + subject, + local->message); + g_dbus_method_invocation_take_error(context, local); + } else { + nm_device_autoconnect_blocked_set(self, NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT); + + nm_device_state_changed(self, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + g_dbus_method_invocation_return_value(context, NULL); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT, self, TRUE, NULL, subject, NULL); + } +} + +static void +_clear_queued_act_request(NMDevicePrivate *priv, NMActiveConnectionStateReason active_reason) +{ + if (priv->queued_act_request) { + gs_unref_object NMActRequest *ac = NULL; + + ac = g_steal_pointer(&priv->queued_act_request); + nm_active_connection_set_state_fail((NMActiveConnection *) ac, active_reason, NULL); + } +} + +static void +impl_device_disconnect(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * dbus_connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDevice * self = NM_DEVICE(obj); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + + if (!priv->act_request.obj) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "This device is not active"); + return; + } + + connection = nm_device_get_applied_connection(self); + nm_assert(connection); + + nm_device_auth_request(self, + invocation, + connection, + NM_AUTH_PERMISSION_NETWORK_CONTROL, + TRUE, + NULL, + disconnect_cb, + NULL); +} + +static void +delete_cb(NMDevice * self, + GDBusMethodInvocation *context, + NMAuthSubject * subject, + GError * error, + gpointer user_data) +{ + GError *local = NULL; + + if (error) { + g_dbus_method_invocation_return_gerror(context, error); + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DELETE, + self, + FALSE, + NULL, + subject, + error->message); + return; + } + + /* Authorized */ + nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DELETE, self, TRUE, NULL, subject, NULL); + if (nm_device_unrealize(self, TRUE, &local)) + g_dbus_method_invocation_return_value(context, NULL); + else + g_dbus_method_invocation_take_error(context, local); +} + +static void +impl_device_delete(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDevice *self = NM_DEVICE(obj); + + if (!nm_device_is_software(self) || !nm_device_is_real(self)) { + g_dbus_method_invocation_return_error_literal( + invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_SOFTWARE, + "This device is not a software device or is not realized"); + return; + } + + nm_device_auth_request(self, + invocation, + NULL, + NM_AUTH_PERMISSION_NETWORK_CONTROL, + TRUE, + NULL, + delete_cb, + NULL); +} + +static void +_device_activate(NMDevice *self, NMActRequest *req) +{ + NMConnection *connection; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + nm_assert(nm_device_is_real(self)); + + /* Ensure the activation request is still valid; the master may have + * already failed in which case activation of this device should not proceed. + */ + if (nm_active_connection_get_state(NM_ACTIVE_CONNECTION(req)) + >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) + return; + + if (!nm_device_get_managed(self, FALSE)) { + /* It's unclear why the device would be unmanaged at this point. + * Just to be sure, handle it and error out. */ + _LOGE(LOGD_DEVICE, + "Activation: failed activating connection '%s' because device is still unmanaged", + nm_active_connection_get_settings_connection_id((NMActiveConnection *) req)); + nm_active_connection_set_state_fail((NMActiveConnection *) req, + NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN, + NULL); + return; + } + + connection = nm_act_request_get_applied_connection(req); + nm_assert(connection); + + _LOGI(LOGD_DEVICE, + "Activation: starting connection '%s' (%s)", + nm_connection_get_id(connection), + nm_connection_get_uuid(connection)); + + delete_on_deactivate_unschedule(self); + + act_request_set(self, req); + + nm_device_activate_schedule_stage1_device_prepare(self, FALSE); +} + +static void +_carrier_wait_check_queued_act_request(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->queued_act_request || !priv->queued_act_request_is_waiting_for_carrier) + return; + + priv->queued_act_request_is_waiting_for_carrier = FALSE; + if (!priv->carrier) { + _LOGD(LOGD_DEVICE, "Cancel queued activation request as we have no carrier after timeout"); + _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED); + } else if (priv->state == NM_DEVICE_STATE_DISCONNECTED) { + gs_unref_object NMActRequest *queued_req = NULL; + + _LOGD(LOGD_DEVICE, "Activate queued activation request as we now have carrier"); + queued_req = g_steal_pointer(&priv->queued_act_request); + _device_activate(self, queued_req); + } +} + +static gboolean +_carrier_wait_check_act_request_must_queue(NMDevice *self, NMActRequest *req) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + + /* If we have carrier or if we are not waiting for it, the activation + * request is not blocked waiting for carrier. */ + if (priv->carrier) + return FALSE; + if (priv->carrier_wait_id == 0) + return FALSE; + + connection = nm_act_request_get_applied_connection(req); + if (!connection_requires_carrier(connection)) + return FALSE; + + if (!nm_device_check_connection_available(self, + connection, + NM_DEVICE_CHECK_CON_AVAILABLE_ALL, + NULL, + NULL)) { + /* We passed all @flags we have, and no @specific_object. + * This equals maximal availability, if a connection is not available + * in this case, it is not waiting for carrier. + * + * Actually, why are we even trying to activate it? Strange, but whatever + * the reason, don't wait for carrier. + */ + return FALSE; + } + + if (nm_device_check_connection_available( + self, + connection, + NM_DEVICE_CHECK_CON_AVAILABLE_ALL + & ~_NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER, + NULL, + NULL)) { + /* The connection was available with flags ALL, and it is still available + * if we pretend not to wait for carrier. That means that the + * connection is available now, and does not wait for carrier. + * + * Since the flags increase the availability of a connection, when checking + * ALL&~WAITING_CARRIER, it means that we certainly would wait for carrier. */ + return FALSE; + } + + /* The activation request must wait for carrier. */ + return TRUE; +} + +void +nm_device_disconnect_active_connection(NMActiveConnection * active, + NMDeviceStateReason device_reason, + NMActiveConnectionStateReason active_reason) +{ + NMDevice * self; + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_ACTIVE_CONNECTION(active)); + + self = nm_active_connection_get_device(active); + if (!self) { + /* hm, no device? Just fail the active connection. */ + goto do_fail; + } + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (NM_ACTIVE_CONNECTION(priv->queued_act_request) == active) { + _clear_queued_act_request(priv, active_reason); + return; + } + + if (NM_ACTIVE_CONNECTION(priv->act_request.obj) == active) { + if (priv->state < NM_DEVICE_STATE_DEACTIVATING) { + /* When the user actively deactivates a profile, we set + * the sys-iface-state to managed so that we deconfigure/cleanup the interface. + * But for external connections that go down otherwise, we don't want to touch the interface. */ + if (nm_device_sys_iface_state_is_external(self)) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED); + + nm_device_state_changed(self, NM_DEVICE_STATE_DEACTIVATING, device_reason); + } else { + /* @active is the current ac of @self, but it's going down already. + * Nothing to do. */ + } + return; + } + + /* the active connection references this device, but it's neither the + * queued_act_request nor the current act_request. Just set it to fail... */ +do_fail: + nm_active_connection_set_state_fail(active, active_reason, NULL); +} + +void +nm_device_queue_activation(NMDevice *self, NMActRequest *req) +{ + NMDevicePrivate *priv; + gboolean must_queue; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + nm_keep_alive_arm(nm_active_connection_get_keep_alive(NM_ACTIVE_CONNECTION(req))); + + if (nm_active_connection_get_state(NM_ACTIVE_CONNECTION(req)) + >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) { + /* it's already deactivating. Nothing to do. */ + nm_assert( + NM_IN_SET(nm_active_connection_get_device(NM_ACTIVE_CONNECTION(req)), NULL, self)); + return; + } + + nm_assert(self == nm_active_connection_get_device(NM_ACTIVE_CONNECTION(req))); + + priv = NM_DEVICE_GET_PRIVATE(self); + + must_queue = _carrier_wait_check_act_request_must_queue(self, req); + + if (!priv->act_request.obj && !must_queue && nm_device_is_real(self)) { + _device_activate(self, req); + return; + } + + /* supersede any already-queued request */ + _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED); + priv->queued_act_request = g_object_ref(req); + priv->queued_act_request_is_waiting_for_carrier = must_queue; + + _LOGD(LOGD_DEVICE, + "queue activation request waiting for %s", + must_queue ? "carrier" : "currently active connection to disconnect"); + + /* Deactivate existing activation request first */ + if (priv->act_request.obj) { + _LOGI(LOGD_DEVICE, "disconnecting for new activation request."); + nm_device_state_changed(self, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_NEW_ACTIVATION); + } +} + +/* + * nm_device_is_activating + * + * Return whether or not the device is currently activating itself. + * + */ +gboolean +nm_device_is_activating(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceState state; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + state = nm_device_get_state(self); + if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_SECONDARIES) + return TRUE; + + /* There's a small race between the time when stage 1 is scheduled + * and when the device actually sets STATE_PREPARE when the activation + * handler is actually run. If there's an activation handler scheduled + * we're activating anyway. + */ + return priv->activation_source_id_4 != 0; +} + +NMProxyConfig * +nm_device_get_proxy_config(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->proxy_config; +} + +static void +nm_device_set_proxy_config(NMDevice *self, const char *pac_url) +{ + NMDevicePrivate *priv; + NMConnection * connection; + NMSettingProxy * s_proxy = NULL; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + g_clear_object(&priv->proxy_config); + priv->proxy_config = nm_proxy_config_new(); + + if (pac_url) { + nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_AUTO); + nm_proxy_config_set_pac_url(priv->proxy_config, pac_url); + _LOGD(LOGD_PROXY, "proxy: PAC url \"%s\"", pac_url); + } else + nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_NONE); + + connection = nm_device_get_applied_connection(self); + if (connection) + s_proxy = nm_connection_get_setting_proxy(connection); + + if (s_proxy) + nm_proxy_config_merge_setting(priv->proxy_config, s_proxy); +} + +/* IP Configuration stuff */ +NMDhcpConfig * +nm_device_get_dhcp_config(NMDevice *self, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + nm_assert_addr_family(addr_family); + + return NM_DEVICE_GET_PRIVATE(self)->dhcp_data_x[IS_IPv4].config; +} + +NMIP4Config * +nm_device_get_ip4_config(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->ip_config_4; +} + +static gboolean +nm_device_set_ip_config(NMDevice * self, + int addr_family, + NMIPConfig *new_config, + gboolean commit, + GPtrArray * ip4_dev_route_blacklist) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const int IS_IPv4 = NM_IS_IPv4(addr_family); + NMIPConfig * old_config; + gboolean has_changes = FALSE; + gboolean success = TRUE; + NMSettingsConnection *settings_connection; + + nm_assert_addr_family(addr_family); + nm_assert(!new_config || nm_ip_config_get_addr_family(new_config) == addr_family); + nm_assert(!new_config + || (new_config && ({ + int ip_ifindex = nm_device_get_ip_ifindex(self); + + (ip_ifindex > 0 && ip_ifindex == nm_ip_config_get_ifindex(new_config)); + }))); + nm_assert(IS_IPv4 || !ip4_dev_route_blacklist); + + _LOGD(LOGD_IPX(IS_IPv4), + "ip%c-config: update (commit=%d, new-config=%p)", + nm_utils_addr_family_to_char(addr_family), + commit, + new_config); + + /* Always commit to nm-platform to update lifetimes */ + if (commit && new_config) { + _commit_mtu(self, IS_IPv4 ? NM_IP4_CONFIG(new_config) : priv->ip_config_4); + + if (IS_IPv4) { + success = nm_ip4_config_commit(NM_IP4_CONFIG(new_config), + nm_device_get_platform(self), + _get_route_table_sync_mode_stateful(self, AF_INET)); + nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self), + nm_ip_config_get_ifindex(new_config), + ip4_dev_route_blacklist); + } else { + gs_unref_ptrarray GPtrArray *temporary_not_available = NULL; + + success = nm_ip6_config_commit(NM_IP6_CONFIG(new_config), + nm_device_get_platform(self), + _get_route_table_sync_mode_stateful(self, AF_INET6), + &temporary_not_available); + + if (!_rt6_temporary_not_available_set(self, temporary_not_available)) + success = FALSE; + } + } + + old_config = priv->ip_config_x[IS_IPv4]; + + if (new_config && old_config) { + /* has_changes is set only on relevant changes, because when the configuration changes, + * this causes a re-read and reset. This should only happen for relevant changes */ + nm_ip_config_replace(old_config, new_config, &has_changes); + if (has_changes) { + _LOGD(LOGD_IPX(IS_IPv4), + "ip%c-config: update IP Config instance (%s)", + nm_utils_addr_family_to_char(addr_family), + nm_dbus_object_get_path(NM_DBUS_OBJECT(old_config))); + } + } else if (new_config /*&& !old_config*/) { + has_changes = TRUE; + priv->ip_config_x[IS_IPv4] = g_object_ref(new_config); + if (!nm_dbus_object_is_exported(NM_DBUS_OBJECT(new_config))) + nm_dbus_object_export(NM_DBUS_OBJECT(new_config)); + + _LOGD(LOGD_IPX(IS_IPv4), + "ip%c-config: set IP Config instance (%s)", + nm_utils_addr_family_to_char(addr_family), + nm_dbus_object_get_path(NM_DBUS_OBJECT(new_config))); + } else if (old_config /*&& !new_config*/) { + has_changes = TRUE; + priv->ip_config_x[IS_IPv4] = NULL; + _LOGD(LOGD_IPX(IS_IPv4), + "ip%c-config: clear IP Config instance (%s)", + nm_utils_addr_family_to_char(addr_family), + nm_dbus_object_get_path(NM_DBUS_OBJECT(old_config))); + if (IS_IPv4) { + /* Device config is invalid if combined config is invalid */ + applied_config_clear(&priv->dev_ip_config_4); + } else + priv->needs_ip6_subnet = FALSE; + } + + if (has_changes) { + if (old_config != priv->ip_config_x[IS_IPv4]) + _notify(self, IS_IPv4 ? PROP_IP4_CONFIG : PROP_IP6_CONFIG); + + g_signal_emit(self, + signals[IS_IPv4 ? IP4_CONFIG_CHANGED : IP6_CONFIG_CHANGED], + 0, + priv->ip_config_x[IS_IPv4], + old_config); + + if (old_config != priv->ip_config_x[IS_IPv4]) + nm_dbus_object_clear_and_unexport(&old_config); + + if (nm_device_sys_iface_state_is_external(self) + && (settings_connection = nm_device_get_settings_connection(self)) + && NM_FLAGS_HAS(nm_settings_connection_get_flags(settings_connection), + NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL) + && nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(priv->act_request.obj)) + == NM_ACTIVATION_TYPE_EXTERNAL) { + gs_unref_object NMConnection *new_connection = NULL; + + new_connection = nm_simple_connection_new_clone( + nm_settings_connection_get_connection(settings_connection)); + + nm_connection_add_setting( + new_connection, + IS_IPv4 ? nm_ip4_config_create_setting(priv->ip_config_4) + : nm_ip6_config_create_setting(priv->ip_config_6, + _get_maybe_ipv6_disabled(self))); + + nm_settings_connection_update(settings_connection, + new_connection, + NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY, + NM_SETTINGS_CONNECTION_INT_FLAGS_NONE, + NM_SETTINGS_CONNECTION_INT_FLAGS_NONE, + NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE, + "update-external", + NULL); + } + + nm_device_queue_recheck_assume(self); + + if (!IS_IPv4) { + if (priv->ndisc) + ndisc_set_router_config(priv->ndisc, self); + } + } + + nm_assert(!old_config || old_config == priv->ip_config_x[IS_IPv4]); + + return success; +} + +static gboolean +_replace_vpn_config_in_list(GSList **plist, GObject *old, GObject *new) +{ + GSList *old_link; + + /* Below, assert that @new is not yet tracked, but still behave + * correctly in any case. Don't complain for missing @old since + * it could have been removed when the parent device became + * unmanaged. */ + + if (old && (old_link = g_slist_find(*plist, old))) { + if (old != new) { + if (new) + old_link->data = g_object_ref(new); + else + *plist = g_slist_delete_link(*plist, old_link); + g_object_unref(old); + } + return TRUE; + } + + if (new) { + if (!g_slist_find(*plist, new)) + *plist = g_slist_append(*plist, g_object_ref(new)); + else + g_return_val_if_reached(TRUE); + return TRUE; + } + + return FALSE; +} + +void +nm_device_replace_vpn4_config(NMDevice *self, NMIP4Config *old, NMIP4Config *config) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(!old || NM_IS_IP4_CONFIG(old)); + nm_assert(!config || NM_IS_IP4_CONFIG(config)); + nm_assert(!old || nm_ip4_config_get_ifindex(old) == nm_device_get_ip_ifindex(self)); + nm_assert(!config || nm_ip4_config_get_ifindex(config) == nm_device_get_ip_ifindex(self)); + + if (!_replace_vpn_config_in_list(&priv->vpn_configs_4, (GObject *) old, (GObject *) config)) + return; + + /* NULL to use existing configs */ + if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) + _LOGW(LOGD_IP4, "failed to set VPN routes for device"); +} + +void +nm_device_set_dev2_ip_config(NMDevice *self, int addr_family, NMIPConfig *config) +{ + NMDevicePrivate *priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6)); + g_return_if_fail(!config || nm_ip_config_get_addr_family(config) == addr_family); + + priv = NM_DEVICE_GET_PRIVATE(self); + + applied_config_init(&priv->dev2_ip_config_x[IS_IPv4], config); + if (!ip_config_merge_and_apply(self, addr_family, TRUE)) { + _LOGW(LOGD_IP, + "failed to set extra device IPv%c configuration", + nm_utils_addr_family_to_char(addr_family)); + } +} + +void +nm_device_replace_vpn6_config(NMDevice *self, NMIP6Config *old, NMIP6Config *config) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(!old || NM_IS_IP6_CONFIG(old)); + nm_assert(!config || NM_IS_IP6_CONFIG(config)); + nm_assert(!old || nm_ip6_config_get_ifindex(old) == nm_device_get_ip_ifindex(self)); + nm_assert(!config || nm_ip6_config_get_ifindex(config) == nm_device_get_ip_ifindex(self)); + + if (!_replace_vpn_config_in_list(&priv->vpn_configs_6, (GObject *) old, (GObject *) config)) + return; + + /* NULL to use existing configs */ + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "failed to set VPN routes for device"); +} + +NMIP6Config * +nm_device_get_ip6_config(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->ip_config_6; +} + +/*****************************************************************************/ + +static gboolean +dispatcher_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->dispatcher.call_id) + return FALSE; + + nm_dispatcher_call_cancel(g_steal_pointer(&priv->dispatcher.call_id)); + priv->dispatcher.post_state = NM_DEVICE_STATE_UNKNOWN; + priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE; + return TRUE; +} + +static void +dispatcher_complete_proceed_state(NMDispatcherCallId *call_id, gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_if_fail(call_id == priv->dispatcher.call_id); + + priv->dispatcher.call_id = NULL; + nm_device_queue_state(self, priv->dispatcher.post_state, priv->dispatcher.post_state_reason); + priv->dispatcher.post_state = NM_DEVICE_STATE_UNKNOWN; + priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE; +} + +/*****************************************************************************/ + +static void +ip_check_pre_up(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (dispatcher_cleanup(self)) + nm_assert_not_reached(); + + priv->dispatcher.post_state = NM_DEVICE_STATE_SECONDARIES; + priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE; + if (!nm_dispatcher_call_device(NM_DISPATCHER_ACTION_PRE_UP, + self, + NULL, + dispatcher_complete_proceed_state, + self, + &priv->dispatcher.call_id)) { + /* Just proceed on errors */ + dispatcher_complete_proceed_state(0, self); + } +} + +static void +ip_check_gw_ping_cleanup(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_clear_g_source(&priv->gw_ping.watch); + nm_clear_g_source(&priv->gw_ping.timeout); + + if (priv->gw_ping.pid) { + nm_utils_kill_child_async(priv->gw_ping.pid, + SIGTERM, + priv->gw_ping.log_domain, + "ping", + 1000, + NULL, + NULL); + priv->gw_ping.pid = 0; + } + + nm_clear_g_free(&priv->gw_ping.binary); + nm_clear_g_free(&priv->gw_ping.address); +} + +static gboolean +spawn_ping(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gs_free char * str_timeout = NULL; + gs_free char * tmp_str = NULL; + const char * args[] = {priv->gw_ping.binary, + "-I", + nm_device_get_ip_iface(self), + "-c", + "1", + "-w", + NULL, + priv->gw_ping.address, + NULL}; + gs_free_error GError *error = NULL; + gboolean ret; + + args[6] = str_timeout = g_strdup_printf("%u", priv->gw_ping.deadline); + tmp_str = g_strjoinv(" ", (char **) args); + _LOGD(priv->gw_ping.log_domain, "ping: running '%s'", tmp_str); + + ret = g_spawn_async("/", + (char **) args, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &priv->gw_ping.pid, + &error); + + if (!ret) { + _LOGW(priv->gw_ping.log_domain, + "ping: could not spawn %s: %s", + priv->gw_ping.binary, + error->message); + } + + return ret; +} + +static gboolean +respawn_ping_cb(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->gw_ping.watch = 0; + + if (spawn_ping(self)) { + priv->gw_ping.watch = g_child_watch_add(priv->gw_ping.pid, ip_check_ping_watch_cb, self); + } else { + ip_check_gw_ping_cleanup(self); + ip_check_pre_up(self); + } + + return FALSE; +} + +static void +ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMLogDomain log_domain = priv->gw_ping.log_domain; + gboolean success = FALSE; + + if (!priv->gw_ping.watch) + return; + priv->gw_ping.watch = 0; + priv->gw_ping.pid = 0; + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == 0) { + _LOGD(log_domain, "ping: gateway ping succeeded"); + success = TRUE; + } else { + _LOGW(log_domain, "ping: gateway ping failed with error code %d", WEXITSTATUS(status)); + } + } else + _LOGW(log_domain, "ping: stopped unexpectedly with status %d", status); + + if (success) { + /* We've got connectivity, proceed to pre_up */ + ip_check_gw_ping_cleanup(self); + ip_check_pre_up(self); + } else { + /* If ping exited with an error it may have returned early, + * wait 1 second and restart it */ + priv->gw_ping.watch = g_timeout_add_seconds(1, respawn_ping_cb, self); + } +} + +static gboolean +ip_check_ping_timeout_cb(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->gw_ping.timeout = 0; + + _LOGW(priv->gw_ping.log_domain, "ping: gateway ping timed out"); + + ip_check_gw_ping_cleanup(self); + ip_check_pre_up(self); + return FALSE; +} + +static gboolean +start_ping(NMDevice * self, + NMLogDomain log_domain, + const char *binary, + const char *address, + guint timeout) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + g_return_val_if_fail(priv->gw_ping.watch == 0, FALSE); + g_return_val_if_fail(priv->gw_ping.timeout == 0, FALSE); + + priv->gw_ping.log_domain = log_domain; + priv->gw_ping.address = g_strdup(address); + priv->gw_ping.binary = g_strdup(binary); + priv->gw_ping.deadline = timeout + 10; /* the proper termination is enforced by a timer */ + + if (spawn_ping(self)) { + priv->gw_ping.watch = g_child_watch_add(priv->gw_ping.pid, ip_check_ping_watch_cb, self); + priv->gw_ping.timeout = g_timeout_add_seconds(timeout, ip_check_ping_timeout_cb, self); + return TRUE; + } + + ip_check_gw_ping_cleanup(self); + return FALSE; +} + +static void +nm_device_start_ip_check(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMConnection * connection; + NMSettingConnection *s_con; + guint timeout = 0; + const char * ping_binary = NULL; + char buf[NM_UTILS_INET_ADDRSTRLEN]; + NMLogDomain log_domain = LOGD_IP4; + + /* Shouldn't be any active ping here, since IP_CHECK happens after the + * first IP method completes. Any subsequently completing IP method doesn't + * get checked. + */ + g_return_if_fail(!priv->gw_ping.watch); + g_return_if_fail(!priv->gw_ping.timeout); + g_return_if_fail(!priv->gw_ping.pid); + g_return_if_fail(priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE + || priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE); + + connection = nm_device_get_applied_connection(self); + g_assert(connection); + + s_con = nm_connection_get_setting_connection(connection); + g_assert(s_con); + timeout = nm_setting_connection_get_gateway_ping_timeout(s_con); + + buf[0] = '\0'; + if (timeout) { + const NMPObject *gw; + + if (priv->ip_config_4 && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) { + gw = nm_ip4_config_best_default_route_get(priv->ip_config_4); + if (gw) { + _nm_utils_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf); + ping_binary = nm_utils_find_helper("ping", "/usr/bin/ping", NULL); + log_domain = LOGD_IP4; + } + } else if (priv->ip_config_6 && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) { + gw = nm_ip6_config_best_default_route_get(priv->ip_config_6); + if (gw) { + _nm_utils_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf); + ping_binary = nm_utils_find_helper("ping6", "/usr/bin/ping6", NULL); + log_domain = LOGD_IP6; + } + } + } + + if (buf[0]) + start_ping(self, log_domain, ping_binary, buf, timeout); + + /* If no ping was started, just advance to pre_up */ + if (!priv->gw_ping.pid) + ip_check_pre_up(self); +} + +/*****************************************************************************/ + +static gboolean +carrier_wait_timeout(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->carrier_wait_id = 0; + nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE); + if (!priv->carrier) + _carrier_wait_check_queued_act_request(self); + return G_SOURCE_REMOVE; +} + +static gboolean +nm_device_is_up(NMDevice *self) +{ + int ifindex; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + ifindex = nm_device_get_ip_ifindex(self); + return ifindex > 0 ? nm_platform_link_is_up(nm_device_get_platform(self), ifindex) : TRUE; +} + +static gint64 +_get_carrier_wait_ms(NMDevice *self) +{ + gs_free char *value = NULL; + + value = nm_config_data_get_device_config(NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT, + self, + NULL); + return _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXINT32, CARRIER_WAIT_TIME_MS); +} + +gboolean +nm_device_bring_up(NMDevice *self, gboolean block, gboolean *no_firmware) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + gboolean device_is_up = FALSE; + NMDeviceCapabilities capabilities; + int ifindex; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + NM_SET_OUT(no_firmware, FALSE); + + if (!nm_device_get_enabled(self)) { + _LOGD(LOGD_PLATFORM, "bringing up device ignored due to disabled"); + return FALSE; + } + + ifindex = nm_device_get_ip_ifindex(self); + _LOGD(LOGD_PLATFORM, "bringing up device %d", ifindex); + if (ifindex <= 0) { + /* assume success. */ + } else { + if (!nm_platform_link_set_up(nm_device_get_platform(self), ifindex, no_firmware)) + return FALSE; + } + + /* Store carrier immediately. */ + nm_device_set_carrier_from_platform(self); + + device_is_up = nm_device_is_up(self); + if (block && !device_is_up) { + gint64 wait_until = nm_utils_get_monotonic_timestamp_usec() + 10000 /* microseconds */; + + do { + g_usleep(200); + if (!nm_platform_link_refresh(nm_device_get_platform(self), ifindex)) + return FALSE; + device_is_up = nm_device_is_up(self); + } while (!device_is_up && nm_utils_get_monotonic_timestamp_usec() < wait_until); + } + + if (!device_is_up) { + if (block) + _LOGW(LOGD_PLATFORM, "device not up after timeout!"); + else + _LOGD(LOGD_PLATFORM, "device not up immediately"); + return FALSE; + } + + /* some ethernet devices fail to report capabilities unless the device + * is up. Re-read the capabilities. */ + capabilities = 0; + if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities) + capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self); + _add_capabilities(self, capabilities); + + /* Devices that support carrier detect must be IFF_UP to report carrier + * changes; so after setting the device IFF_UP we must suppress startup + * complete (via a pending action) until either the carrier turns on, or + * a timeout is reached. + */ + if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)) { + gint64 now_ms, until_ms; + + /* we start a grace period of 5 seconds during which we will schedule + * a pending action whenever we have no carrier. + * + * If during that time carrier goes away, we declare the interface + * as not ready. */ + nm_clear_g_source(&priv->carrier_wait_id); + if (!priv->carrier) + nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE); + + now_ms = nm_utils_get_monotonic_timestamp_msec(); + until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms); + priv->carrier_wait_id = g_timeout_add(until_ms - now_ms, carrier_wait_timeout, self); + } + + /* Can only get HW address of some devices when they are up */ + nm_device_update_hw_address(self); + + /* when the link comes up, we must restore IP configuration if necessary. */ + if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) { + if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) + _LOGW(LOGD_IP4, "failed applying IP4 config after bringing link up"); + } + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) { + if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) + _LOGW(LOGD_IP6, "failed applying IP6 config after bringing link up"); + } + + return TRUE; +} + +void +nm_device_take_down(NMDevice *self, gboolean block) +{ + int ifindex; + gboolean device_is_up; + + g_return_if_fail(NM_IS_DEVICE(self)); + + ifindex = nm_device_get_ip_ifindex(self); + _LOGD(LOGD_PLATFORM, "taking down device %d", ifindex); + if (ifindex <= 0) { + /* devices without ifindex are always up. */ + return; + } + + if (!nm_platform_link_set_down(nm_device_get_platform(self), ifindex)) + return; + + device_is_up = nm_device_is_up(self); + if (block && device_is_up) { + gint64 wait_until = nm_utils_get_monotonic_timestamp_usec() + 10000 /* microseconds */; + + do { + g_usleep(200); + if (!nm_platform_link_refresh(nm_device_get_platform(self), ifindex)) + return; + device_is_up = nm_device_is_up(self); + } while (device_is_up && nm_utils_get_monotonic_timestamp_usec() < wait_until); + } + + if (device_is_up) { + if (block) + _LOGW(LOGD_PLATFORM, "device not down after timeout!"); + else + _LOGD(LOGD_PLATFORM, "device not down immediately"); + } +} + +void +nm_device_set_firmware_missing(NMDevice *self, gboolean new_missing) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + if (priv->firmware_missing != new_missing) { + priv->firmware_missing = new_missing; + _notify(self, PROP_FIRMWARE_MISSING); + } +} + +gboolean +nm_device_get_firmware_missing(NMDevice *self) +{ + return NM_DEVICE_GET_PRIVATE(self)->firmware_missing; +} + +static void +intersect_ext_config(NMDevice * self, + AppliedConfig *config, + gboolean intersect_addresses, + gboolean intersect_routes) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMIPConfig * ext; + guint32 penalty; + int family; + + if (!config->orig) + return; + + family = nm_ip_config_get_addr_family(config->orig); + penalty = default_route_metric_penalty_get(self, family); + ext = family == AF_INET ? (NMIPConfig *) priv->ext_ip_config_4 + : (NMIPConfig *) priv->ext_ip_config_6; + + if (config->current) { + nm_ip_config_intersect(config->current, + ext, + intersect_addresses, + intersect_routes, + penalty); + } else { + config->current = nm_ip_config_intersect_alloc(config->orig, + ext, + intersect_addresses, + intersect_routes, + penalty); + } +} + +static gboolean +update_ext_ip_config(NMDevice *self, int addr_family, gboolean intersect_configs) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + int ifindex; + GSList * iter; + gboolean is_up; + + nm_assert_addr_family(addr_family); + + ifindex = nm_device_get_ip_ifindex(self); + if (!ifindex) + return FALSE; + + is_up = nm_platform_link_is_up(nm_device_get_platform(self), ifindex); + + if (NM_IS_IPv4(addr_family)) { + g_clear_object(&priv->ext_ip_config_4); + priv->ext_ip_config_4 = nm_ip4_config_capture(nm_device_get_multi_index(self), + nm_device_get_platform(self), + ifindex); + if (priv->ext_ip_config_4) { + if (intersect_configs) { + /* This function was called upon external changes. Remove the configuration + * (addresses,routes) that is no longer present externally from the internal + * config. This way, we don't re-add addresses that were manually removed + * by the user. */ + if (priv->con_ip_config_4) { + nm_ip4_config_intersect(priv->con_ip_config_4, + priv->ext_ip_config_4, + TRUE, + is_up, + default_route_metric_penalty_get(self, AF_INET)); + } + + intersect_ext_config(self, &priv->dev_ip_config_4, TRUE, is_up); + intersect_ext_config(self, &priv->dev2_ip_config_4, TRUE, is_up); + + for (iter = priv->vpn_configs_4; iter; iter = iter->next) + nm_ip4_config_intersect(iter->data, priv->ext_ip_config_4, TRUE, is_up, 0); + } + + /* Remove parts from ext_ip_config_4 to only contain the information that + * was configured externally -- we already have the same configuration from + * internal origins. */ + if (priv->con_ip_config_4) { + nm_ip4_config_subtract(priv->ext_ip_config_4, + priv->con_ip_config_4, + default_route_metric_penalty_get(self, AF_INET)); + } + if (applied_config_get_current(&priv->dev_ip_config_4)) { + nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_4, + applied_config_get_current(&priv->dev_ip_config_4), + default_route_metric_penalty_get(self, AF_INET)); + } + if (applied_config_get_current(&priv->dev2_ip_config_4)) { + nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_4, + applied_config_get_current(&priv->dev2_ip_config_4), + default_route_metric_penalty_get(self, AF_INET)); + } + for (iter = priv->vpn_configs_4; iter; iter = iter->next) + nm_ip4_config_subtract(priv->ext_ip_config_4, iter->data, 0); + } + + } else { + nm_assert(!NM_IS_IPv4(addr_family)); + + g_clear_object(&priv->ext_ip_config_6); + g_clear_object(&priv->ext_ip6_config_captured); + priv->ext_ip6_config_captured = + nm_ip6_config_capture(nm_device_get_multi_index(self), + nm_device_get_platform(self), + ifindex, + NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN); + if (priv->ext_ip6_config_captured) { + priv->ext_ip_config_6 = nm_ip6_config_new_cloned(priv->ext_ip6_config_captured); + + if (intersect_configs) { + /* This function was called upon external changes. Remove the configuration + * (addresses,routes) that is no longer present externally from the internal + * config. This way, we don't re-add addresses that were manually removed + * by the user. */ + if (priv->con_ip_config_6) { + nm_ip6_config_intersect(priv->con_ip_config_6, + priv->ext_ip_config_6, + is_up, + is_up, + default_route_metric_penalty_get(self, AF_INET6)); + } + + intersect_ext_config(self, &priv->ac_ip6_config, is_up, is_up); + intersect_ext_config(self, &priv->dhcp6.ip6_config, is_up, is_up); + intersect_ext_config(self, &priv->dev2_ip_config_6, is_up, is_up); + + for (iter = priv->vpn_configs_6; iter; iter = iter->next) + nm_ip6_config_intersect(iter->data, priv->ext_ip_config_6, is_up, is_up, 0); + + if (is_up && priv->ipv6ll_has + && !nm_ip6_config_lookup_address(priv->ext_ip_config_6, &priv->ipv6ll_addr)) + priv->ipv6ll_has = FALSE; + } + + /* Remove parts from ext_ip_config_6 to only contain the information that + * was configured externally -- we already have the same configuration from + * internal origins. */ + if (priv->con_ip_config_6) { + nm_ip6_config_subtract(priv->ext_ip_config_6, + priv->con_ip_config_6, + default_route_metric_penalty_get(self, AF_INET6)); + } + if (applied_config_get_current(&priv->ac_ip6_config)) { + nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6, + applied_config_get_current(&priv->ac_ip6_config), + default_route_metric_penalty_get(self, AF_INET6)); + } + if (applied_config_get_current(&priv->dhcp6.ip6_config)) { + nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6, + applied_config_get_current(&priv->dhcp6.ip6_config), + default_route_metric_penalty_get(self, AF_INET6)); + } + if (applied_config_get_current(&priv->dev2_ip_config_6)) { + nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6, + applied_config_get_current(&priv->dev2_ip_config_6), + default_route_metric_penalty_get(self, AF_INET6)); + } + for (iter = priv->vpn_configs_6; iter; iter = iter->next) + nm_ip6_config_subtract(priv->ext_ip_config_6, iter->data, 0); + } + } + + return TRUE; +} + +static void +update_ip_config(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert_addr_family(addr_family); + + if (NM_IS_IPv4(addr_family)) + priv->update_ip_config_completed_v4 = TRUE; + else + priv->update_ip_config_completed_v6 = TRUE; + + if (update_ext_ip_config(self, addr_family, TRUE)) { + if (NM_IS_IPv4(addr_family)) { + if (priv->ext_ip_config_4) + ip_config_merge_and_apply(self, AF_INET, FALSE); + } else { + if (priv->ext_ip6_config_captured) + ip_config_merge_and_apply(self, AF_INET6, FALSE); + } + } +} + +void +nm_device_capture_initial_config(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->update_ip_config_completed_v4) + update_ip_config(self, AF_INET); + if (!priv->update_ip_config_completed_v6) + update_ip_config(self, AF_INET6); +} + +static gboolean +queued_ip_config_change(NMDevice *self, int addr_family) +{ + NMDevicePrivate *priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + /* Wait for any queued state changes */ + if (priv->queued_state.id) + return G_SOURCE_CONTINUE; + + /* If a commit is scheduled, this function would potentially interfere with + * it changing IP configurations before they are applied. Postpone the + * update in such case. + */ + if (priv->activation_source_id_x[IS_IPv4] != 0 + && priv->activation_source_func_x[IS_IPv4] + == activate_stage5_ip_config_result_x_fcn(addr_family)) + return G_SOURCE_CONTINUE; + + priv->queued_ip_config_id_x[IS_IPv4] = 0; + + update_ip_config(self, addr_family); + + if (!IS_IPv4) { + /* Check whether we need to complete waiting for link-local. + * We are also called from an idle handler, so no problem doing state transitions + * now. */ + linklocal6_check_complete(self); + } + + if (!IS_IPv4) { + NMPlatform * platform; + GSList * dad6_failed_addrs, *iter; + const NMPlatformLink *pllink; + + dad6_failed_addrs = g_steal_pointer(&priv->dad6_failed_addrs); + + if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state < NM_DEVICE_STATE_DEACTIVATING + && priv->ifindex > 0 && !nm_device_sys_iface_state_is_external(self) + && (platform = nm_device_get_platform(self)) + && (pllink = nm_platform_link_get(platform, priv->ifindex)) + && (pllink->n_ifi_flags & IFF_UP)) { + gboolean need_ipv6ll = FALSE; + NMNDiscConfigMap ndisc_config_changed = NM_NDISC_CONFIG_NONE; + + /* Handle DAD failures */ + for (iter = dad6_failed_addrs; iter; iter = iter->next) { + const NMPObject * obj = iter->data; + const NMPlatformIP6Address *addr; + + if (!nm_ndisc_dad_addr_is_fail_candidate(platform, obj)) + continue; + + addr = NMP_OBJECT_CAST_IP6_ADDRESS(obj); + + _LOGI(LOGD_IP6, + "ipv6: duplicate address check failed for the %s address", + nm_platform_ip6_address_to_string(addr, NULL, 0)); + + if (IN6_IS_ADDR_LINKLOCAL(&addr->address)) + need_ipv6ll = TRUE; + else if (priv->ndisc) + ndisc_config_changed |= nm_ndisc_dad_failed(priv->ndisc, &addr->address, FALSE); + } + + if (ndisc_config_changed != NM_NDISC_CONFIG_NONE) + nm_ndisc_emit_config_change(priv->ndisc, ndisc_config_changed); + + /* If no IPv6 link-local address exists but other addresses do then we + * must add the LL address to remain conformant with RFC 3513 chapter 2.1 + * ("Addressing Model"): "All interfaces are required to have at least + * one link-local unicast address". + */ + if (priv->ip_config_6 && nm_ip6_config_get_num_addresses(priv->ip_config_6)) + need_ipv6ll = TRUE; + if (need_ipv6ll) + check_and_add_ipv6ll_addr(self); + } + + g_slist_free_full(dad6_failed_addrs, (GDestroyNotify) nmp_object_unref); + } + + if (!IS_IPv4) { + /* Check if DAD is still pending */ + if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF && priv->dad6_ip6_config + && priv->ext_ip6_config_captured + && !nm_ip6_config_has_any_dad_pending(priv->ext_ip6_config_captured, + priv->dad6_ip6_config)) { + _LOGD(LOGD_DEVICE | LOGD_IP6, "IPv6 DAD terminated"); + g_clear_object(&priv->dad6_ip6_config); + _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE); + check_ip_state(self, FALSE, TRUE); + if (priv->rt6_temporary_not_available) + nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL); + } + } + + set_unmanaged_external_down(self, TRUE); + + return G_SOURCE_REMOVE; +} + +static gboolean +queued_ip4_config_change(gpointer user_data) +{ + return queued_ip_config_change(user_data, AF_INET); +} + +static gboolean +queued_ip6_config_change(gpointer user_data) +{ + return queued_ip_config_change(user_data, AF_INET6); +} + +static void +device_ipx_changed(NMPlatform * platform, + int obj_type_i, + int ifindex, + gconstpointer platform_object, + int change_type_i, + NMDevice * self) +{ + const NMPObjectType obj_type = obj_type_i; + const NMPlatformSignalChangeType change_type = change_type_i; + NMDevicePrivate * priv; + const NMPlatformIP6Address * addr; + + if (nm_device_get_ip_ifindex(self) != ifindex) + return; + + if (!nm_device_is_real(self)) + return; + + if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) { + /* ignore all platform signals until the link is initialized in platform. */ + return; + } + + priv = NM_DEVICE_GET_PRIVATE(self); + + switch (obj_type) { + case NMP_OBJECT_TYPE_IP4_ADDRESS: + case NMP_OBJECT_TYPE_IP4_ROUTE: + if (!priv->queued_ip_config_id_4) { + priv->queued_ip_config_id_4 = g_idle_add(queued_ip4_config_change, self); + _LOGD(LOGD_DEVICE, "queued IP4 config change"); + } + break; + case NMP_OBJECT_TYPE_IP6_ADDRESS: + addr = platform_object; + + if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state < NM_DEVICE_STATE_DEACTIVATING + && nm_ndisc_dad_addr_is_fail_candidate_event(change_type, addr)) { + priv->dad6_failed_addrs = + g_slist_prepend(priv->dad6_failed_addrs, + (gpointer) nmp_object_ref(NMP_OBJECT_UP_CAST(addr))); + } + + /* fall-through */ + case NMP_OBJECT_TYPE_IP6_ROUTE: + if (!priv->queued_ip_config_id_6) { + priv->queued_ip_config_id_6 = g_idle_add(queued_ip6_config_change, self); + _LOGD(LOGD_DEVICE, "queued IP6 config change"); + } + break; + default: + g_return_if_reached(); + } +} + +/*****************************************************************************/ + +NM_UTILS_FLAGS2STR_DEFINE(nm_unmanaged_flags2str, + NMUnmanagedFlags, + NM_UTILS_FLAGS2STR(NM_UNMANAGED_SLEEPING, "sleeping"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_QUITTING, "quitting"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_PARENT, "parent"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_TYPE, "by-type"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_PLATFORM_INIT, "platform-init"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_EXPLICIT, "user-explicit"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_DEFAULT, "by-default"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_SETTINGS, "user-settings"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_CONF, "user-conf"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_UDEV, "user-udev"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_EXTERNAL_DOWN, "external-down"), + NM_UTILS_FLAGS2STR(NM_UNMANAGED_IS_SLAVE, "is-slave"), ); + +static const char * +_unmanaged_flags2str(NMUnmanagedFlags flags, NMUnmanagedFlags mask, char *buf, gsize len) +{ + char buf2[512]; + char *b; + char *tmp, *tmp2; + gsize l; + + nm_utils_to_string_buffer_init(&buf, &len); + if (!len) + return buf; + + b = buf; + + mask |= flags; + + nm_unmanaged_flags2str(flags, b, len); + l = strlen(b); + b += l; + len -= l; + + nm_unmanaged_flags2str(mask & ~flags, buf2, sizeof(buf2)); + if (buf2[0]) { + gboolean add_separator = l > 0; + + tmp = buf2; + while (TRUE) { + if (add_separator) + nm_utils_strbuf_append_c(&b, &len, ','); + add_separator = TRUE; + + tmp2 = strchr(tmp, ','); + if (tmp2) + tmp2[0] = '\0'; + + nm_utils_strbuf_append_c(&b, &len, '!'); + nm_utils_strbuf_append_str(&b, &len, tmp); + if (!tmp2) + break; + + tmp = &tmp2[1]; + } + } + + return buf; +} + +static gboolean +_get_managed_by_flags(NMUnmanagedFlags flags, NMUnmanagedFlags mask, gboolean for_user_request) +{ + /* Evaluate the managed state based on the unmanaged flags. + * + * Some flags are authoritative, meaning they always cause + * the device to be unmanaged (e.g. @NM_UNMANAGED_PLATFORM_INIT). + * + * OTOH, some flags can be overwritten. For example NM_UNMANAGED_USER_UDEV + * is ignored once NM_UNMANAGED_USER_EXPLICIT is set. The idea is that + * the flag from the configuration has no effect once the user explicitly + * touches the unmanaged flags. */ + + if (for_user_request) { + /* @for_user_request can make the result only ~more~ managed. + * If the flags already indicate a managed state for a non-user-request, + * then it is also managed for an explicit user-request. + * + * Effectively, this check is redundant, as the code below already + * already ensures that. Still, express this invariant explicitly here. */ + if (_get_managed_by_flags(flags, mask, FALSE)) + return TRUE; + + /* A for-user-request, is effectively the same as pretending + * that user-explicit flag is cleared. */ + mask |= NM_UNMANAGED_USER_EXPLICIT; + flags &= ~NM_UNMANAGED_USER_EXPLICIT; + } + + if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_SETTINGS) + && !NM_FLAGS_ANY(flags, NM_UNMANAGED_USER_SETTINGS)) { + /* NM_UNMANAGED_USER_SETTINGS can only explicitly unmanage a device. It cannot + * *manage* it. Having NM_UNMANAGED_USER_SETTINGS explicitly not set, is the + * same as having it not set at all. */ + mask &= ~NM_UNMANAGED_USER_SETTINGS; + } + + if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_UDEV)) { + /* configuration from udev or nm-config overwrites the by-default flag + * which is based on the device type. + * configuration from udev overwrites external-down */ + flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_EXTERNAL_DOWN); + } + + if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_CONF)) { + /* configuration from NetworkManager.conf overwrites the by-default flag + * which is based on the device type. + * It also overwrites the udev configuration and external-down */ + flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_EXTERNAL_DOWN); + } + + if (NM_FLAGS_HAS(mask, NM_UNMANAGED_IS_SLAVE) && !NM_FLAGS_HAS(flags, NM_UNMANAGED_IS_SLAVE)) { + /* for an enslaved device, by-default doesn't matter */ + flags &= ~NM_UNMANAGED_BY_DEFAULT; + } + + if (NM_FLAGS_HAS(mask, NM_UNMANAGED_USER_EXPLICIT)) { + /* if the device is managed by user-decision, certain other flags + * are ignored. */ + flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_CONF + | NM_UNMANAGED_EXTERNAL_DOWN); + } + + return flags == NM_UNMANAGED_NONE; +} + +/** + * nm_device_get_managed: + * @self: the #NMDevice + * @for_user_request: whether to check the flags for an explicit user-request + * Setting this to %TRUE has the same effect as if %NM_UNMANAGED_USER_EXPLICIT + * unmanaged flag would be unset (meaning: explicitly not-unmanaged). + * If this parameter is %TRUE, the device can only appear more managed. + * + * Whether the device is unmanaged according to the unmanaged flags. + * + * Returns: %TRUE if the device is unmanaged because of the flags. + */ +gboolean +nm_device_get_managed(NMDevice *self, gboolean for_user_request) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + if (!nm_device_is_real(self)) { + /* a unrealized device is always considered unmanaged. */ + return FALSE; + } + + priv = NM_DEVICE_GET_PRIVATE(self); + + return _get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, for_user_request); +} + +/** + * nm_device_get_unmanaged_mask: + * @self: the #NMDevice + * @flag: the unmanaged flags to check. + * + * Return the unmanaged flags mask set on this device. + * + * Returns: the flags of the device ( & @flag) + */ +NMUnmanagedFlags +nm_device_get_unmanaged_mask(NMDevice *self, NMUnmanagedFlags flag) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_UNMANAGED_NONE); + g_return_val_if_fail(flag != NM_UNMANAGED_NONE, NM_UNMANAGED_NONE); + + return NM_DEVICE_GET_PRIVATE(self)->unmanaged_mask & flag; +} + +/** + * nm_device_get_unmanaged_flags: + * @self: the #NMDevice + * @flag: the unmanaged flags to check. + * + * Return the unmanaged flags of the device. + * + * Returns: the flags of the device ( & @flag) + */ +NMUnmanagedFlags +nm_device_get_unmanaged_flags(NMDevice *self, NMUnmanagedFlags flag) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_UNMANAGED_NONE); + g_return_val_if_fail(flag != NM_UNMANAGED_NONE, NM_UNMANAGED_NONE); + + return NM_DEVICE_GET_PRIVATE(self)->unmanaged_flags & flag; +} + +/** + * _set_unmanaged_flags: + * @self: the #NMDevice instance + * @flags: which #NMUnmanagedFlags to set. + * @set_op: whether to set/clear/forget the flags. You can also pass + * boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED + * and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively. + * @allow_state_transition: if %FALSE, setting flags never triggers a device + * state change. If %TRUE, the device can change state, if it is real and + * switches from managed to unmanaged (or vice versa). + * @now: whether the state change should be immediate or delayed + * @reason: the device state reason passed to nm_device_state_changed() if + * the device becomes managed/unmanaged. This is only relevant if the + * device switches state and if @allow_state_transition is %TRUE. + * + * Set the unmanaged flags of the device. + **/ +static void +_set_unmanaged_flags(NMDevice * self, + NMUnmanagedFlags flags, + NMUnmanFlagOp set_op, + gboolean allow_state_transition, + gboolean now, + NMDeviceStateReason reason) +{ + NMDevicePrivate *priv; + gboolean was_managed, transition_state; + NMUnmanagedFlags old_flags, old_mask; + NMDeviceState new_state; + const char * operation = NULL; + char str1[512]; + char str2[512]; + gboolean do_notify_has_pending_actions = FALSE; + gboolean had_pending_actions = FALSE; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(flags); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->real) + allow_state_transition = FALSE; + was_managed = allow_state_transition && nm_device_get_managed(self, FALSE); + + if (NM_FLAGS_HAS(priv->unmanaged_flags, NM_UNMANAGED_PLATFORM_INIT) + && NM_FLAGS_HAS(flags, NM_UNMANAGED_PLATFORM_INIT) + && NM_IN_SET(set_op, NM_UNMAN_FLAG_OP_SET_MANAGED)) { + /* we are clearing the platform-init flags. This triggers additional actions. */ + if (!NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_SETTINGS)) { + gboolean unmanaged; + + unmanaged = nm_device_spec_match_list( + self, + nm_settings_get_unmanaged_specs(NM_DEVICE_GET_PRIVATE(self)->settings)); + nm_device_set_unmanaged_flags(self, NM_UNMANAGED_USER_SETTINGS, !!unmanaged); + } + + /* trigger an initial update of IP configuration. */ + nm_assert_se(!nm_clear_g_source(&priv->queued_ip_config_id_4)); + nm_assert_se(!nm_clear_g_source(&priv->queued_ip_config_id_6)); + priv->queued_ip_config_id_4 = g_idle_add(queued_ip4_config_change, self); + priv->queued_ip_config_id_6 = g_idle_add(queued_ip6_config_change, self); + + if (!priv->pending_actions) { + do_notify_has_pending_actions = TRUE; + had_pending_actions = nm_device_has_pending_action(self); + } + } + + old_flags = priv->unmanaged_flags; + old_mask = priv->unmanaged_mask; + + switch (set_op) { + case NM_UNMAN_FLAG_OP_FORGET: + priv->unmanaged_mask &= ~flags; + priv->unmanaged_flags &= ~flags; + operation = "forget"; + break; + case NM_UNMAN_FLAG_OP_SET_UNMANAGED: + priv->unmanaged_mask |= flags; + priv->unmanaged_flags |= flags; + operation = "set-unmanaged"; + break; + case NM_UNMAN_FLAG_OP_SET_MANAGED: + priv->unmanaged_mask |= flags; + priv->unmanaged_flags &= ~flags; + operation = "set-managed"; + break; + default: + g_return_if_reached(); + } + + if (old_flags == priv->unmanaged_flags && old_mask == priv->unmanaged_mask) + return; + + transition_state = + allow_state_transition && was_managed != nm_device_get_managed(self, FALSE) + && (was_managed + || (!was_managed && nm_device_get_state(self) == NM_DEVICE_STATE_UNMANAGED)); + + _LOGD(LOGD_DEVICE, + "unmanaged: flags set to [%s%s0x%0x/0x%x/%s%s], %s [%s=0x%0x]%s%s%s)", + _unmanaged_flags2str(priv->unmanaged_flags, priv->unmanaged_mask, str1, sizeof(str1)), + (priv->unmanaged_flags | priv->unmanaged_mask) ? "=" : "", + (guint) priv->unmanaged_flags, + (guint) priv->unmanaged_mask, + (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE) + ? "managed" + : (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, TRUE) + ? "manageable" + : "unmanaged")), + priv->real ? "" : "/unrealized", + operation, + nm_unmanaged_flags2str(flags, str2, sizeof(str2)), + flags, + NM_PRINT_FMT_QUOTED(allow_state_transition, + ", reason ", + reason_to_string_a(reason), + transition_state ? ", transition-state" : "", + "")); + + if (do_notify_has_pending_actions && had_pending_actions != nm_device_has_pending_action(self)) + _notify(self, PROP_HAS_PENDING_ACTION); + + if (transition_state) { + new_state = was_managed ? NM_DEVICE_STATE_UNMANAGED : NM_DEVICE_STATE_UNAVAILABLE; + if (now) + nm_device_state_changed(self, new_state, reason); + else + nm_device_queue_state(self, new_state, reason); + } +} + +/** + * @self: the #NMDevice instance + * @flags: which #NMUnmanagedFlags to set. + * @set_op: whether to set/clear/forget the flags. You can also pass + * boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED + * and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively. + * + * Set the unmanaged flags of the device (does not trigger a state change). + **/ +void +nm_device_set_unmanaged_flags(NMDevice *self, NMUnmanagedFlags flags, NMUnmanFlagOp set_op) +{ + _set_unmanaged_flags(self, flags, set_op, FALSE, FALSE, NM_DEVICE_STATE_REASON_NONE); +} + +/** + * nm_device_set_unmanaged_by_flags: + * @self: the #NMDevice instance + * @flags: which #NMUnmanagedFlags to set. + * @set_op: whether to set/clear/forget the flags. You can also pass + * boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED + * and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively. + * @reason: the device state reason passed to nm_device_state_changed() if + * the device becomes managed/unmanaged. + * + * Set the unmanaged flags of the device and possibly trigger a state change. + **/ +void +nm_device_set_unmanaged_by_flags(NMDevice * self, + NMUnmanagedFlags flags, + NMUnmanFlagOp set_op, + NMDeviceStateReason reason) +{ + _set_unmanaged_flags(self, flags, set_op, TRUE, TRUE, reason); +} + +void +nm_device_set_unmanaged_by_flags_queue(NMDevice * self, + NMUnmanagedFlags flags, + NMUnmanFlagOp set_op, + NMDeviceStateReason reason) +{ + _set_unmanaged_flags(self, flags, set_op, TRUE, FALSE, reason); +} + +/** + * nm_device_check_unrealized_device_managed: + * + * Checks if a unrealized device is managed from user settings + * or user configuration. + */ +gboolean +nm_device_check_unrealized_device_managed(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(!nm_device_is_real(self)); + + if (!nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED, + self, + TRUE, + TRUE)) + return FALSE; + + if (nm_device_spec_match_list(self, nm_settings_get_unmanaged_specs(priv->settings))) + return FALSE; + + return TRUE; +} + +void +nm_device_set_unmanaged_by_user_settings(NMDevice *self) +{ + gboolean unmanaged; + + g_return_if_fail(NM_IS_DEVICE(self)); + + if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) { + /* the device is already unmanaged due to platform-init. + * + * We want to delay evaluating the device spec, because it will freeze + * the permanent MAC address. That should not be done, before the platform + * link is fully initialized (via UDEV). + * + * Note that when clearing NM_UNMANAGED_PLATFORM_INIT, we will re-evaluate + * whether the device is unmanaged by user-settings. */ + return; + } + + unmanaged = nm_device_spec_match_list( + self, + nm_settings_get_unmanaged_specs(NM_DEVICE_GET_PRIVATE(self)->settings)); + + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_USER_SETTINGS, + !!unmanaged, + unmanaged ? NM_DEVICE_STATE_REASON_NOW_UNMANAGED + : NM_DEVICE_STATE_REASON_NOW_MANAGED); +} + +void +nm_device_set_unmanaged_by_user_udev(NMDevice *self) +{ + int ifindex; + gboolean platform_unmanaged = FALSE; + + ifindex = self->_priv->ifindex; + + if (ifindex <= 0 + || !nm_platform_link_get_unmanaged(nm_device_get_platform(self), + ifindex, + &platform_unmanaged)) + return; + + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_USER_UDEV, + platform_unmanaged, + NM_DEVICE_STATE_REASON_USER_REQUESTED); +} + +void +nm_device_set_unmanaged_by_user_conf(NMDevice *self) +{ + gboolean value; + NMUnmanFlagOp set_op; + + value = nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED, + self, + -1, + TRUE); + switch (value) { + case TRUE: + set_op = NM_UNMAN_FLAG_OP_SET_MANAGED; + break; + case FALSE: + set_op = NM_UNMAN_FLAG_OP_SET_UNMANAGED; + break; + default: + set_op = NM_UNMAN_FLAG_OP_FORGET; + break; + } + + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_USER_CONF, + set_op, + NM_DEVICE_STATE_REASON_USER_REQUESTED); +} + +void +nm_device_set_unmanaged_by_quitting(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + gboolean need_deactivate = + nm_device_is_activating(self) || priv->state == NM_DEVICE_STATE_ACTIVATED; + + /* It's OK to block here because we're quitting */ + if (need_deactivate) + _set_state_full(self, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_NOW_UNMANAGED, + TRUE); + + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_QUITTING, + TRUE, + need_deactivate ? NM_DEVICE_STATE_REASON_REMOVED + : NM_DEVICE_STATE_REASON_NOW_UNMANAGED); +} + +/*****************************************************************************/ + +void +nm_device_set_dhcp_anycast_address(NMDevice *self, const char *addr) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(!addr || nm_utils_hwaddr_valid(addr, ETH_ALEN)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + g_free(priv->dhcp_anycast_address); + priv->dhcp_anycast_address = g_strdup(addr); +} + +void +nm_device_reapply_settings_immediately(NMDevice *self) +{ + NMConnection * applied_connection; + NMSettingsConnection *settings_connection; + NMDeviceState state; + NMSettingConnection * s_con_settings; + NMSettingConnection * s_con_applied; + const char * zone; + NMMetered metered; + guint64 version_id; + + g_return_if_fail(NM_IS_DEVICE(self)); + + state = nm_device_get_state(self); + if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED) + return; + + applied_connection = nm_device_get_applied_connection(self); + settings_connection = nm_device_get_settings_connection(self); + + if (!nm_settings_connection_has_unmodified_applied_connection( + settings_connection, + applied_connection, + NM_SETTING_COMPARE_FLAG_IGNORE_REAPPLY_IMMEDIATELY)) + return; + + s_con_settings = nm_connection_get_setting_connection( + nm_settings_connection_get_connection(settings_connection)); + s_con_applied = nm_connection_get_setting_connection(applied_connection); + + if (!nm_streq0((zone = nm_setting_connection_get_zone(s_con_settings)), + nm_setting_connection_get_zone(s_con_applied))) { + version_id = nm_active_connection_version_id_bump( + (NMActiveConnection *) self->_priv->act_request.obj); + _LOGD(LOGD_DEVICE, + "reapply setting: zone = %s%s%s (version-id %llu)", + NM_PRINT_FMT_QUOTE_STRING(zone), + (unsigned long long) version_id); + + g_object_set(G_OBJECT(s_con_applied), NM_SETTING_CONNECTION_ZONE, zone, NULL); + + nm_device_update_firewall_zone(self); + } + + if ((metered = nm_setting_connection_get_metered(s_con_settings)) + != nm_setting_connection_get_metered(s_con_applied)) { + version_id = nm_active_connection_version_id_bump( + (NMActiveConnection *) self->_priv->act_request.obj); + _LOGD(LOGD_DEVICE, + "reapply setting: metered = %d (version-id %llu)", + (int) metered, + (unsigned long long) version_id); + + g_object_set(G_OBJECT(s_con_applied), NM_SETTING_CONNECTION_METERED, metered, NULL); + + nm_device_update_metered(self); + } +} + +void +nm_device_update_firewall_zone(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->fw_state >= FIREWALL_STATE_INITIALIZED + && !nm_device_sys_iface_state_is_external(self)) + fw_change_zone(self); +} + +void +nm_device_update_metered(NMDevice *self) +{ +#define NM_METERED_INVALID ((NMMetered) -1) + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingConnection *setting; + NMMetered conn_value, value = NM_METERED_INVALID; + NMConnection * connection = NULL; + NMDeviceState state; + + g_return_if_fail(NM_IS_DEVICE(self)); + + state = nm_device_get_state(self); + if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED) + value = NM_METERED_UNKNOWN; + + if (value == NM_METERED_INVALID) { + connection = nm_device_get_applied_connection(self); + if (connection) { + setting = nm_connection_get_setting_connection(connection); + if (setting) { + conn_value = nm_setting_connection_get_metered(setting); + if (conn_value != NM_METERED_UNKNOWN) + value = conn_value; + } + } + } + + if (value == NM_METERED_INVALID && NM_DEVICE_GET_CLASS(self)->get_guessed_metered + && NM_DEVICE_GET_CLASS(self)->get_guessed_metered(self)) + value = NM_METERED_GUESS_YES; + + /* Try to guess a value using the metered flag in IP configuration */ + if (value == NM_METERED_INVALID) { + if (priv->ip_config_4 && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE + && nm_ip4_config_get_metered(priv->ip_config_4)) + value = NM_METERED_GUESS_YES; + } + + /* Otherwise, look at connection type. For Bluetooth, we look at the type of + * Bluetooth sharing: for PANU/DUN (where we are receiving internet from + * another device) we set GUESS_YES; for NAP (where we are sharing internet + * to another device) we set GUESS_NO. We ignore WiMAX here as it’s no + * longer supported by NetworkManager. */ + if (value == NM_METERED_INVALID + && nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) { + if (_nm_connection_get_setting_bluetooth_for_nap(connection)) { + /* NAP types are not metered, but other types are. */ + value = NM_METERED_GUESS_NO; + } else + value = NM_METERED_GUESS_YES; + } + + if (value == NM_METERED_INVALID) { + if (nm_connection_is_type(connection, NM_SETTING_GSM_SETTING_NAME) + || nm_connection_is_type(connection, NM_SETTING_CDMA_SETTING_NAME)) + value = NM_METERED_GUESS_YES; + else + value = NM_METERED_GUESS_NO; + } + + if (value != priv->metered) { + _LOGD(LOGD_DEVICE, "set metered value %d", value); + priv->metered = value; + _notify(self, PROP_METERED); + } +} + +static NMDeviceCheckDevAvailableFlags +_device_check_dev_available_flags_from_con(NMDeviceCheckConAvailableFlags con_flags) +{ + NMDeviceCheckDevAvailableFlags dev_flags; + + dev_flags = NM_DEVICE_CHECK_DEV_AVAILABLE_NONE; + + if (NM_FLAGS_HAS(con_flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER)) + dev_flags |= _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER; + + return dev_flags; +} + +static gboolean +_nm_device_check_connection_available(NMDevice * self, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceState state; + GError * local = NULL; + + /* an unrealized software device is always available, hardware devices never. */ + if (!nm_device_is_real(self)) { + if (nm_device_is_software(self)) { + if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) { + if (error) { + g_return_val_if_fail(local, FALSE); + nm_utils_error_set(error, + local->domain == NM_UTILS_ERROR ? local->code + : NM_UTILS_ERROR_UNKNOWN, + "profile is not compatible with software device (%s)", + local->message); + g_error_free(local); + } + return FALSE; + } + return TRUE; + } + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE, + "hardware device is not realized"); + return FALSE; + } + + state = nm_device_get_state(self); + if (state < NM_DEVICE_STATE_UNMANAGED) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE, + "device is in unknown state"); + return FALSE; + } + if (state < NM_DEVICE_STATE_UNAVAILABLE) { + if (nm_device_get_managed(self, FALSE)) { + /* device is managed, both for user-requests and non-user-requests alike. */ + } else { + if (!nm_device_get_managed(self, TRUE)) { + /* device is strictly unmanaged by authoritative unmanaged reasons. */ + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE, + "device is strictly unmanaged"); + return FALSE; + } + if (!NM_FLAGS_HAS(flags, + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED)) { + /* device could be managed for an explict user-request, but this is not such a request. */ + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE, + "device is currently unmanaged"); + return FALSE; + } + } + } + if (state < NM_DEVICE_STATE_DISCONNECTED && !nm_device_is_software(self)) { + if (!nm_device_is_available(self, _device_check_dev_available_flags_from_con(flags))) { + if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device is not available"); + } else { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device is not available for internal request"); + } + return FALSE; + } + } + + if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) { + if (error) { + nm_utils_error_set(error, + local->domain == NM_UTILS_ERROR ? local->code + : NM_UTILS_ERROR_UNKNOWN, + "profile is not compatible with device (%s)", + local->message); + g_error_free(local); + } + return FALSE; + } + + return NM_DEVICE_GET_CLASS(self)->check_connection_available(self, + connection, + flags, + specific_object, + error); +} + +/** + * nm_device_check_connection_available(): + * @self: the #NMDevice + * @connection: the #NMConnection to check for availability + * @flags: flags to affect the decision making of whether a connection + * is available. Adding a flag can only make a connection more available, + * not less. + * @specific_object: a device type dependent argument to further + * filter the result. Passing a non %NULL specific object can only reduce + * the availability of a connection. + * @error: optionally give reason why not available. + * + * Check if @connection is available to be activated on @self. + * + * Returns: %TRUE if @connection can be activated on @self + */ +gboolean +nm_device_check_connection_available(NMDevice * self, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + gboolean available; + + available = + _nm_device_check_connection_available(self, connection, flags, specific_object, error); + +#if NM_MORE_ASSERTS >= 2 + { + /* The meaning of the flags is so that *adding* a flag relaxes a condition, thus making + * the device *more* available. Assert against that requirement by testing all the flags. */ + NMDeviceCheckConAvailableFlags i, j, k; + gboolean available_all[NM_DEVICE_CHECK_CON_AVAILABLE_ALL + 1] = {FALSE}; + + for (i = 0; i <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; i++) + available_all[i] = + _nm_device_check_connection_available(self, connection, i, specific_object, NULL); + + for (i = 0; i <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; i++) { + for (j = 1; j <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; j <<= 1) { + if (NM_FLAGS_ANY(i, j)) { + k = i & ~j; + nm_assert(available_all[i] == available_all[k] || available_all[i]); + } + } + } + } +#endif + + return available; +} + +static gboolean +available_connections_del_all(NMDevice *self) +{ + if (g_hash_table_size(self->_priv->available_connections) == 0) + return FALSE; + g_hash_table_remove_all(self->_priv->available_connections); + return TRUE; +} + +static gboolean +available_connections_add(NMDevice *self, NMSettingsConnection *sett_conn) +{ + return g_hash_table_add(self->_priv->available_connections, g_object_ref(sett_conn)); +} + +static gboolean +available_connections_del(NMDevice *self, NMSettingsConnection *sett_conn) +{ + return g_hash_table_remove(self->_priv->available_connections, sett_conn); +} + +static gboolean +check_connection_available(NMDevice * self, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + /* Connections which require a network connection are not available when + * the device has no carrier, even with ignore-carrer=TRUE. + */ + if (priv->carrier || !connection_requires_carrier(connection)) + return TRUE; + + if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER) + && priv->carrier_wait_id != 0) { + /* The device has no carrier though the connection requires it. + * + * If we are still waiting for carrier, the connection is available + * for an explicit user-request. */ + return TRUE; + } + + /* master types are always available even without carrier. + * Making connection non-available would un-enslave slaves which + * is not desired. */ + if (nm_device_is_master(self)) + return TRUE; + + if (!priv->up) { + /* If the device is !IFF_UP it also has no carrier. But we assume that if we + * would start activating the device (and thereby set the device IFF_UP), + * that we would get a carrier. We only know after we set the device up, + * and we only set it up after we start activating it. So presumably, this + * profile would be available (but we just don't know). */ + return TRUE; + } + + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has no carrier"); + return FALSE; +} + +void +nm_device_recheck_available_connections(NMDevice *self) +{ + NMDevicePrivate * priv; + NMSettingsConnection *const *connections; + gboolean changed = FALSE; + GHashTableIter h_iter; + NMSettingsConnection * sett_conn; + guint i; + gs_unref_hashtable GHashTable *prune_list = NULL; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (g_hash_table_size(priv->available_connections) > 0) { + prune_list = g_hash_table_new(nm_direct_hash, NULL); + g_hash_table_iter_init(&h_iter, priv->available_connections); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &sett_conn, NULL)) + g_hash_table_add(prune_list, sett_conn); + } + + connections = nm_settings_get_connections(priv->settings, NULL); + for (i = 0; connections[i]; i++) { + sett_conn = connections[i]; + + if (nm_device_check_connection_available(self, + nm_settings_connection_get_connection(sett_conn), + NM_DEVICE_CHECK_CON_AVAILABLE_NONE, + NULL, + NULL)) { + if (available_connections_add(self, sett_conn)) + changed = TRUE; + if (prune_list) + g_hash_table_remove(prune_list, sett_conn); + } + } + + if (prune_list) { + g_hash_table_iter_init(&h_iter, prune_list); + while (g_hash_table_iter_next(&h_iter, (gpointer *) &sett_conn, NULL)) { + if (available_connections_del(self, sett_conn)) + changed = TRUE; + } + } + + if (changed) + _notify(self, PROP_AVAILABLE_CONNECTIONS); + available_connections_check_delete_unrealized(self); +} + +/** + * nm_device_get_best_connection: + * @self: the #NMDevice + * @specific_object: a specific object path if any + * @error: reason why no connection was returned + * + * Returns a connection that's most suitable for user-initiated activation + * of a device, optionally with a given specific object. + * + * Returns: the #NMSettingsConnection or %NULL (setting an @error) + */ +NMSettingsConnection * +nm_device_get_best_connection(NMDevice *self, const char *specific_object, GError **error) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMSettingsConnection *sett_conn = NULL; + NMSettingsConnection *candidate; + guint64 best_timestamp = 0; + GHashTableIter iter; + + g_hash_table_iter_init(&iter, priv->available_connections); + while (g_hash_table_iter_next(&iter, (gpointer) &candidate, NULL)) { + guint64 candidate_timestamp = 0; + + /* If a specific object is given, only include connections that are + * compatible with it. + */ + if (specific_object /* << Optimization: we know that the connection is available without @specific_object. */ + && !nm_device_check_connection_available( + self, + nm_settings_connection_get_connection(candidate), + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST, + specific_object, + NULL)) + continue; + + nm_settings_connection_get_timestamp(candidate, &candidate_timestamp); + if (!sett_conn || (candidate_timestamp > best_timestamp)) { + sett_conn = candidate; + best_timestamp = candidate_timestamp; + } + } + + if (!sett_conn) { + g_set_error(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_UNKNOWN_CONNECTION, + "The device '%s' has no connections available for activation.", + nm_device_get_iface(self)); + } + + return sett_conn; +} + +static void +cp_connection_added_or_updated(NMDevice *self, NMSettingsConnection *sett_conn) +{ + gboolean changed; + + g_return_if_fail(NM_IS_DEVICE(self)); + g_return_if_fail(NM_IS_SETTINGS_CONNECTION(sett_conn)); + + if (nm_device_check_connection_available(self, + nm_settings_connection_get_connection(sett_conn), + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST, + NULL, + NULL)) + changed = available_connections_add(self, sett_conn); + else + changed = available_connections_del(self, sett_conn); + + if (changed) { + _notify(self, PROP_AVAILABLE_CONNECTIONS); + available_connections_check_delete_unrealized(self); + } +} + +static void +cp_connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data) +{ + cp_connection_added_or_updated(user_data, sett_conn); +} + +static void +cp_connection_updated(NMSettings * settings, + NMSettingsConnection *sett_conn, + guint update_reason_u, + gpointer user_data) +{ + cp_connection_added_or_updated(user_data, sett_conn); +} + +static void +cp_connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data) +{ + NMDevice *self = user_data; + + g_return_if_fail(NM_IS_DEVICE(self)); + + if (available_connections_del(self, sett_conn)) { + _notify(self, PROP_AVAILABLE_CONNECTIONS); + available_connections_check_delete_unrealized(self); + } +} + +gboolean +nm_device_supports_vlans(NMDevice *self) +{ + return nm_platform_link_supports_vlans(nm_device_get_platform(self), + nm_device_get_ifindex(self)); +} + +/** + * nm_device_add_pending_action(): + * @self: the #NMDevice to add the pending action to + * @action: a static string that identifies the action. The string instance must + * stay valid until the pending action is removed (that is, the string is + * not cloned, but ownership stays with the caller). + * @assert_not_yet_pending: if %TRUE, assert that the @action is currently not yet pending. + * Otherwise, ignore duplicate scheduling of the same action silently. + * + * Adds a pending action to the device. + * + * Returns: %TRUE if the action was added (and not already added before). %FALSE + * if the same action is already scheduled. In the latter case, the action was not scheduled + * a second time. + */ +gboolean +nm_device_add_pending_action(NMDevice *self, const char *action, gboolean assert_not_yet_pending) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + GSList * iter; + guint count = 0; + + g_return_val_if_fail(action, FALSE); + + /* Check if the action is already pending. Cannot add duplicate actions */ + for (iter = priv->pending_actions; iter; iter = iter->next) { + if (nm_streq(action, iter->data)) { + if (assert_not_yet_pending) { + _LOGW(LOGD_DEVICE, + "add_pending_action (%d): '%s' already pending", + count + g_slist_length(iter), + action); + g_return_val_if_reached(FALSE); + } else { + _LOGT(LOGD_DEVICE, + "add_pending_action (%d): '%s' already pending (expected)", + count + g_slist_length(iter), + action); + } + return FALSE; + } + count++; + } + + priv->pending_actions = g_slist_prepend(priv->pending_actions, (char *) action); + count++; + + _LOGD(LOGD_DEVICE, "add_pending_action (%d): '%s'", count, action); + + if (count == 1) + _notify(self, PROP_HAS_PENDING_ACTION); + + return TRUE; +} + +/** + * nm_device_remove_pending_action(): + * @self: the #NMDevice to remove the pending action from + * @action: a string that identifies the action. + * @assert_is_pending: if %TRUE, assert that the @action is pending. + * If %FALSE, don't do anything if the current action is not pending and + * return %FALSE. + * + * Removes a pending action previously added by nm_device_add_pending_action(). + * + * Returns: whether the @action was pending and is now removed. + */ +gboolean +nm_device_remove_pending_action(NMDevice *self, const char *action, gboolean assert_is_pending) +{ + NMDevicePrivate *priv; + GSList * iter, *next; + guint count = 0; + + g_return_val_if_fail(self, FALSE); + g_return_val_if_fail(action, FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + for (iter = priv->pending_actions; iter; iter = next) { + next = iter->next; + if (nm_streq(action, iter->data)) { + _LOGD(LOGD_DEVICE, + "remove_pending_action (%d): '%s'", + count + g_slist_length(iter->next), /* length excluding 'iter' */ + action); + priv->pending_actions = g_slist_delete_link(priv->pending_actions, iter); + if (priv->pending_actions == NULL) + _notify(self, PROP_HAS_PENDING_ACTION); + return TRUE; + } + count++; + } + + if (assert_is_pending) { + _LOGW(LOGD_DEVICE, "remove_pending_action (%d): '%s' not pending", count, action); + g_return_val_if_reached(FALSE); + } else + _LOGT(LOGD_DEVICE, + "remove_pending_action (%d): '%s' not pending (expected)", + count, + action); + + return FALSE; +} + +const char * +nm_device_has_pending_action_reason(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->pending_actions) { + if (!priv->pending_actions->next && nm_device_get_state(self) == NM_DEVICE_STATE_ACTIVATED + && nm_streq(priv->pending_actions->data, NM_PENDING_ACTION_CARRIER_WAIT)) { + /* if the device is already in activated state, and the only reason + * why it appears still busy is "carrier-wait", then we are already complete. */ + return NULL; + } + + return priv->pending_actions->data; + } + + if (nm_device_is_real(self) + && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) { + /* as long as the platform link is not yet initialized, we have a pending + * action. */ + return NM_PENDING_ACTION_LINK_INIT; + } + + return NULL; +} + +/*****************************************************************************/ + +static void +_cancel_activation(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->fw_call) { + nm_firewall_manager_cancel_call(priv->fw_call); + nm_assert(!priv->fw_call); + priv->fw_call = NULL; + priv->fw_state = FIREWALL_STATE_INITIALIZED; + } + + dispatcher_cleanup(self); + ip_check_gw_ping_cleanup(self); + + /* Break the activation chain */ + activation_source_clear(self, AF_INET); + activation_source_clear(self, AF_INET6); +} + +static void +_cleanup_generic_pre(NMDevice *self, CleanupType cleanup_type) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + guint i; + + _cancel_activation(self); + + priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_INIT; + + if (cleanup_type != CLEANUP_TYPE_KEEP) { + nm_manager_device_route_metric_clear(NM_MANAGER_GET, nm_device_get_ip_ifindex(self)); + } + + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE && priv->fw_state >= FIREWALL_STATE_INITIALIZED + && priv->fw_mgr && !nm_device_sys_iface_state_is_external(self)) { + nm_firewall_manager_remove_from_zone(priv->fw_mgr, + nm_device_get_ip_iface(self), + NULL, + NULL, + NULL); + } + priv->fw_state = FIREWALL_STATE_UNMANAGED; + g_clear_object(&priv->fw_mgr); + + queued_state_clear(self); + + nm_clear_pointer(&priv->shared_ip_handle, nm_netns_shared_ip_release); + + for (i = 0; i < 2; i++) + nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free); + + _cleanup_ip_pre(self, AF_INET, cleanup_type); + _cleanup_ip_pre(self, AF_INET6, cleanup_type); +} + +static void +_cleanup_generic_post(NMDevice *self, CleanupType cleanup_type) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + priv->v4_commit_first_time = TRUE; + priv->v6_commit_first_time = TRUE; + + priv->v4_route_table_initialized = FALSE; + priv->v6_route_table_initialized = FALSE; + + priv->v4_route_table_all_sync_before = FALSE; + priv->v6_route_table_all_sync_before = FALSE; + + priv->default_route_metric_penalty_ip4_has = FALSE; + priv->default_route_metric_penalty_ip6_has = FALSE; + + priv->linklocal6_dad_counter = 0; + + priv->mtu_force_set_done = FALSE; + + /* Clean up IP configs; this does not actually deconfigure the + * interface; the caller must flush routes and addresses explicitly. + */ + nm_device_set_ip_config(self, AF_INET, NULL, TRUE, NULL); + nm_device_set_ip_config(self, AF_INET6, NULL, TRUE, NULL); + g_clear_object(&priv->proxy_config); + g_clear_object(&priv->con_ip_config_4); + applied_config_clear(&priv->dev_ip_config_4); + applied_config_clear(&priv->dev2_ip_config_4); + g_clear_object(&priv->ext_ip_config_4); + g_clear_object(&priv->ip_config_4); + g_clear_object(&priv->con_ip_config_6); + applied_config_clear(&priv->ac_ip6_config); + g_clear_object(&priv->ext_ip_config_6); + g_clear_object(&priv->ext_ip6_config_captured); + applied_config_clear(&priv->dev2_ip_config_6); + g_clear_object(&priv->ip_config_6); + g_clear_object(&priv->dad6_ip6_config); + priv->ipv6ll_has = FALSE; + memset(&priv->ipv6ll_addr, 0, sizeof(priv->ipv6ll_addr)); + + nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref); + nm_clear_g_source(&priv->rt6_temporary_not_available_id); + + g_slist_free_full(priv->vpn_configs_4, g_object_unref); + priv->vpn_configs_4 = NULL; + g_slist_free_full(priv->vpn_configs_6, g_object_unref); + priv->vpn_configs_6 = NULL; + + /* We no longer accept the delegations. nm_device_set_ip_config(NULL) + * above disables them. */ + nm_assert(priv->needs_ip6_subnet == FALSE); + + if (priv->act_request.obj) { + nm_active_connection_set_default(NM_ACTIVE_CONNECTION(priv->act_request.obj), + AF_INET, + FALSE); + nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id); + act_request_set(self, NULL); + } + + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) { + /* Check if the device was deactivated, and if so, delete_link. + * Don't call delete_link synchronously because we are currently + * handling a state change -- which is not reentrant. */ + delete_on_deactivate_check_and_schedule(self, nm_device_get_ip_ifindex(self)); + } + + /* ip_iface should be cleared after flushing all routes and addresses, since + * those are identified by ip_iface, not by iface (which might be a tty + * or ATM device). + */ + _set_ip_ifindex(self, 0, NULL); +} + +/* + * nm_device_cleanup + * + * Remove a device's routing table entries and IP addresses. + * + */ +static void +nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanup_type) +{ + NMDevicePrivate *priv; + int ifindex; + + g_return_if_fail(NM_IS_DEVICE(self)); + + if (reason == NM_DEVICE_STATE_REASON_NOW_MANAGED) + _LOGD(LOGD_DEVICE, "preparing device"); + else + _LOGD(LOGD_DEVICE, + "deactivating device (reason '%s') [%d]", + reason_to_string_a(reason), + reason); + + /* Save whether or not we tried IPv6 for later */ + priv = NM_DEVICE_GET_PRIVATE(self); + + _cleanup_generic_pre(self, cleanup_type); + + /* Turn off kernel IPv6 */ + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) { + set_disable_ipv6(self, "1"); + nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", "0"); + } + + /* Call device type-specific deactivation */ + if (NM_DEVICE_GET_CLASS(self)->deactivate) + NM_DEVICE_GET_CLASS(self)->deactivate(self); + + ifindex = nm_device_get_ip_ifindex(self); + + if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) { + /* master: release slaves */ + nm_device_master_release_slaves(self); + + /* Take out any entries in the routing table and any IP address the device had. */ + if (ifindex > 0) { + NMPlatform * platform = nm_device_get_platform(self); + NMUtilsIPv6IfaceId iid = {}; + + nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex); + nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex); + nm_platform_tfilter_sync(platform, ifindex, NULL); + nm_platform_qdisc_sync(platform, ifindex, NULL); + set_ipv6_token(self, iid, "::"); + } + } + + _routing_rules_sync(self, + cleanup_type == CLEANUP_TYPE_KEEP ? NM_TERNARY_DEFAULT : NM_TERNARY_FALSE); + + if (ifindex > 0) + nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self), ifindex, NULL); + + /* slave: mark no longer enslaved */ + if (priv->master && priv->ifindex > 0 + && nm_platform_link_get_master(nm_device_get_platform(self), priv->ifindex) <= 0) + nm_device_master_release_one_slave(priv->master, + self, + FALSE, + FALSE, + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED); + + if (priv->lldp_listener) + nm_lldp_listener_stop(priv->lldp_listener); + + nm_device_update_metered(self); + + if (ifindex > 0) { + /* during device cleanup, we want to reset the MAC address of the device + * to the initial state. + * + * We certainly want to do that when reaching the UNMANAGED state... */ + if (nm_device_get_state(self) <= NM_DEVICE_STATE_UNMANAGED) + nm_device_hw_addr_reset(self, "unmanage"); + else { + /* for other device states (UNAVAILABLE, DISCONNECTED), allow the + * device to overwrite the reset behavior, so that Wi-Fi can set + * a randomized MAC address used during scanning. */ + NM_DEVICE_GET_CLASS(self)->deactivate_reset_hw_addr(self); + } + } + + priv->mtu_source = NM_DEVICE_MTU_SOURCE_NONE; + priv->ip6_mtu = 0; + if (priv->mtu_initial || priv->ip6_mtu_initial) { + ifindex = nm_device_get_ip_ifindex(self); + + if (ifindex > 0 && cleanup_type == CLEANUP_TYPE_DECONFIGURE) { + _LOGT(LOGD_DEVICE, + "mtu: reset device-mtu: %u, ipv6-mtu: %u, ifindex: %d", + (guint) priv->mtu_initial, + (guint) priv->ip6_mtu_initial, + ifindex); + if (priv->mtu_initial) { + nm_platform_link_set_mtu(nm_device_get_platform(self), ifindex, priv->mtu_initial); + priv->carrier_wait_until_ms = + nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS; + } + if (priv->ip6_mtu_initial) { + char sbuf[64]; + + nm_device_sysctl_ip_conf_set( + self, + AF_INET6, + "mtu", + nm_sprintf_buf(sbuf, "%u", (unsigned) priv->ip6_mtu_initial)); + } + } + priv->mtu_initial = 0; + priv->ip6_mtu_initial = 0; + } + + _ethtool_state_reset(self); + + _cleanup_generic_post(self, cleanup_type); +} + +static void +deactivate_reset_hw_addr(NMDevice *self) +{ + nm_device_hw_addr_reset(self, "deactivate"); +} + +static char * +find_dhcp4_address(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + const NMPlatformIP4Address *a; + NMDedupMultiIter ipconf_iter; + + if (!priv->ip_config_4) + return NULL; + + nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, priv->ip_config_4, &a) { + if (a->addr_source == NM_IP_CONFIG_SOURCE_DHCP) + return nm_utils_inet4_ntop_dup(a->address); + } + return NULL; +} + +void +nm_device_spawn_iface_helper(NMDevice *self) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + gboolean configured = FALSE; + NMConnection * connection; + GError * error = NULL; + const char * method; + GPtrArray * argv; + gs_free char * dhcp4_address = NULL; + char * logging_backend; + NMUtilsStableType stable_type; + const char * stable_id; + + if (priv->state != NM_DEVICE_STATE_ACTIVATED) + return; + if (!nm_device_can_assume_connections(self)) + return; + + connection = nm_device_get_applied_connection(self); + + g_return_if_fail(connection); + + argv = g_ptr_array_sized_new(10); + g_ptr_array_set_free_func(argv, g_free); + + g_ptr_array_add(argv, g_strdup(LIBEXECDIR "/nm-iface-helper")); + g_ptr_array_add(argv, g_strdup("--ifname")); + g_ptr_array_add(argv, g_strdup(nm_device_get_ip_iface(self))); + g_ptr_array_add(argv, g_strdup("--uuid")); + g_ptr_array_add(argv, g_strdup(nm_connection_get_uuid(connection))); + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + if (stable_type != NM_UTILS_STABLE_TYPE_UUID) { + g_ptr_array_add(argv, g_strdup("--stable-id")); + g_ptr_array_add(argv, g_strdup_printf("%d %s", (int) stable_type, stable_id)); + } + + logging_backend = + nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG, + NM_CONFIG_KEYFILE_GROUP_LOGGING, + NM_CONFIG_KEYFILE_KEY_LOGGING_BACKEND, + NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY); + if (logging_backend) { + g_ptr_array_add(argv, g_strdup("--logging-backend")); + g_ptr_array_add(argv, logging_backend); + } + + g_ptr_array_add(argv, g_strdup("--log-level")); + g_ptr_array_add(argv, g_strdup(nm_logging_level_to_string())); + + g_ptr_array_add(argv, g_strdup("--log-domains")); + g_ptr_array_add(argv, g_strdup(nm_logging_domains_to_string())); + + dhcp4_address = find_dhcp4_address(self); + + method = nm_device_get_effective_ip_config_method(self, AF_INET); + if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) { + NMSettingIPConfig *s_ip4; + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + nm_assert(s_ip4); + + g_ptr_array_add(argv, g_strdup("--priority4")); + g_ptr_array_add(argv, g_strdup_printf("%u", nm_device_get_route_metric(self, AF_INET))); + + g_ptr_array_add(argv, g_strdup("--dhcp4")); + g_ptr_array_add(argv, g_strdup(dhcp4_address)); + if (nm_setting_ip_config_get_may_fail(s_ip4) == FALSE) + g_ptr_array_add(argv, g_strdup("--dhcp4-required")); + + if (priv->dhcp_data_4.client) { + const char *hostname; + GBytes * client_id; + + client_id = nm_dhcp_client_get_client_id(priv->dhcp_data_4.client); + if (client_id) { + g_ptr_array_add(argv, g_strdup("--dhcp4-clientid")); + g_ptr_array_add(argv, + nm_utils_bin2hexstr_full(g_bytes_get_data(client_id, NULL), + g_bytes_get_size(client_id), + ':', + FALSE, + NULL)); + } + + hostname = nm_dhcp_client_get_hostname(priv->dhcp_data_4.client); + if (hostname) { + if (nm_dhcp_client_get_use_fqdn(priv->dhcp_data_4.client)) + g_ptr_array_add(argv, g_strdup("--dhcp4-fqdn")); + else + g_ptr_array_add(argv, g_strdup("--dhcp4-hostname")); + g_ptr_array_add(argv, g_strdup(hostname)); + } + } + + configured = TRUE; + } + + method = nm_utils_get_ip_config_method(connection, AF_INET6); + if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) { + NMSettingIPConfig *s_ip6; + NMUtilsIPv6IfaceId iid = NM_UTILS_IPV6_IFACE_ID_INIT; + + s_ip6 = nm_connection_get_setting_ip6_config(connection); + g_assert(s_ip6); + + g_ptr_array_add(argv, g_strdup("--priority6")); + g_ptr_array_add(argv, g_strdup_printf("%u", nm_device_get_route_metric(self, AF_INET6))); + + g_ptr_array_add(argv, g_strdup("--slaac")); + + if (nm_setting_ip_config_get_may_fail(s_ip6) == FALSE) + g_ptr_array_add(argv, g_strdup("--slaac-required")); + + g_ptr_array_add(argv, g_strdup("--slaac-tempaddr")); + g_ptr_array_add(argv, g_strdup_printf("%d", priv->ndisc_use_tempaddr)); + + if (nm_device_get_ip_iface_identifier(self, &iid, FALSE)) { + g_ptr_array_add(argv, g_strdup("--iid")); + g_ptr_array_add( + argv, + nm_utils_bin2hexstr_full(iid.id_u8, sizeof(NMUtilsIPv6IfaceId), ':', FALSE, NULL)); + } + + g_ptr_array_add(argv, g_strdup("--addr-gen-mode")); + g_ptr_array_add( + argv, + g_strdup_printf("%d", + nm_setting_ip6_config_get_addr_gen_mode(NM_SETTING_IP6_CONFIG(s_ip6)))); + + configured = TRUE; + } + + if (configured) { + GPid pid; + + g_ptr_array_add(argv, NULL); + + if (nm_logging_enabled(LOGL_DEBUG, LOGD_DEVICE)) { + char *tmp; + + tmp = g_strjoinv(" ", (char **) argv->pdata); + _LOGD(LOGD_DEVICE, "running '%s'", tmp); + g_free(tmp); + } + + if (g_spawn_async(NULL, + (char **) argv->pdata, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, + NULL, + &pid, + &error)) { + _LOGI(LOGD_DEVICE, "spawned helper PID %u", (guint) pid); + } else { + _LOGW(LOGD_DEVICE, "failed to spawn helper: %s", error->message); + g_error_free(error); + } + } + + g_ptr_array_unref(argv); +} + +/*****************************************************************************/ + +static gboolean +ip_config_valid(NMDeviceState state) +{ + return (state == NM_DEVICE_STATE_UNMANAGED) + || (state >= NM_DEVICE_STATE_IP_CHECK && state <= NM_DEVICE_STATE_DEACTIVATING); +} + +static void +notify_ip_properties(NMDevice *self) +{ + _notify(self, PROP_IP_IFACE); + _notify(self, PROP_IP4_CONFIG); + _notify(self, PROP_DHCP4_CONFIG); + _notify(self, PROP_IP6_CONFIG); + _notify(self, PROP_DHCP6_CONFIG); +} + +static void +ip6_managed_setup(NMDevice *self) +{ + set_nm_ipv6ll(self, TRUE); + set_disable_ipv6(self, "1"); + nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "0"); + nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", "0"); + nm_device_sysctl_ip_conf_set(self, AF_INET6, "forwarding", "0"); +} + +static void +deactivate_ready(NMDevice *self, NMDeviceStateReason reason) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->dispatcher.call_id) + return; + + if (priv->sriov_reset_pending > 0) + return; + + if (priv->state == NM_DEVICE_STATE_DEACTIVATING) + nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, reason); +} + +static void +sriov_reset_on_deactivate_cb(GError *error, gpointer user_data) +{ + NMDevice * self; + NMDevicePrivate *priv; + gpointer reason; + + nm_utils_user_data_unpack(user_data, &self, &reason); + priv = NM_DEVICE_GET_PRIVATE(self); + nm_assert(priv->sriov_reset_pending > 0); + priv->sriov_reset_pending--; + + if (nm_utils_error_is_cancelled(error)) + return; + + deactivate_ready(self, GPOINTER_TO_INT(reason)); +} + +static void +sriov_reset_on_failure_cb(GError *error, gpointer user_data) +{ + NMDevice * self = user_data; + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert(priv->sriov_reset_pending > 0); + priv->sriov_reset_pending--; + + if (nm_utils_error_is_cancelled(error)) + return; + + if (priv->state == NM_DEVICE_STATE_FAILED) { + nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_REASON_NONE); + } +} + +static void +deactivate_async_ready(NMDevice *self, GError *error, gpointer user_data) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceStateReason reason = GPOINTER_TO_UINT(user_data); + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + _LOGD(LOGD_DEVICE, "Deactivation cancelled"); + return; + } + + g_clear_object(&priv->deactivating_cancellable); + + /* In every other case, transition to the DISCONNECTED state */ + if (error) { + _LOGW(LOGD_DEVICE, "Deactivation failed: %s", error->message); + } + + deactivate_ready(self, reason); +} + +static void +deactivate_dispatcher_complete(NMDispatcherCallId *call_id, gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceStateReason reason; + + g_return_if_fail(call_id == priv->dispatcher.call_id); + g_return_if_fail(priv->dispatcher.post_state == NM_DEVICE_STATE_DISCONNECTED); + + reason = priv->state_reason; + + priv->dispatcher.call_id = NULL; + priv->dispatcher.post_state = NM_DEVICE_STATE_UNKNOWN; + priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE; + + if (nm_clear_g_cancellable(&priv->deactivating_cancellable)) + nm_assert_not_reached(); + + if (NM_DEVICE_GET_CLASS(self)->deactivate_async) { + /* FIXME: the virtual function deactivate_async() has only this caller here. + * And the NMDevice subtypes are well aware of the circumstances when they + * are called. We shall make the function less generic and thus (as the scope + * is narrower) more convenient. + * + * - Drop the callback argument. Instead, when deactivate_async() completes, the + * subtype shall call a method _nm_device_deactivate_async_done(). Because as + * it is currently, subtypes need to pretend this callback and the user-data + * would be opaque, and carry it around. When it's in fact very clear what this + * is. + * + * - Also drop the GCancellable argument. Upon cancellation, NMDevice shall + * call another virtual function deactivate_async_abort(). As it is currently, + * callers need to register to the cancelled signal of the cancellable. It + * seems simpler to just implement the deactivate_async_abort() function. + * On the other hand, some implementations actually use the GCancellable. + * So, NMDevice shall do both: it shall both pass a cancellable, but also + * invoke deactivate_async_abort(). It allow the implementation to honor + * whatever is simpler for their purpose. + * + * - sometimes, the subclass can complete right away. Scheduling the completion + * in an idle handler is cumbersome. Allow the function to return FALSE to + * indicate that the device is already deactivated and the callback (or + * _nm_device_deactivate_async_done()) won't be invoked. + */ + priv->deactivating_cancellable = g_cancellable_new(); + NM_DEVICE_GET_CLASS(self)->deactivate_async(self, + priv->deactivating_cancellable, + deactivate_async_ready, + GUINT_TO_POINTER(reason)); + } else + deactivate_ready(self, reason); +} + +static void +_set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason, gboolean quitting) +{ + NMDevicePrivate *priv; + NMDeviceState old_state; + gs_unref_object NMActRequest *req = NULL; + gboolean no_firmware = FALSE; + NMSettingsConnection * sett_conn; + NMSettingSriov * s_sriov; + gboolean concheck_now; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + /* Track re-entry */ + g_warn_if_fail(priv->in_state_changed == FALSE); + + old_state = priv->state; + + if (state == NM_DEVICE_STATE_FAILED && nm_device_sys_iface_state_is_external_or_assume(self)) { + /* Avoid tearing down assumed connection, assume it's connected */ + state = NM_DEVICE_STATE_ACTIVATED; + reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED; + } + + /* Do nothing if state isn't changing, but as a special case allow + * re-setting UNAVAILABLE if the device is missing firmware so that we + * can retry device initialization. + */ + if ((priv->state == state) + && (state != NM_DEVICE_STATE_UNAVAILABLE || !priv->firmware_missing)) { + _LOGD(LOGD_DEVICE, + "state change: %s -> %s (reason '%s', sys-iface-state: '%s'%s)", + nm_device_state_to_str(old_state), + nm_device_state_to_str(state), + reason_to_string_a(reason), + _sys_iface_state_to_str(priv->sys_iface_state), + priv->firmware_missing ? ", missing firmware" : ""); + return; + } + + _LOGI(LOGD_DEVICE, + "state change: %s -> %s (reason '%s', sys-iface-state: '%s')", + nm_device_state_to_str(old_state), + nm_device_state_to_str(state), + reason_to_string_a(reason), + _sys_iface_state_to_str(priv->sys_iface_state)); + + /* in order to prevent triggering any callback caused + * by the device not having any pending action anymore + * we add one here that gets removed at the end of the function */ + nm_device_add_pending_action(self, NM_PENDING_ACTION_IN_STATE_CHANGE, TRUE); + priv->in_state_changed = TRUE; + + priv->state = state; + priv->state_reason = reason; + + queued_state_clear(self); + + dispatcher_cleanup(self); + + nm_clear_g_cancellable(&priv->deactivating_cancellable); + + /* Cache the activation request for the dispatcher */ + req = nm_g_object_ref(priv->act_request.obj); + + if (state > NM_DEVICE_STATE_UNMANAGED && state <= NM_DEVICE_STATE_ACTIVATED + && nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_NOW_MANAGED + && NM_IN_SET_TYPED(NMDeviceSysIfaceState, + priv->sys_iface_state, + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_ASSUME)) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED); + + if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_ACTIVATED) + priv->auth_retries = NM_DEVICE_AUTH_RETRIES_UNSET; + + if (state > NM_DEVICE_STATE_DISCONNECTED) + nm_device_assume_state_reset(self); + + if (state <= NM_DEVICE_STATE_UNAVAILABLE) { + if (available_connections_del_all(self)) + _notify(self, PROP_AVAILABLE_CONNECTIONS); + if (old_state > NM_DEVICE_STATE_UNAVAILABLE) { + _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED); + } + } + + /* Update the available connections list when a device first becomes available */ + if (state >= NM_DEVICE_STATE_DISCONNECTED && old_state < NM_DEVICE_STATE_DISCONNECTED) + nm_device_recheck_available_connections(self); + + if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_DEACTIVATING) { + if (nm_clear_g_free(&priv->current_stable_id)) + _LOGT(LOGD_DEVICE, "stable-id: clear"); + } + + /* Handle the new state here; but anything that could trigger + * another state change should be done below. + */ + switch (state) { + case NM_DEVICE_STATE_UNMANAGED: + nm_device_set_firmware_missing(self, FALSE); + if (old_state > NM_DEVICE_STATE_UNMANAGED) { + if (priv->sys_iface_state != NM_DEVICE_SYS_IFACE_STATE_MANAGED) { + nm_device_cleanup(self, + reason, + priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_REMOVED + ? CLEANUP_TYPE_REMOVED + : CLEANUP_TYPE_KEEP); + } else { + /* Clean up if the device is now unmanaged but was activated */ + if (nm_device_get_act_request(self)) + nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE); + nm_device_take_down(self, TRUE); + nm_device_hw_addr_reset(self, "unmanage"); + set_nm_ipv6ll(self, FALSE); + restore_ip6_properties(self); + } + } + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_EXTERNAL); + break; + case NM_DEVICE_STATE_UNAVAILABLE: + if (old_state == NM_DEVICE_STATE_UNMANAGED) { + save_ip6_properties(self); + if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) + ip6_managed_setup(self); + device_init_static_sriov_num_vfs(self); + } + + if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) { + if (old_state == NM_DEVICE_STATE_UNMANAGED || priv->firmware_missing) { + if (!nm_device_bring_up(self, TRUE, &no_firmware) && no_firmware) + _LOGW(LOGD_PLATFORM, "firmware may be missing."); + nm_device_set_firmware_missing(self, no_firmware ? TRUE : FALSE); + } + + /* Ensure the device gets deactivated in response to stuff like + * carrier changes or rfkill. But don't deactivate devices that are + * about to assume a connection since that defeats the purpose of + * assuming the device's existing connection. + * + * Note that we "deactivate" the device even when coming from + * UNMANAGED, to ensure that it's in a clean state. + */ + nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE); + } + break; + case NM_DEVICE_STATE_DISCONNECTED: + if (old_state > NM_DEVICE_STATE_DISCONNECTED) { + /* Ensure devices that previously assumed a connection now have + * userspace IPv6LL enabled. + */ + set_nm_ipv6ll(self, TRUE); + + nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE); + } else if (old_state < NM_DEVICE_STATE_DISCONNECTED) { + if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) { + /* Ensure IPv6 is set up as it may not have been done when + * entering the UNAVAILABLE state depending on the reason. + */ + ip6_managed_setup(self); + } + } + break; + case NM_DEVICE_STATE_PREPARE: + nm_device_update_initial_hw_address(self); + break; + case NM_DEVICE_STATE_NEED_AUTH: + if (old_state > NM_DEVICE_STATE_NEED_AUTH) { + /* Clean up any half-done IP operations if the device's layer2 + * finds out it needs authentication during IP config. + */ + _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_DECONFIGURE); + _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE); + } + break; + default: + break; + } + + /* Reset intern autoconnect flags when the device is activating or connected. */ + if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_ACTIVATED) + nm_device_autoconnect_blocked_unset(self, NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL); + + _notify(self, PROP_STATE); + _notify(self, PROP_STATE_REASON); + nm_dbus_object_emit_signal(NM_DBUS_OBJECT(self), + &interface_info_device, + &signal_info_state_changed, + "(uuu)", + (guint32) state, + (guint32) old_state, + (guint32) reason); + g_signal_emit(self, + signals[STATE_CHANGED], + 0, + (guint) state, + (guint) old_state, + (guint) reason); + + /* Post-process the event after internal notification */ + + switch (state) { + case NM_DEVICE_STATE_UNAVAILABLE: + /* If the device can activate now (ie, it's got a carrier, the supplicant + * is active, or whatever) schedule a delayed transition to DISCONNECTED + * to get things rolling. The device can't transition immediately because + * we can't change states again from the state handler for a variety of + * reasons. + */ + if (nm_device_is_available(self, NM_DEVICE_CHECK_DEV_AVAILABLE_NONE)) { + nm_device_queue_recheck_available(self, + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_NONE); + } else { + _LOGD(LOGD_DEVICE, "device not yet available for transition to DISCONNECTED"); + } + break; + case NM_DEVICE_STATE_DEACTIVATING: + _cancel_activation(self); + + /* We cache the ignore_carrier state to not react on config-reloads while the connection + * is active. But on deactivating, reset the ignore-carrier flag to the current state. */ + priv->ignore_carrier = nm_config_data_get_ignore_carrier(NM_CONFIG_GET_DATA, self); + + if (quitting) { + nm_dispatcher_call_device_sync(NM_DISPATCHER_ACTION_PRE_DOWN, self, req); + } else { + priv->dispatcher.post_state = NM_DEVICE_STATE_DISCONNECTED; + priv->dispatcher.post_state_reason = reason; + if (!nm_dispatcher_call_device(NM_DISPATCHER_ACTION_PRE_DOWN, + self, + req, + deactivate_dispatcher_complete, + self, + &priv->dispatcher.call_id)) { + /* Just proceed on errors */ + deactivate_dispatcher_complete(0, self); + } + + if (priv->ifindex > 0 + && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) { + priv->sriov_reset_pending++; + sriov_op_queue(self, + 0, + NM_OPTION_BOOL_TRUE, + sriov_reset_on_deactivate_cb, + nm_utils_user_data_pack(self, GINT_TO_POINTER(reason))); + } + } + + nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id); + break; + case NM_DEVICE_STATE_DISCONNECTED: + if (priv->queued_act_request && !priv->queued_act_request_is_waiting_for_carrier) { + gs_unref_object NMActRequest *queued_req = NULL; + + queued_req = g_steal_pointer(&priv->queued_act_request); + _device_activate(self, queued_req); + } + break; + case NM_DEVICE_STATE_ACTIVATED: + _LOGI(LOGD_DEVICE, "Activation: successful, device activated."); + nm_device_update_metered(self); + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_UP, self, req, NULL, NULL, NULL); + + if (priv->proxy_config) + _pacrunner_manager_add(self); + break; + case NM_DEVICE_STATE_FAILED: + /* Usually upon failure the activation chain is interrupted in + * one of the stages; but in some cases the device fails for + * external events (as a failure of master connection) while + * the activation sequence is running and so we need to ensure + * that the chain is terminated here. + */ + _cancel_activation(self); + + sett_conn = nm_device_get_settings_connection(self); + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: failed for connection '%s'", + sett_conn ? nm_settings_connection_get_id(sett_conn) : "<unknown>"); + + /* Notify any slaves of the unexpected failure */ + nm_device_master_release_slaves(self); + + /* If the connection doesn't yet have a timestamp, set it to zero so that + * we can distinguish between connections we've tried to activate and have + * failed (zero timestamp), connections that succeeded (non-zero timestamp), + * and those we haven't tried yet (no timestamp). + */ + if (sett_conn && !nm_settings_connection_get_timestamp(sett_conn, NULL)) + nm_settings_connection_update_timestamp(sett_conn, (guint64) 0); + + if (priv->ifindex > 0 + && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) { + priv->sriov_reset_pending++; + sriov_op_queue(self, 0, NM_OPTION_BOOL_TRUE, sriov_reset_on_failure_cb, self); + break; + } + /* Schedule the transition to DISCONNECTED. The device can't transition + * immediately because we can't change states again from the state + * handler for a variety of reasons. + */ + nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_REASON_NONE); + break; + case NM_DEVICE_STATE_IP_CHECK: + { + gboolean change_zone = FALSE; + + if (!nm_device_sys_iface_state_is_external(self)) { + if (priv->ip_iface) { + /* The device now has a @ip_iface different from the + * @iface on which we previously set the zone. */ + change_zone = TRUE; + } else if (priv->fw_state == FIREWALL_STATE_UNMANAGED && priv->ifindex > 0) { + /* We didn't set the zone earlier because there was + * no ifindex. */ + change_zone = TRUE; + } + } + + if (change_zone) { + priv->fw_state = FIREWALL_STATE_WAIT_IP_CONFIG; + fw_change_zone(self); + } else + nm_device_start_ip_check(self); + + /* IP-related properties are only valid when the device has IP configuration; + * now that it does, ensure their change notifications are emitted. + */ + notify_ip_properties(self); + break; + } + case NM_DEVICE_STATE_SECONDARIES: + ip_check_gw_ping_cleanup(self); + _LOGD(LOGD_DEVICE, "device entered SECONDARIES state"); + break; + default: + break; + } + + if (state > NM_DEVICE_STATE_DISCONNECTED) + delete_on_deactivate_unschedule(self); + + if ((old_state == NM_DEVICE_STATE_ACTIVATED || old_state == NM_DEVICE_STATE_DEACTIVATING) + && (state != NM_DEVICE_STATE_DEACTIVATING)) { + if (quitting) { + nm_dispatcher_call_device_sync(NM_DISPATCHER_ACTION_DOWN, self, req); + } else { + nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DOWN, self, req, NULL, NULL, NULL); + } + } + + /* IP-related properties are only valid when the device has IP configuration. + * If it no longer does, ensure their change notifications are emitted. + */ + if (ip_config_valid(old_state) && !ip_config_valid(state)) + notify_ip_properties(self); + + concheck_now = NM_IN_SET(state, NM_DEVICE_STATE_ACTIVATED, NM_DEVICE_STATE_DISCONNECTED) + || old_state >= NM_DEVICE_STATE_ACTIVATED; + concheck_update_interval(self, AF_INET, concheck_now); + concheck_update_interval(self, AF_INET6, concheck_now); + + priv->in_state_changed = FALSE; + nm_device_remove_pending_action(self, NM_PENDING_ACTION_IN_STATE_CHANGE, TRUE); + + if ((old_state > NM_DEVICE_STATE_UNMANAGED) != (state > NM_DEVICE_STATE_UNMANAGED)) + _notify(self, PROP_MANAGED); +} + +void +nm_device_state_changed(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason) +{ + _set_state_full(self, state, reason, FALSE); +} + +static gboolean +queued_state_set(gpointer user_data) +{ + NMDevice * self = NM_DEVICE(user_data); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMDeviceState new_state; + NMDeviceStateReason new_reason; + + nm_assert(priv->queued_state.id); + + _LOGD(LOGD_DEVICE, + "queue-state[%s, reason:%s, id:%u]: %s", + nm_device_state_to_str(priv->queued_state.state), + reason_to_string_a(priv->queued_state.reason), + priv->queued_state.id, + "change state"); + + /* Clear queued state struct before triggering state change, since + * the state change may queue another state. + */ + priv->queued_state.id = 0; + new_state = priv->queued_state.state; + new_reason = priv->queued_state.reason; + + nm_device_state_changed(self, new_state, new_reason); + nm_device_remove_pending_action(self, queued_state_to_string(new_state), TRUE); + + return G_SOURCE_REMOVE; +} + +void +nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason) +{ + NMDevicePrivate *priv; + + g_return_if_fail(NM_IS_DEVICE(self)); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->queued_state.id && priv->queued_state.state == state) { + _LOGD(LOGD_DEVICE, + "queue-state[%s, reason:%s, id:%u]: %s%s%s%s", + nm_device_state_to_str(priv->queued_state.state), + reason_to_string_a(priv->queued_state.reason), + priv->queued_state.id, + "ignore queuing same state change", + NM_PRINT_FMT_QUOTED(priv->queued_state.reason != reason, + " (reason differs: ", + reason_to_string_a(reason), + ")", + "")); + return; + } + + /* Add pending action for the new state before clearing the queued states, so + * that we don't accidentally pop all pending states and reach 'startup complete' */ + nm_device_add_pending_action(self, queued_state_to_string(state), TRUE); + + /* We should only ever have one delayed state transition at a time */ + if (priv->queued_state.id) { + _LOGW(LOGD_DEVICE, + "queue-state[%s, reason:%s, id:%u]: %s", + nm_device_state_to_str(priv->queued_state.state), + reason_to_string_a(priv->queued_state.reason), + priv->queued_state.id, + "replace previously queued state change"); + nm_clear_g_source(&priv->queued_state.id); + nm_device_remove_pending_action(self, + queued_state_to_string(priv->queued_state.state), + TRUE); + } + + priv->queued_state.state = state; + priv->queued_state.reason = reason; + priv->queued_state.id = g_idle_add(queued_state_set, self); + + _LOGD(LOGD_DEVICE, + "queue-state[%s, reason:%s, id:%u]: %s", + nm_device_state_to_str(state), + reason_to_string_a(reason), + priv->queued_state.id, + "queue state change"); +} + +static void +queued_state_clear(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->queued_state.id) + return; + + _LOGD(LOGD_DEVICE, + "queue-state[%s, reason:%s, id:%u]: %s", + nm_device_state_to_str(priv->queued_state.state), + reason_to_string_a(priv->queued_state.reason), + priv->queued_state.id, + "clear queued state change"); + nm_clear_g_source(&priv->queued_state.id); + nm_device_remove_pending_action(self, queued_state_to_string(priv->queued_state.state), TRUE); +} + +NMDeviceState +nm_device_get_state(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_STATE_UNKNOWN); + + return NM_DEVICE_GET_PRIVATE(self)->state; +} + +/*****************************************************************************/ +/* NMConfigDevice interface related stuff */ + +const char * +nm_device_get_hw_address(NMDevice *self) +{ + NMDevicePrivate *priv; + char buf[NM_UTILS_HWADDR_LEN_MAX]; + gsize l; + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + priv = NM_DEVICE_GET_PRIVATE(self); + + nm_assert((!priv->hw_addr && priv->hw_addr_len == 0) + || (priv->hw_addr && _nm_utils_hwaddr_aton(priv->hw_addr, buf, sizeof(buf), &l) + && l == priv->hw_addr_len)); + + return priv->hw_addr; +} + +gboolean +nm_device_update_hw_address(NMDevice *self) +{ + NMDevicePrivate *priv; + const guint8 * hwaddr; + gsize hwaddrlen = 0; + + priv = NM_DEVICE_GET_PRIVATE(self); + if (priv->ifindex <= 0) + return FALSE; + + hwaddr = nm_platform_link_get_address(nm_device_get_platform(self), priv->ifindex, &hwaddrlen); + + if (priv->type == NM_DEVICE_TYPE_ETHERNET && hwaddr + && nm_utils_hwaddr_matches(hwaddr, + hwaddrlen, + &nm_ether_addr_zero, + sizeof(nm_ether_addr_zero))) + hwaddrlen = 0; + + if (!hwaddrlen) + return FALSE; + + if (priv->hw_addr_len && priv->hw_addr_len != hwaddrlen) { + char s_buf[NM_UTILS_HWADDR_LEN_MAX_STR]; + + /* we cannot change the address length of a device once it is set (except + * unrealizing the device). + * + * The reason is that the permanent and initial MAC addresses also must have the + * same address length, so it's unclear what it would mean that the length changes. */ + _LOGD(LOGD_PLATFORM | LOGD_DEVICE, + "hw-addr: read a MAC address with differing length (%s vs. %s)", + priv->hw_addr, + _nm_utils_hwaddr_ntoa(hwaddr, hwaddrlen, TRUE, s_buf, sizeof(s_buf))); + return FALSE; + } + + if (priv->hw_addr && nm_utils_hwaddr_matches(priv->hw_addr, -1, hwaddr, hwaddrlen)) + return FALSE; + + g_free(priv->hw_addr); + priv->hw_addr_len_ = hwaddrlen; + priv->hw_addr = nm_utils_hwaddr_ntoa(hwaddr, hwaddrlen); + + _LOGD(LOGD_PLATFORM | LOGD_DEVICE, "hw-addr: hardware address now %s", priv->hw_addr); + _notify(self, PROP_HW_ADDRESS); + + if (!priv->hw_addr_initial + || (priv->hw_addr_type == HW_ADDR_TYPE_UNSET && priv->state < NM_DEVICE_STATE_PREPARE + && !nm_device_is_activating(self))) { + /* when we get a hw_addr the first time or while the device + * is not activated (with no explicit hw address set), always + * update our initial hw-address as well. */ + nm_device_update_initial_hw_address(self); + } + return TRUE; +} + +void +nm_device_update_initial_hw_address(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->hw_addr && !nm_streq0(priv->hw_addr_initial, priv->hw_addr)) { + if (priv->hw_addr_initial && priv->hw_addr_type != HW_ADDR_TYPE_UNSET) { + /* once we have the initial hw address set, we only allow + * update if the currently type is "unset". */ + return; + } + g_free(priv->hw_addr_initial); + priv->hw_addr_initial = g_strdup(priv->hw_addr); + _LOGD(LOGD_DEVICE, "hw-addr: update initial MAC address %s", priv->hw_addr_initial); + } +} + +void +nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_freeze) +{ + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + guint8 buf[NM_UTILS_HWADDR_LEN_MAX]; + size_t len = 0; + gboolean success_read; + int ifindex; + const NMPlatformLink * pllink; + const NMConfigDeviceStateData *dev_state; + + if (priv->hw_addr_perm) { + /* the permanent hardware address is only read once and not + * re-read later. + * + * Except during unrealize/realize cycles, where we clear the permanent + * hardware address during unrealization. */ + return; + } + + ifindex = priv->ifindex; + if (ifindex <= 0) + return; + + /* the user is advised to configure stable MAC addresses for software devices via + * UDEV. Thus, check whether the link is fully initialized. */ + pllink = nm_platform_link_get(nm_device_get_platform(self), ifindex); + if (!pllink || !pllink->initialized) { + if (!force_freeze) { + /* we can afford to wait. Back off and leave the permanent MAC address + * undecided for now. */ + return; + } + /* try to refresh the link just to give UDEV a bit more time... */ + nm_platform_link_refresh(nm_device_get_platform(self), ifindex); + /* maybe the MAC address changed... */ + nm_device_update_hw_address(self); + } else if (!priv->hw_addr_len) + nm_device_update_hw_address(self); + + if (!priv->hw_addr_len) { + /* we need the current MAC address because we require the permanent MAC address + * to have the same length as the current address. + * + * Abort if there is no current MAC address. */ + return; + } + + success_read = + nm_platform_link_get_permanent_address(nm_device_get_platform(self), ifindex, buf, &len); + if (success_read && priv->hw_addr_len == len) { + priv->hw_addr_perm_fake = FALSE; + priv->hw_addr_perm = nm_utils_hwaddr_ntoa(buf, len); + _LOGD(LOGD_DEVICE, "hw-addr: read permanent MAC address '%s'", priv->hw_addr_perm); + goto notify_and_out; + } + + /* we failed to read a permanent MAC address, thus we use a fake address, + * that is the current MAC address of the device. + * + * Note that the permanet MAC address of a NMDevice instance does not change + * after being set once. Thus, we use now a fake address and stick to that + * (until we unrealize the device). */ + priv->hw_addr_perm_fake = TRUE; + + /* We also persist our choice of the fake address to the device state + * file to use the same address on restart of NetworkManager. + * First, try to reload the address from the state file. */ + dev_state = nm_config_device_state_get(nm_config_get(), ifindex); + if (dev_state && dev_state->perm_hw_addr_fake + && nm_utils_hwaddr_aton(dev_state->perm_hw_addr_fake, buf, priv->hw_addr_len) + && !nm_utils_hwaddr_matches(buf, priv->hw_addr_len, priv->hw_addr, -1)) { + _LOGD(LOGD_PLATFORM | LOGD_ETHER, + "hw-addr: %s (use from statefile: %s, current: %s)", + success_read ? "read HW addr length of permanent MAC address differs" + : "unable to read permanent MAC address", + dev_state->perm_hw_addr_fake, + priv->hw_addr); + priv->hw_addr_perm = nm_utils_hwaddr_ntoa(buf, priv->hw_addr_len); + goto notify_and_out; + } + + _LOGD(LOGD_PLATFORM | LOGD_ETHER, + "hw-addr: %s (use current: %s)", + success_read ? "read HW addr length of permanent MAC address differs" + : "unable to read permanent MAC address", + priv->hw_addr); + priv->hw_addr_perm = g_strdup(priv->hw_addr); + +notify_and_out: + _notify(self, PROP_PERM_HW_ADDRESS); +} + +gboolean +nm_device_hw_addr_is_explict(NMDevice *self) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + return !NM_IN_SET((HwAddrType) priv->hw_addr_type, HW_ADDR_TYPE_PERMANENT, HW_ADDR_TYPE_UNSET); +} + +static gboolean +_hw_addr_matches(NMDevice *self, const guint8 *addr, gsize addr_len) +{ + const char *cur_addr; + + cur_addr = nm_device_get_hw_address(self); + return cur_addr && nm_utils_hwaddr_matches(addr, addr_len, cur_addr, -1); +} + +static gboolean +_hw_addr_set(NMDevice * self, + const char *const addr, + const char *const operation, + const char *const detail) +{ + NMDevicePrivate *priv; + gboolean success = FALSE; + int r; + guint8 addr_bytes[NM_UTILS_HWADDR_LEN_MAX]; + gsize addr_len; + gboolean was_taken_down = FALSE; + gboolean retry_down; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(addr); + nm_assert(operation); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!_nm_utils_hwaddr_aton(addr, addr_bytes, sizeof(addr_bytes), &addr_len)) + g_return_val_if_reached(FALSE); + + /* Do nothing if current MAC is same */ + if (_hw_addr_matches(self, addr_bytes, addr_len)) { + _LOGT(LOGD_DEVICE, "set-hw-addr: no MAC address change needed (%s)", addr); + return TRUE; + } + + if (priv->hw_addr_len && priv->hw_addr_len != addr_len) { + _LOGT(LOGD_DEVICE, + "set-hw-addr: setting MAC address to '%s' (%s, %s) failed because of wrong address " + "length (should be %u bytes)", + addr, + operation, + detail, + priv->hw_addr_len); + return FALSE; + } + + _LOGT(LOGD_DEVICE, + "set-hw-addr: setting MAC address to '%s' (%s, %s)...", + addr, + operation, + detail); + + if (nm_device_get_device_type(self) == NM_DEVICE_TYPE_WIFI) { + /* Always take the device down for Wi-Fi because + * wpa_supplicant needs it to properly detect the MAC + * change. */ + retry_down = FALSE; + was_taken_down = TRUE; + nm_device_take_down(self, FALSE); + } + +again: + r = nm_platform_link_set_address(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self), + addr_bytes, + addr_len); + success = (r >= 0); + if (!success) { + retry_down = + !was_taken_down && r != -NME_PL_NOT_FOUND + && nm_platform_link_is_up(nm_device_get_platform(self), nm_device_get_ip_ifindex(self)); + _NMLOG((retry_down || r == -NME_PL_NOT_FOUND) ? LOGL_DEBUG : LOGL_WARN, + LOGD_DEVICE, + "set-hw-addr: failed to %s MAC address to %s (%s) (%s)%s", + operation, + addr, + detail, + nm_strerror(r), + retry_down ? " (retry with taking down)" : ""); + } else { + /* MAC address successfully changed; update the current MAC to match */ + nm_device_update_hw_address(self); + + if (!_hw_addr_matches(self, addr_bytes, addr_len)) { + gint64 poll_end, now; + + _LOGD(LOGD_DEVICE, + "set-hw-addr: new MAC address %s not successfully %s (%s) (refresh link)", + addr, + operation, + detail); + + /* The platform call indicated success, however the address is not + * as expected. That is either due to a driver issue (brcmfmac, bgo#770456, + * rh#1374023) or a race where externally the MAC address was reset. + * The race is rather unlikely. + * + * The alternative would be to postpone the activation in case the + * MAC address is not yet ready and poll without blocking. However, + * that is rather complicated and it is not expected that this case + * happens for regular drivers. + * Note that brcmfmac can block NetworkManager for 500 msec while + * taking down the device. Let's add another 100 msec to that. + * + * wait/poll up to 100 msec until it changes. */ + + poll_end = nm_utils_get_monotonic_timestamp_usec() + (100 * 1000); + for (;;) { + if (!nm_platform_link_refresh(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self))) + goto handle_fail; + if (!nm_device_update_hw_address(self)) + goto handle_wait; + if (!_hw_addr_matches(self, addr_bytes, addr_len)) + goto handle_fail; + + break; +handle_wait: + now = nm_utils_get_monotonic_timestamp_usec(); + if (now < poll_end) { + g_usleep(NM_MIN(poll_end - now, 500)); + continue; + } +handle_fail: + success = FALSE; + break; + } + } + + if (success) { + retry_down = FALSE; + _LOGI(LOGD_DEVICE, "set-hw-addr: %s MAC address to %s (%s)", operation, addr, detail); + } else { + retry_down = !was_taken_down + && nm_platform_link_is_up(nm_device_get_platform(self), + nm_device_get_ip_ifindex(self)); + + _NMLOG(retry_down ? LOGL_DEBUG : LOGL_WARN, + LOGD_DEVICE, + "set-hw-addr: new MAC address %s not successfully %s (%s)%s", + addr, + operation, + detail, + retry_down ? " (retry with taking down)" : ""); + } + } + + if (retry_down) { + /* changing the MAC address failed, but also the device was up (and we did not yet try to take + * it down). Optimally, we change the MAC address without taking the device down, but some + * devices don't like that. So, retry with taking the device down. */ + retry_down = FALSE; + was_taken_down = TRUE; + nm_device_take_down(self, FALSE); + goto again; + } + + if (was_taken_down) { + if (!nm_device_bring_up(self, TRUE, NULL)) + return FALSE; + } + + return success; +} + +gboolean +nm_device_hw_addr_set(NMDevice *self, const char *addr, const char *detail, gboolean set_permanent) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!addr) + g_return_val_if_reached(FALSE); + + if (set_permanent) { + /* The type is set to PERMANENT by NMDeviceVlan when taking the MAC + * address from the parent and by NMDeviceWifi when setting a random MAC + * address during scanning. + */ + priv->hw_addr_type = HW_ADDR_TYPE_PERMANENT; + } + + return _hw_addr_set(self, addr, "set", detail); +} + +/* + * _hw_addr_get_cloned: + * @self: a #NMDevice + * @connection: a #NMConnection + * @is_wifi: whether the device is Wi-Fi + * @preserve: (out): whether the address must be reset to initial one + * @hwaddr: (out): the cloned MAC address to set on interface + * @hwaddr_type: (out): the type of address to set + * @hwaddr_detail: (out): the detail (origin) of address to set + * @error: (out): on return, an error or %NULL + * + * Computes the MAC to be set on a interface. On success, one of the + * following exclusive conditions are verified: + * + * - @preserve is %TRUE: the address must be reset to the initial one + * - @hwaddr is not %NULL: the given address must be set on the device + * - @hwaddr is %NULL and @preserve is %FALSE: no action needed + * + * Returns: %FALSE in case of error in determining the cloned MAC address, + * %TRUE otherwise + */ +static gboolean +_hw_addr_get_cloned(NMDevice * self, + NMConnection *connection, + gboolean is_wifi, + gboolean * preserve, + char ** hwaddr, + HwAddrType * hwaddr_type, + char ** hwaddr_detail, + GError ** error) +{ + NMDevicePrivate *priv; + gs_free char * addr_setting_free = NULL; + gs_free char * hw_addr_generated = NULL; + gs_free char * generate_mac_address_mask_tmp = NULL; + const char * addr, *addr_setting; + char * addr_out; + HwAddrType type_out; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!connection) + g_return_val_if_reached(FALSE); + + addr = addr_setting = + _prop_get_x_cloned_mac_address(self, connection, is_wifi, &addr_setting_free); + + if (nm_streq(addr, NM_CLONED_MAC_PRESERVE)) { + /* "preserve" means to reset the initial MAC address. */ + NM_SET_OUT(preserve, TRUE); + NM_SET_OUT(hwaddr, NULL); + NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET); + NM_SET_OUT(hwaddr_detail, g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting)); + return TRUE; + } + + if (nm_streq(addr, NM_CLONED_MAC_PERMANENT)) { + gboolean is_fake; + + addr = nm_device_get_permanent_hw_address_full(self, TRUE, &is_fake); + if (is_fake) { + /* Preserve the current address if the permanent address if fake */ + NM_SET_OUT(preserve, TRUE); + NM_SET_OUT(hwaddr, NULL); + NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET); + NM_SET_OUT(hwaddr_detail, + g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting)); + return TRUE; + } else if (!addr) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "failed to retrieve permanent address"); + return FALSE; + } + addr_out = g_strdup(addr); + type_out = HW_ADDR_TYPE_PERMANENT; + } else if (NM_IN_STRSET(addr, NM_CLONED_MAC_RANDOM)) { + if (priv->hw_addr_type == HW_ADDR_TYPE_GENERATED) { + /* hm, we already use a generate MAC address. Most certainly, that is from the same + * activation request, so we should not create a new random address, instead keep + * the current. */ + goto out_no_action; + } + hw_addr_generated = nm_utils_hw_addr_gen_random_eth( + nm_device_get_initial_hw_address(self), + _prop_get_x_generate_mac_address_mask(self, + connection, + is_wifi, + &generate_mac_address_mask_tmp)); + if (!hw_addr_generated) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "failed to generate %s MAC address", + "random"); + return FALSE; + } + + addr_out = g_steal_pointer(&hw_addr_generated); + type_out = HW_ADDR_TYPE_GENERATED; + } else if (NM_IN_STRSET(addr, NM_CLONED_MAC_STABLE)) { + NMUtilsStableType stable_type; + const char * stable_id; + + if (priv->hw_addr_type == HW_ADDR_TYPE_GENERATED) { + /* hm, we already use a generate MAC address. Most certainly, that is from the same + * activation request, so let's skip creating the stable address anew. */ + goto out_no_action; + } + + stable_id = _prop_get_connection_stable_id(self, connection, &stable_type); + hw_addr_generated = nm_utils_hw_addr_gen_stable_eth( + stable_type, + stable_id, + nm_device_get_ip_iface(self), + nm_device_get_initial_hw_address(self), + _prop_get_x_generate_mac_address_mask(self, + connection, + is_wifi, + &generate_mac_address_mask_tmp)); + if (!hw_addr_generated) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "failed to generate %s MAC address", + "stable"); + return FALSE; + } + + addr_out = g_steal_pointer(&hw_addr_generated); + type_out = HW_ADDR_TYPE_GENERATED; + } else { + /* this must be a valid address. Otherwise, we shouldn't come here. */ + if (!nm_utils_hwaddr_valid(addr, -1)) + g_return_val_if_reached(FALSE); + + addr_out = g_strdup(addr); + type_out = HW_ADDR_TYPE_EXPLICIT; + } + + NM_SET_OUT(preserve, FALSE); + NM_SET_OUT(hwaddr, addr_out); + NM_SET_OUT(hwaddr_type, type_out); + NM_SET_OUT(hwaddr_detail, g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting)); + return TRUE; +out_no_action: + NM_SET_OUT(preserve, FALSE); + NM_SET_OUT(hwaddr, NULL); + NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET); + NM_SET_OUT(hwaddr_detail, NULL); + return TRUE; +} + +gboolean +nm_device_hw_addr_get_cloned(NMDevice * self, + NMConnection *connection, + gboolean is_wifi, + char ** hwaddr, + gboolean * preserve, + GError ** error) +{ + if (!_hw_addr_get_cloned(self, connection, is_wifi, preserve, hwaddr, NULL, NULL, error)) + return FALSE; + + return TRUE; +} + +gboolean +nm_device_hw_addr_set_cloned(NMDevice *self, NMConnection *connection, gboolean is_wifi) +{ + NMDevicePrivate *priv; + gboolean preserve = FALSE; + gs_free char * hwaddr = NULL; + gs_free char * detail = NULL; + HwAddrType type = HW_ADDR_TYPE_UNSET; + gs_free_error GError *error = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!_hw_addr_get_cloned(self, + connection, + is_wifi, + &preserve, + &hwaddr, + &type, + &detail, + &error)) { + _LOGW(LOGD_DEVICE, "set-hw-addr: %s", error->message); + return FALSE; + } + + if (preserve) + return nm_device_hw_addr_reset(self, detail); + + if (hwaddr) { + priv->hw_addr_type = type; + return _hw_addr_set(self, hwaddr, "set-cloned", detail); + } + + return TRUE; +} + +gboolean +nm_device_hw_addr_reset(NMDevice *self, const char *detail) +{ + NMDevicePrivate *priv; + const char * addr; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->hw_addr_type == HW_ADDR_TYPE_UNSET) + return TRUE; + + priv->hw_addr_type = HW_ADDR_TYPE_UNSET; + addr = nm_device_get_initial_hw_address(self); + if (!addr) { + /* as hw_addr_type is not UNSET, we expect that we can get an + * initial address to which to reset. */ + g_return_val_if_reached(FALSE); + } + + return _hw_addr_set(self, addr, "reset", detail); +} + +const char * +nm_device_get_permanent_hw_address_full(NMDevice *self, + gboolean force_freeze, + gboolean *out_is_fake) +{ + NMDevicePrivate *priv; + + g_return_val_if_fail(NM_IS_DEVICE(self), ({ + NM_SET_OUT(out_is_fake, FALSE); + NULL; + })); + + priv = NM_DEVICE_GET_PRIVATE(self); + + if (!priv->hw_addr_perm && force_freeze) { + /* somebody requests a permanent MAC address, but we don't have it set + * yet. We cannot delay it any longer and try to get it without waiting + * for UDEV. */ + nm_device_update_permanent_hw_address(self, TRUE); + } + + NM_SET_OUT(out_is_fake, priv->hw_addr_perm && priv->hw_addr_perm_fake); + return priv->hw_addr_perm; +} + +const char * +nm_device_get_permanent_hw_address(NMDevice *self) +{ + return nm_device_get_permanent_hw_address_full(self, TRUE, NULL); +} + +const char * +nm_device_get_initial_hw_address(NMDevice *self) +{ + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + + return NM_DEVICE_GET_PRIVATE(self)->hw_addr_initial; +} + +/** + * nm_device_spec_match_list: + * @self: an #NMDevice + * @specs: (element-type utf8): a list of device specs + * + * Checks if @self matches any of the specifications in @specs. The + * currently-supported spec types are: + * + * "mac:00:11:22:33:44:55" - matches a device with the given + * hardware address + * + * "interface-name:foo0" - matches a device with the given + * interface name + * + * "s390-subchannels:00.11.22" - matches a device with the given + * z/VM / s390 subchannels. + * + * "*" - matches any device + * + * Returns: #TRUE if @self matches one of the specs in @specs + */ +gboolean +nm_device_spec_match_list(NMDevice *self, const GSList *specs) +{ + return nm_device_spec_match_list_full(self, specs, FALSE); +} + +int +nm_device_spec_match_list_full(NMDevice *self, const GSList *specs, int no_match_value) +{ + NMDeviceClass * klass; + NMMatchSpecMatchType m; + const char * hw_address = NULL; + gboolean is_fake; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + klass = NM_DEVICE_GET_CLASS(self); + hw_address = nm_device_get_permanent_hw_address_full( + self, + !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT), + &is_fake); + + m = nm_match_spec_device(specs, + nm_device_get_iface(self), + nm_device_get_type_description(self), + nm_device_get_driver(self), + nm_device_get_driver_version(self), + is_fake ? NULL : hw_address, + klass->get_s390_subchannels ? klass->get_s390_subchannels(self) : NULL, + nm_dhcp_manager_get_config(nm_dhcp_manager_get())); + + switch (m) { + case NM_MATCH_SPEC_MATCH: + return TRUE; + case NM_MATCH_SPEC_NEG_MATCH: + return FALSE; + case NM_MATCH_SPEC_NO_MATCH: + return no_match_value; + } + nm_assert_not_reached(); + return no_match_value; +} + +guint +nm_device_get_supplicant_timeout(NMDevice *self) +{ + NMConnection * connection; + NMSetting8021x *s_8021x; + int timeout; +#define SUPPLICANT_DEFAULT_TIMEOUT 25 + + g_return_val_if_fail(NM_IS_DEVICE(self), SUPPLICANT_DEFAULT_TIMEOUT); + + connection = nm_device_get_applied_connection(self); + + g_return_val_if_fail(connection, SUPPLICANT_DEFAULT_TIMEOUT); + + s_8021x = nm_connection_get_setting_802_1x(connection); + if (s_8021x) { + timeout = nm_setting_802_1x_get_auth_timeout(s_8021x); + if (timeout > 0) + return timeout; + } + + return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("802-1x.auth-timeout"), + self, + 1, + G_MAXINT32, + SUPPLICANT_DEFAULT_TIMEOUT); +} + +gboolean +nm_device_auth_retries_try_next(NMDevice *self) +{ + NMDevicePrivate * priv; + NMSettingConnection *s_con; + int auth_retries; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + auth_retries = priv->auth_retries; + + if (G_UNLIKELY(auth_retries == NM_DEVICE_AUTH_RETRIES_UNSET)) { + auth_retries = -1; + + s_con = nm_device_get_applied_setting(self, NM_TYPE_SETTING_CONNECTION); + if (s_con) + auth_retries = nm_setting_connection_get_auth_retries(s_con); + + if (auth_retries == -1) { + auth_retries = nm_config_data_get_connection_default_int64( + NM_CONFIG_GET_DATA, + NM_CON_DEFAULT("connection.auth-retries"), + self, + -1, + G_MAXINT32, + -1); + } + + if (auth_retries == 0) + auth_retries = NM_DEVICE_AUTH_RETRIES_INFINITY; + else if (auth_retries == -1) + auth_retries = NM_DEVICE_AUTH_RETRIES_DEFAULT; + else + nm_assert(auth_retries > 0); + + priv->auth_retries = auth_retries; + } + + if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY) + return TRUE; + if (auth_retries <= 0) { + nm_assert(auth_retries == 0); + return FALSE; + } + priv->auth_retries--; + return TRUE; +} + +static void +hostname_dns_lookup_callback(GObject *source, GAsyncResult *result, gpointer user_data) +{ + HostnameResolver *resolver; + NMDevice * self; + gs_free char * hostname = NULL; + gs_free char * addr_str = NULL; + gs_free_error GError *error = NULL; + + hostname = g_resolver_lookup_by_address_finish(G_RESOLVER(source), result, &error); + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + resolver = user_data; + self = resolver->device; + resolver->state = RESOLVER_DONE; + resolver->hostname = g_strdup(hostname); + + _LOGD(LOGD_DNS, + "hostname-from-dns: lookup done for %s, result %s%s%s", + (addr_str = g_inet_address_to_string(resolver->address)), + NM_PRINT_FMT_QUOTE_STRING(hostname)); + + nm_clear_g_cancellable(&resolver->cancellable); + g_signal_emit(self, signals[DNS_LOOKUP_DONE], 0); +} + +static gboolean +hostname_dns_address_timeout(gpointer user_data) +{ + HostnameResolver *resolver = user_data; + NMDevice * self = resolver->device; + + g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE); + + nm_assert(resolver->state == RESOLVER_WAIT_ADDRESS); + nm_assert(!resolver->address); + nm_assert(!resolver->cancellable); + + _LOGT(LOGD_DNS, + "hostname-from-dns: timed out while waiting IPv%c address", + nm_utils_addr_family_to_char(resolver->addr_family)); + + resolver->timeout_id = 0; + resolver->state = RESOLVER_DONE; + g_signal_emit(self, signals[DNS_LOOKUP_DONE], 0); + + return G_SOURCE_REMOVE; +} + +static const char * +_resolver_state_to_string(ResolverState state) +{ + switch (state) { + case RESOLVER_WAIT_ADDRESS: + return "wait-address"; + case RESOLVER_IN_PROGRESS: + return "in-progress"; + case RESOLVER_DONE: + return "done"; + default: + nm_assert_not_reached(); + return "unknown"; + } +} + +void +nm_device_clear_dns_lookup_data(NMDevice *self) +{ + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + guint i; + + for (i = 0; i < 2; i++) + nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free); +} + +/* return value is valid only immediately */ +const char * +nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean *out_wait) +{ + NMDevicePrivate * priv; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + HostnameResolver *resolver; + NMIPConfig * ip_config; + const char * method; + gboolean address_changed = FALSE; + gs_unref_object GInetAddress *new_address = NULL; + + g_return_val_if_fail(NM_IS_DEVICE(self), NULL); + priv = NM_DEVICE_GET_PRIVATE(self); + + /* If the device is not supposed to have addresses, + * return an immediate empty result.*/ + if (!nm_device_get_applied_connection(self)) { + NM_SET_OUT(out_wait, FALSE); + return NULL; + } + + method = nm_device_get_effective_ip_config_method(self, addr_family); + if (IS_IPv4) { + if (NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) { + nm_clear_pointer(&priv->hostname_resolver_x[IS_IPv4], _hostname_resolver_free); + NM_SET_OUT(out_wait, FALSE); + return NULL; + } + } else { + if (NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED, + NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) { + nm_clear_pointer(&priv->hostname_resolver_x[IS_IPv4], _hostname_resolver_free); + NM_SET_OUT(out_wait, FALSE); + return NULL; + } + } + + resolver = priv->hostname_resolver_x[IS_IPv4]; + if (!resolver) { + resolver = g_slice_new(HostnameResolver); + *resolver = (HostnameResolver){ + .resolver = g_resolver_get_default(), + .device = self, + .addr_family = addr_family, + .state = RESOLVER_WAIT_ADDRESS, + }; + priv->hostname_resolver_x[IS_IPv4] = resolver; + } + + /* Determine the first address of the interface and + * whether it changed from the previous lookup */ + ip_config = priv->ip_config_x[IS_IPv4]; + if (ip_config) { + const NMPlatformIPAddress *addr; + + addr = nm_ip_config_get_first_address(ip_config); + if (addr) { + new_address = g_inet_address_new_from_bytes(addr->address_ptr, + IS_IPv4 ? G_SOCKET_FAMILY_IPV4 + : G_SOCKET_FAMILY_IPV6); + } + } + + if (new_address && resolver->address) { + if (!g_inet_address_equal(new_address, resolver->address)) + address_changed = TRUE; + } else if (new_address != resolver->address) + address_changed = TRUE; + + { + gs_free char *old_str = NULL; + gs_free char *new_str = NULL; + + _LOGT(LOGD_DNS, + "hostname-from-dns: ipv%c resolver state %s, old address %s, new address %s", + nm_utils_addr_family_to_char(resolver->addr_family), + _resolver_state_to_string(resolver->state), + resolver->address ? (old_str = g_inet_address_to_string(resolver->address)) + : "(null)", + new_address ? (new_str = g_inet_address_to_string(new_address)) : "(null)"); + } + + /* In every state, if the address changed, we restart + * the resolution with the new address */ + if (address_changed) { + nm_clear_g_cancellable(&resolver->cancellable); + g_clear_object(&resolver->address); + resolver->state = RESOLVER_WAIT_ADDRESS; + } + + if (address_changed && new_address) { + gs_free char *str = NULL; + + _LOGT(LOGD_DNS, + "hostname-from-dns: starting lookup for address %s", + (str = g_inet_address_to_string(new_address))); + + resolver->state = RESOLVER_IN_PROGRESS; + resolver->cancellable = g_cancellable_new(); + resolver->address = g_steal_pointer(&new_address); + g_resolver_lookup_by_address_async(resolver->resolver, + resolver->address, + resolver->cancellable, + hostname_dns_lookup_callback, + resolver); + nm_clear_g_source(&resolver->timeout_id); + } + + switch (resolver->state) { + case RESOLVER_WAIT_ADDRESS: + if (!resolver->timeout_id) + resolver->timeout_id = g_timeout_add(30000, hostname_dns_address_timeout, resolver); + NM_SET_OUT(out_wait, TRUE); + return NULL; + case RESOLVER_IN_PROGRESS: + NM_SET_OUT(out_wait, TRUE); + return NULL; + case RESOLVER_DONE: + NM_SET_OUT(out_wait, FALSE); + return resolver->hostname; + } + + return nm_assert_unreachable_val(NULL); +} + +/*****************************************************************************/ + +static const char * +_activation_func_to_string(ActivationHandleFunc func) +{ +#define FUNC_TO_STRING_CHECK_AND_RETURN(func, f) \ + G_STMT_START \ + { \ + if ((func) == (f)) \ + return #f; \ + } \ + G_STMT_END + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage1_device_prepare); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage2_device_config); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage3_ip_config_start); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage4_ip_config_timeout_4); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage4_ip_config_timeout_6); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage5_ip_config_result_4); + FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage5_ip_config_result_6); + g_return_val_if_reached("unknown"); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDevice * self = NM_DEVICE(object); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + GVariantBuilder array_builder; + + switch (prop_id) { + case PROP_UDI: + /* UDI is (depending on the device type) a path to sysfs and can contain + * non-UTF-8. + * ip link add name $'d\xccf\\c' type dummy */ + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->udi, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE)); + break; + case PROP_PATH: + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->path, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE)); + break; + case PROP_IFACE: + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->iface, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)); + break; + case PROP_IP_IFACE: + if (ip_config_valid(priv->state)) { + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(nm_device_get_ip_iface(self), + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)); + } else + g_value_set_string(value, NULL); + break; + case PROP_IFINDEX: + g_value_set_int(value, priv->ifindex); + break; + case PROP_DRIVER: + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->driver, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)); + break; + case PROP_DRIVER_VERSION: + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->driver_version, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)); + break; + case PROP_FIRMWARE_VERSION: + g_value_take_string( + value, + nm_utils_str_utf8safe_escape_cp(priv->firmware_version, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)); + break; + case PROP_CAPABILITIES: + g_value_set_uint(value, (priv->capabilities & ~NM_DEVICE_CAP_INTERNAL_MASK)); + break; + case PROP_IP4_ADDRESS: + g_value_set_variant(value, nm_g_variant_singleton_u_0()); + break; + case PROP_CARRIER: + g_value_set_boolean(value, priv->carrier); + break; + case PROP_MTU: + g_value_set_uint(value, priv->mtu); + break; + case PROP_IP4_CONFIG: + nm_dbus_utils_g_value_set_object_path(value, + ip_config_valid(priv->state) ? priv->ip_config_4 + : NULL); + break; + case PROP_DHCP4_CONFIG: + nm_dbus_utils_g_value_set_object_path( + value, + ip_config_valid(priv->state) ? priv->dhcp_data_4.config : NULL); + break; + case PROP_IP6_CONFIG: + nm_dbus_utils_g_value_set_object_path(value, + ip_config_valid(priv->state) ? priv->ip_config_6 + : NULL); + break; + case PROP_DHCP6_CONFIG: + nm_dbus_utils_g_value_set_object_path( + value, + ip_config_valid(priv->state) ? priv->dhcp_data_6.config : NULL); + break; + case PROP_STATE: + g_value_set_uint(value, priv->state); + break; + case PROP_STATE_REASON: + g_value_take_variant(value, g_variant_new("(uu)", priv->state, priv->state_reason)); + break; + case PROP_ACTIVE_CONNECTION: + g_value_set_string(value, nm_dbus_track_obj_path_get(&priv->act_request)); + break; + case PROP_DEVICE_TYPE: + g_value_set_uint(value, priv->type); + break; + case PROP_LINK_TYPE: + g_value_set_uint(value, priv->link_type); + break; + case PROP_MANAGED: + /* The managed state exposed on D-Bus only depends on the current device state alone. */ + g_value_set_boolean(value, nm_device_get_state(self) > NM_DEVICE_STATE_UNMANAGED); + break; + case PROP_AUTOCONNECT: + g_value_set_boolean( + value, + nm_device_autoconnect_blocked_get(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL) ? FALSE + : TRUE); + break; + case PROP_FIRMWARE_MISSING: + g_value_set_boolean(value, priv->firmware_missing); + break; + case PROP_NM_PLUGIN_MISSING: + g_value_set_boolean(value, priv->nm_plugin_missing); + break; + case PROP_TYPE_DESC: + g_value_set_string(value, priv->type_desc); + break; + case PROP_RFKILL_TYPE: + g_value_set_uint(value, priv->rfkill_type); + break; + case PROP_AVAILABLE_CONNECTIONS: + nm_dbus_utils_g_value_set_object_path_from_hash(value, priv->available_connections, TRUE); + break; + case PROP_PHYSICAL_PORT_ID: + g_value_set_string(value, priv->physical_port_id); + break; + case PROP_MASTER: + g_value_set_object(value, nm_device_get_master(self)); + break; + case PROP_PARENT: + g_value_set_string(value, nm_dbus_track_obj_path_get(&priv->parent_device)); + break; + case PROP_HW_ADDRESS: + g_value_set_string(value, priv->hw_addr); + break; + case PROP_PERM_HW_ADDRESS: + { + const char *perm_hw_addr; + gboolean perm_hw_addr_is_fake; + + perm_hw_addr = nm_device_get_permanent_hw_address_full(self, FALSE, &perm_hw_addr_is_fake); + /* this property is exposed on D-Bus for NMDeviceEthernet and NMDeviceWifi. */ + g_value_set_string(value, perm_hw_addr && !perm_hw_addr_is_fake ? perm_hw_addr : NULL); + break; + } + case PROP_HAS_PENDING_ACTION: + g_value_set_boolean(value, nm_device_has_pending_action(self)); + break; + case PROP_METERED: + g_value_set_uint(value, priv->metered); + break; + case PROP_LLDP_NEIGHBORS: + if (priv->lldp_listener) + g_value_set_variant(value, nm_lldp_listener_get_neighbors(priv->lldp_listener)); + else { + g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aa{sv}")); + g_value_take_variant(value, g_variant_builder_end(&array_builder)); + } + break; + case PROP_REAL: + g_value_set_boolean(value, nm_device_is_real(self)); + break; + case PROP_SLAVES: + { + CList *slave_iter; + char **slave_list; + gsize i, n; + + n = c_list_length(&priv->slaves); + slave_list = g_new(char *, n + 1); + i = 0; + c_list_for_each (slave_iter, &priv->slaves) { + SlaveInfo * info = c_list_entry(slave_iter, SlaveInfo, lst_slave); + const char *path; + + if (!NM_DEVICE_GET_PRIVATE(info->slave)->is_enslaved) + continue; + path = nm_dbus_object_get_path(NM_DBUS_OBJECT(info->slave)); + if (path) + slave_list[i++] = g_strdup(path); + } + nm_assert(i <= n); + slave_list[i] = NULL; + g_value_take_boxed(value, slave_list); + break; + } + case PROP_STATISTICS_REFRESH_RATE_MS: + g_value_set_uint(value, priv->stats.refresh_rate_ms); + break; + case PROP_STATISTICS_TX_BYTES: + g_value_set_uint64(value, priv->stats.tx_bytes); + break; + case PROP_STATISTICS_RX_BYTES: + g_value_set_uint64(value, priv->stats.rx_bytes); + break; + case PROP_IP4_CONNECTIVITY: + g_value_set_uint(value, priv->concheck_x[1].state); + break; + case PROP_IP6_CONNECTIVITY: + g_value_set_uint(value, priv->concheck_x[0].state); + break; + case PROP_INTERFACE_FLAGS: + g_value_set_uint(value, priv->interface_flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDevice * self = (NMDevice *) object; + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_UDI: + /* construct-only */ + priv->udi = g_value_dup_string(value); + break; + case PROP_IFACE: + /* construct-only */ + priv->iface_ = g_value_dup_string(value); + break; + case PROP_DRIVER: + /* construct-only */ + priv->driver = g_value_dup_string(value); + break; + case PROP_MANAGED: + /* via D-Bus */ + if (nm_device_is_real(self)) { + gboolean managed; + NMDeviceStateReason reason; + + managed = g_value_get_boolean(value); + if (managed) { + reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED; + if (NM_IN_SET_TYPED(NMDeviceSysIfaceState, + priv->sys_iface_state, + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_REMOVED)) + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_ASSUME); + } else { + reason = NM_DEVICE_STATE_REASON_REMOVED; + nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_REMOVED); + } + nm_device_set_unmanaged_by_flags(self, NM_UNMANAGED_USER_EXPLICIT, !managed, reason); + } + break; + case PROP_AUTOCONNECT: + /* via D-Bus */ + if (g_value_get_boolean(value)) + nm_device_autoconnect_blocked_unset(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL); + else + nm_device_autoconnect_blocked_set(self, NM_DEVICE_AUTOCONNECT_BLOCKED_USER); + break; + case PROP_NM_PLUGIN_MISSING: + /* construct-only */ + priv->nm_plugin_missing = g_value_get_boolean(value); + break; + case PROP_DEVICE_TYPE: + /* construct-only */ + nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN); + priv->type = g_value_get_uint(value); + nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN); + nm_assert(priv->type <= NM_DEVICE_TYPE_VRF); + break; + case PROP_LINK_TYPE: + /* construct-only */ + nm_assert(priv->link_type == NM_LINK_TYPE_NONE); + priv->link_type = g_value_get_uint(value); + break; + case PROP_TYPE_DESC: + /* construct-only */ + priv->type_desc = g_value_dup_string(value); + break; + case PROP_RFKILL_TYPE: + /* construct-only */ + priv->rfkill_type = g_value_get_uint(value); + break; + case PROP_PERM_HW_ADDRESS: + /* construct-only */ + priv->hw_addr_perm = g_value_dup_string(value); + break; + case PROP_STATISTICS_REFRESH_RATE_MS: + /* via D-Bus */ + _stats_set_refresh_rate(self, g_value_get_uint(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_init(NMDevice *self) +{ + NMDevicePrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DEVICE, NMDevicePrivate); + + self->_priv = priv; + + c_list_init(&priv->concheck_lst_head); + c_list_init(&self->devices_lst); + c_list_init(&priv->slaves); + + priv->concheck_x[0].state = NM_CONNECTIVITY_UNKNOWN; + priv->concheck_x[1].state = NM_CONNECTIVITY_UNKNOWN; + + nm_dbus_track_obj_path_init(&priv->parent_device, G_OBJECT(self), obj_properties[PROP_PARENT]); + nm_dbus_track_obj_path_init(&priv->act_request, + G_OBJECT(self), + obj_properties[PROP_ACTIVE_CONNECTION]); + + priv->netns = g_object_ref(NM_NETNS_GET); + + priv->autoconnect_blocked_flags = DEFAULT_AUTOCONNECT ? NM_DEVICE_AUTOCONNECT_BLOCKED_NONE + : NM_DEVICE_AUTOCONNECT_BLOCKED_USER; + + priv->auth_retries = NM_DEVICE_AUTH_RETRIES_UNSET; + priv->type = NM_DEVICE_TYPE_UNKNOWN; + priv->capabilities = NM_DEVICE_CAP_NM_SUPPORTED; + priv->state = NM_DEVICE_STATE_UNMANAGED; + priv->state_reason = NM_DEVICE_STATE_REASON_NONE; + priv->rfkill_type = RFKILL_TYPE_UNKNOWN; + priv->unmanaged_flags = NM_UNMANAGED_PLATFORM_INIT; + priv->unmanaged_mask = priv->unmanaged_flags; + priv->available_connections = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL); + priv->ip6_saved_properties = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free); + priv->sys_iface_state_ = NM_DEVICE_SYS_IFACE_STATE_EXTERNAL; + + priv->v4_commit_first_time = TRUE; + priv->v6_commit_first_time = TRUE; +} + +static GObject * +constructor(GType type, guint n_construct_params, GObjectConstructParam *construct_params) +{ + GObject * object; + GObjectClass * klass; + NMDevice * self; + NMDevicePrivate * priv; + const NMPlatformLink *pllink; + + klass = G_OBJECT_CLASS(nm_device_parent_class); + object = klass->constructor(type, n_construct_params, construct_params); + if (!object) + return NULL; + + self = NM_DEVICE(object); + priv = NM_DEVICE_GET_PRIVATE(self); + + if (priv->iface && G_LIKELY(!nm_utils_get_testing())) { + pllink = nm_platform_link_get_by_ifname(nm_device_get_platform(self), priv->iface); + + if (pllink && link_type_compatible(self, pllink->type, NULL, NULL)) { + _set_ifindex(self, pllink->ifindex, FALSE); + priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP); + } + } + + if (priv->hw_addr_perm) { + guint8 buf[NM_UTILS_HWADDR_LEN_MAX]; + gsize l; + + if (!_nm_utils_hwaddr_aton(priv->hw_addr_perm, buf, sizeof(buf), &l)) { + nm_clear_g_free(&priv->hw_addr_perm); + g_return_val_if_reached(object); + } + + priv->hw_addr_len_ = l; + priv->hw_addr = nm_utils_hwaddr_ntoa(buf, l); + _LOGT(LOGD_DEVICE, "hw-addr: has permanent hw-address '%s'", priv->hw_addr_perm); + } + + return object; +} + +static void +constructed(GObject *object) +{ + NMDevice * self = NM_DEVICE(object); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatform * platform; + + if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities) + priv->capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self); + + /* Watch for external IP config changes */ + platform = nm_device_get_platform(self); + g_signal_connect(platform, + NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, + G_CALLBACK(device_ipx_changed), + self); + g_signal_connect(platform, + NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, + G_CALLBACK(device_ipx_changed), + self); + g_signal_connect(platform, + NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, + G_CALLBACK(device_ipx_changed), + self); + g_signal_connect(platform, + NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, + G_CALLBACK(device_ipx_changed), + self); + g_signal_connect(platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, G_CALLBACK(link_changed_cb), self); + + priv->manager = g_object_ref(NM_MANAGER_GET); + priv->settings = g_object_ref(NM_SETTINGS_GET); + + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_ADDED, + G_CALLBACK(cp_connection_added), + self); + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_UPDATED, + G_CALLBACK(cp_connection_updated), + self); + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_REMOVED, + G_CALLBACK(cp_connection_removed), + self); + + G_OBJECT_CLASS(nm_device_parent_class)->constructed(object); + + _LOGD(LOGD_DEVICE, "constructed (%s)", G_OBJECT_TYPE_NAME(self)); +} + +static void +dispose(GObject *object) +{ + NMDevice * self = NM_DEVICE(object); + NMDevicePrivate * priv = NM_DEVICE_GET_PRIVATE(self); + NMPlatform * platform; + NMDeviceConnectivityHandle *con_handle; + gs_free_error GError *cancelled_error = NULL; + + _LOGD(LOGD_DEVICE, "disposing"); + + nm_assert(c_list_is_empty(&self->devices_lst)); + + while ((con_handle = c_list_first_entry(&priv->concheck_lst_head, + NMDeviceConnectivityHandle, + concheck_lst))) { + if (!cancelled_error) + nm_utils_error_set_cancelled(&cancelled_error, FALSE, "NMDevice"); + concheck_handle_complete(con_handle, cancelled_error); + } + + nm_clear_g_cancellable(&priv->deactivating_cancellable); + + nm_device_assume_state_reset(self); + + _parent_set_ifindex(self, 0, FALSE); + + platform = nm_device_get_platform(self); + g_signal_handlers_disconnect_by_func(platform, G_CALLBACK(device_ipx_changed), self); + g_signal_handlers_disconnect_by_func(platform, G_CALLBACK(link_changed_cb), self); + + arp_cleanup(self); + + nm_clear_g_signal_handler(nm_config_get(), &priv->config_changed_id); + nm_clear_g_signal_handler(priv->manager, &priv->ifindex_changed_id); + + dispatcher_cleanup(self); + + nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id); + + _cleanup_generic_pre(self, CLEANUP_TYPE_KEEP); + + nm_assert(c_list_is_empty(&priv->slaves)); + + /* Let the kernel manage IPv6LL again */ + set_nm_ipv6ll(self, FALSE); + + _cleanup_generic_post(self, CLEANUP_TYPE_KEEP); + + nm_assert(priv->master_ready_id == 0); + + g_hash_table_remove_all(priv->ip6_saved_properties); + + nm_clear_g_source(&priv->recheck_assume_id); + nm_clear_g_source(&priv->recheck_available.call_id); + + nm_clear_g_source(&priv->check_delete_unrealized_id); + + nm_clear_g_source(&priv->stats.timeout_id); + + carrier_disconnected_action_cancel(self); + + _set_ifindex(self, 0, FALSE); + _set_ifindex(self, 0, TRUE); + + if (priv->settings) { + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_added, self); + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_updated, self); + g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_removed, self); + } + + available_connections_del_all(self); + + if (nm_clear_g_source(&priv->carrier_wait_id)) + nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE); + + _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED); + + nm_clear_g_source(&priv->device_link_changed_id); + nm_clear_g_source(&priv->device_ip_link_changed_id); + + if (priv->lldp_listener) { + g_signal_handlers_disconnect_by_func(priv->lldp_listener, + G_CALLBACK(lldp_neighbors_changed), + self); + nm_lldp_listener_stop(priv->lldp_listener); + g_clear_object(&priv->lldp_listener); + } + + nm_clear_g_source(&priv->concheck_x[0].p_cur_id); + nm_clear_g_source(&priv->concheck_x[1].p_cur_id); + + nm_assert(!priv->sriov.pending); + if (priv->sriov.next) { + nm_g_slice_free(priv->sriov.next); + priv->sriov.next = NULL; + } + + G_OBJECT_CLASS(nm_device_parent_class)->dispose(object); + + if (nm_clear_g_source(&priv->queued_state.id)) { + /* FIXME: we'd expect the queud_state to be already cleared and this statement + * not being necessary. Add this check here to hopefully investigate crash + * rh#1270247. */ + g_return_if_reached(); + } +} + +static void +finalize(GObject *object) +{ + NMDevice * self = NM_DEVICE(object); + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + + _LOGD(LOGD_DEVICE, "finalize(): %s", G_OBJECT_TYPE_NAME(self)); + + g_free(priv->hw_addr); + g_free(priv->hw_addr_perm); + g_free(priv->hw_addr_initial); + g_slist_free(priv->pending_actions); + g_slist_free_full(priv->dad6_failed_addrs, (GDestroyNotify) nmp_object_unref); + nm_clear_g_free(&priv->physical_port_id); + g_free(priv->udi); + g_free(priv->path); + g_free(priv->iface_); + g_free(priv->ip_iface_); + g_free(priv->driver); + g_free(priv->driver_version); + g_free(priv->firmware_version); + g_free(priv->type_desc); + g_free(priv->dhcp_anycast_address); + g_free(priv->current_stable_id); + + g_hash_table_unref(priv->ip6_saved_properties); + g_hash_table_unref(priv->available_connections); + + nm_dbus_track_obj_path_deinit(&priv->parent_device); + nm_dbus_track_obj_path_deinit(&priv->act_request); + + G_OBJECT_CLASS(nm_device_parent_class)->finalize(object); + + /* for testing, NMDeviceTest does not invoke NMDevice::constructed, + * and thus @settings might be unset. */ + nm_g_object_unref(priv->settings); + nm_g_object_unref(priv->manager); + + nm_g_object_unref(priv->concheck_mgr); + + g_object_unref(priv->netns); +} + +/*****************************************************************************/ + +static const GDBusSignalInfo signal_info_state_changed = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT( + "StateChanged", + .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("new_state", "u"), + NM_DEFINE_GDBUS_ARG_INFO("old_state", "u"), + NM_DEFINE_GDBUS_ARG_INFO("reason", "u"), ), ); + +static const NMDBusInterfaceInfoExtended interface_info_device = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "Reapply", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"), + NM_DEFINE_GDBUS_ARG_INFO("version_id", "t"), + NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ), ), + .handle = impl_device_reapply, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "GetAppliedConnection", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"), + NM_DEFINE_GDBUS_ARG_INFO("version_id", "t"), ), ), + .handle = impl_device_get_applied_connection, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Disconnect", ), + .handle = impl_device_disconnect, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Delete", ), + .handle = impl_device_delete, ), ), + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&signal_info_state_changed, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Udi", "s", NM_DEVICE_UDI), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Path", "s", NM_DEVICE_PATH), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Interface", "s", NM_DEVICE_IFACE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IpInterface", + "s", + NM_DEVICE_IP_IFACE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Driver", "s", NM_DEVICE_DRIVER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DriverVersion", + "s", + NM_DEVICE_DRIVER_VERSION), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FirmwareVersion", + "s", + NM_DEVICE_FIRMWARE_VERSION), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Capabilities", + "u", + NM_DEVICE_CAPABILITIES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip4Address", + "u", + NM_DEVICE_IP4_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("State", "u", NM_DEVICE_STATE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("StateReason", + "(uu)", + NM_DEVICE_STATE_REASON), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ActiveConnection", + "o", + NM_DEVICE_ACTIVE_CONNECTION), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip4Config", + "o", + NM_DEVICE_IP4_CONFIG), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Dhcp4Config", + "o", + NM_DEVICE_DHCP4_CONFIG), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip6Config", + "o", + NM_DEVICE_IP6_CONFIG), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Dhcp6Config", + "o", + NM_DEVICE_DHCP6_CONFIG), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE_L("Managed", + "b", + NM_DEVICE_MANAGED, + NM_AUTH_PERMISSION_NETWORK_CONTROL, + NM_AUDIT_OP_DEVICE_MANAGED), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE_L("Autoconnect", + "b", + NM_DEVICE_AUTOCONNECT, + NM_AUTH_PERMISSION_NETWORK_CONTROL, + NM_AUDIT_OP_DEVICE_AUTOCONNECT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FirmwareMissing", + "b", + NM_DEVICE_FIRMWARE_MISSING), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NmPluginMissing", + "b", + NM_DEVICE_NM_PLUGIN_MISSING), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DeviceType", + "u", + NM_DEVICE_DEVICE_TYPE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AvailableConnections", + "ao", + NM_DEVICE_AVAILABLE_CONNECTIONS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PhysicalPortId", + "s", + NM_DEVICE_PHYSICAL_PORT_ID), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mtu", "u", NM_DEVICE_MTU), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Metered", "u", NM_DEVICE_METERED), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("LldpNeighbors", + "aa{sv}", + NM_DEVICE_LLDP_NEIGHBORS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Real", "b", NM_DEVICE_REAL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ip4Connectivity", + "u", + NM_DEVICE_IP4_CONNECTIVITY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ip6Connectivity", + "u", + NM_DEVICE_IP6_CONNECTIVITY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("InterfaceFlags", + "u", + NM_DEVICE_INTERFACE_FLAGS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), ), ), +}; + +const NMDBusInterfaceInfoExtended nm_interface_info_device_statistics = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_STATISTICS, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE( + "RefreshRateMs", + "u", + NM_DEVICE_STATISTICS_REFRESH_RATE_MS, + NM_AUTH_PERMISSION_ENABLE_DISABLE_STATISTICS, + NM_AUDIT_OP_STATISTICS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("TxBytes", + "t", + NM_DEVICE_STATISTICS_TX_BYTES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RxBytes", + "t", + NM_DEVICE_STATISTICS_RX_BYTES), ), ), +}; + +static void +nm_device_class_init(NMDeviceClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + + g_type_class_add_private(object_class, sizeof(NMDevicePrivate)); + + dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/Devices"); + dbus_object_class->interface_infos = + NM_DBUS_INTERFACE_INFOS(&interface_info_device, &nm_interface_info_device_statistics); + + object_class->dispose = dispose; + object_class->finalize = finalize; + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->constructor = constructor; + object_class->constructed = constructed; + + klass->link_changed = link_changed; + + klass->is_available = is_available; + klass->act_stage2_config = act_stage2_config; + klass->act_stage3_ip_config_start = act_stage3_ip_config_start; + klass->act_stage4_ip_config_timeout = act_stage4_ip_config_timeout; + + klass->get_type_description = get_type_description; + klass->can_auto_connect = can_auto_connect; + klass->can_update_from_platform_link = can_update_from_platform_link; + klass->check_connection_compatible = check_connection_compatible; + klass->check_connection_available = check_connection_available; + klass->can_unmanaged_external_down = can_unmanaged_external_down; + klass->realize_start_notify = realize_start_notify; + klass->unrealize_notify = unrealize_notify; + klass->carrier_changed_notify = carrier_changed_notify; + klass->get_ip_iface_identifier = get_ip_iface_identifier; + klass->unmanaged_on_quit = unmanaged_on_quit; + klass->deactivate_reset_hw_addr = deactivate_reset_hw_addr; + klass->parent_changed_notify = parent_changed_notify; + klass->can_reapply_change = can_reapply_change; + klass->reapply_connection = reapply_connection; + klass->set_platform_mtu = set_platform_mtu; + + obj_properties[PROP_UDI] = + g_param_spec_string(NM_DEVICE_UDI, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_PATH] = g_param_spec_string(NM_DEVICE_PATH, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IFACE] = + g_param_spec_string(NM_DEVICE_IFACE, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IP_IFACE] = g_param_spec_string(NM_DEVICE_IP_IFACE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_DRIVER] = + g_param_spec_string(NM_DEVICE_DRIVER, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_DRIVER_VERSION] = + g_param_spec_string(NM_DEVICE_DRIVER_VERSION, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_FIRMWARE_VERSION] = + g_param_spec_string(NM_DEVICE_FIRMWARE_VERSION, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_CAPABILITIES, + "", + "", + 0, + G_MAXUINT32, + NM_DEVICE_CAP_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_CARRIER] = g_param_spec_boolean(NM_DEVICE_CARRIER, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_MTU] = g_param_spec_uint(NM_DEVICE_MTU, + "", + "", + 0, + G_MAXUINT32, + 1500, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IP4_ADDRESS] = + g_param_spec_variant(NM_DEVICE_IP4_ADDRESS, + "", + "", + G_VARIANT_TYPE_UINT32, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IP4_CONFIG] = + g_param_spec_string(NM_DEVICE_IP4_CONFIG, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_DHCP4_CONFIG] = + g_param_spec_string(NM_DEVICE_DHCP4_CONFIG, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IP6_CONFIG] = + g_param_spec_string(NM_DEVICE_IP6_CONFIG, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_DHCP6_CONFIG] = + g_param_spec_string(NM_DEVICE_DHCP6_CONFIG, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_STATE] = g_param_spec_uint(NM_DEVICE_STATE, + "", + "", + 0, + G_MAXUINT32, + NM_DEVICE_STATE_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_STATE_REASON] = + g_param_spec_variant(NM_DEVICE_STATE_REASON, + "", + "", + G_VARIANT_TYPE("(uu)"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_ACTIVE_CONNECTION] = + g_param_spec_string(NM_DEVICE_ACTIVE_CONNECTION, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_DEVICE_TYPE] = + g_param_spec_uint(NM_DEVICE_DEVICE_TYPE, + "", + "", + 0, + G_MAXUINT32, + NM_DEVICE_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_LINK_TYPE] = + g_param_spec_uint(NM_DEVICE_LINK_TYPE, + "", + "", + 0, + G_MAXUINT32, + NM_LINK_TYPE_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_MANAGED] = g_param_spec_boolean(NM_DEVICE_MANAGED, + "", + "", + FALSE, + G_PARAM_READWRITE | /* via D-Bus */ + G_PARAM_STATIC_STRINGS); + obj_properties[PROP_AUTOCONNECT] = g_param_spec_boolean(NM_DEVICE_AUTOCONNECT, + "", + "", + DEFAULT_AUTOCONNECT, + G_PARAM_READWRITE | /* via D-Bus */ + G_PARAM_STATIC_STRINGS); + obj_properties[PROP_FIRMWARE_MISSING] = + g_param_spec_boolean(NM_DEVICE_FIRMWARE_MISSING, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_NM_PLUGIN_MISSING] = + g_param_spec_boolean(NM_DEVICE_NM_PLUGIN_MISSING, + "", + "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_TYPE_DESC] = + g_param_spec_string(NM_DEVICE_TYPE_DESC, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_RFKILL_TYPE] = + g_param_spec_uint(NM_DEVICE_RFKILL_TYPE, + "", + "", + RFKILL_TYPE_WLAN, + RFKILL_TYPE_MAX, + RFKILL_TYPE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IFINDEX] = g_param_spec_int(NM_DEVICE_IFINDEX, + "", + "", + 0, + G_MAXINT, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_AVAILABLE_CONNECTIONS] = + g_param_spec_boxed(NM_DEVICE_AVAILABLE_CONNECTIONS, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_PHYSICAL_PORT_ID] = + g_param_spec_string(NM_DEVICE_PHYSICAL_PORT_ID, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_MASTER] = g_param_spec_object(NM_DEVICE_MASTER, + "", + "", + NM_TYPE_DEVICE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_PARENT] = g_param_spec_string(NM_DEVICE_PARENT, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_HW_ADDRESS] = + g_param_spec_string(NM_DEVICE_HW_ADDRESS, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_PERM_HW_ADDRESS] = + g_param_spec_string(NM_DEVICE_PERM_HW_ADDRESS, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_HAS_PENDING_ACTION] = + g_param_spec_boolean(NM_DEVICE_HAS_PENDING_ACTION, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_METERED] = g_param_spec_uint(NM_DEVICE_METERED, + "", + "", + 0, + G_MAXUINT32, + NM_METERED_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_LLDP_NEIGHBORS] = + g_param_spec_variant(NM_DEVICE_LLDP_NEIGHBORS, + "", + "", + G_VARIANT_TYPE("aa{sv}"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_REAL] = g_param_spec_boolean(NM_DEVICE_REAL, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_SLAVES] = g_param_spec_boxed(NM_DEVICE_SLAVES, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_STATISTICS_REFRESH_RATE_MS] = + g_param_spec_uint(NM_DEVICE_STATISTICS_REFRESH_RATE_MS, + "", + "", + 0, + UINT32_MAX, + 0, + G_PARAM_READWRITE | /* via D-Bus */ + G_PARAM_STATIC_STRINGS); + obj_properties[PROP_STATISTICS_TX_BYTES] = + g_param_spec_uint64(NM_DEVICE_STATISTICS_TX_BYTES, + "", + "", + 0, + UINT64_MAX, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_STATISTICS_RX_BYTES] = + g_param_spec_uint64(NM_DEVICE_STATISTICS_RX_BYTES, + "", + "", + 0, + UINT64_MAX, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IP4_CONNECTIVITY] = + g_param_spec_uint(NM_DEVICE_IP4_CONNECTIVITY, + "", + "", + NM_CONNECTIVITY_UNKNOWN, + NM_CONNECTIVITY_FULL, + NM_CONNECTIVITY_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_IP6_CONNECTIVITY] = + g_param_spec_uint(NM_DEVICE_IP6_CONNECTIVITY, + "", + "", + NM_CONNECTIVITY_UNKNOWN, + NM_CONNECTIVITY_FULL, + NM_CONNECTIVITY_UNKNOWN, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + obj_properties[PROP_INTERFACE_FLAGS] = + g_param_spec_uint(NM_DEVICE_INTERFACE_FLAGS, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[STATE_CHANGED] = g_signal_new(NM_DEVICE_STATE_CHANGED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(NMDeviceClass, state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_UINT, + G_TYPE_UINT, + G_TYPE_UINT); + + signals[AUTOCONNECT_ALLOWED] = g_signal_new(NM_DEVICE_AUTOCONNECT_ALLOWED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + autoconnect_allowed_accumulator, + NULL, + NULL, + G_TYPE_BOOLEAN, + 0); + + signals[IP4_CONFIG_CHANGED] = g_signal_new(NM_DEVICE_IP4_CONFIG_CHANGED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_OBJECT, + G_TYPE_OBJECT); + + signals[IP6_CONFIG_CHANGED] = g_signal_new(NM_DEVICE_IP6_CONFIG_CHANGED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_OBJECT, + G_TYPE_OBJECT); + + signals[IP6_PREFIX_DELEGATED] = g_signal_new(NM_DEVICE_IP6_PREFIX_DELEGATED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_POINTER); + + signals[IP6_SUBNET_NEEDED] = g_signal_new(NM_DEVICE_IP6_SUBNET_NEEDED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[REMOVED] = g_signal_new(NM_DEVICE_REMOVED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[RECHECK_AUTO_ACTIVATE] = g_signal_new(NM_DEVICE_RECHECK_AUTO_ACTIVATE, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[RECHECK_ASSUME] = g_signal_new(NM_DEVICE_RECHECK_ASSUME, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[DNS_LOOKUP_DONE] = g_signal_new(NM_DEVICE_DNS_LOOKUP_DONE, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + +/* Connection defaults from plugins */ +NM_CON_DEFAULT_NOP("cdma.mtu"); +NM_CON_DEFAULT_NOP("gsm.mtu"); +NM_CON_DEFAULT_NOP("wifi.ap-isolation"); +NM_CON_DEFAULT_NOP("wifi.powersave"); +NM_CON_DEFAULT_NOP("wifi.wake-on-wlan"); +NM_CON_DEFAULT_NOP("wifi-sec.pmf"); +NM_CON_DEFAULT_NOP("wifi-sec.fils"); diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h new file mode 100644 index 00000000..72777d0a --- /dev/null +++ b/src/core/devices/nm-device.h @@ -0,0 +1,874 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2017 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_H__ +#define __NETWORKMANAGER_DEVICE_H__ + +#include <netinet/in.h> + +#include "nm-setting-connection.h" +#include "nm-dbus-object.h" +#include "nm-dbus-interface.h" +#include "nm-connection.h" +#include "nm-rfkill-manager.h" +#include "NetworkManagerUtils.h" + +typedef enum _nm_packed { + NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, + NM_DEVICE_SYS_IFACE_STATE_ASSUME, + NM_DEVICE_SYS_IFACE_STATE_MANAGED, + + /* the REMOVED state applies when the device is manually set to unmanaged + * or the link was externally removed. In both cases, we move the device + * to UNMANAGED state, without touching the link -- be it, because the link + * is already gone or because we want to release it (give it up). + */ + NM_DEVICE_SYS_IFACE_STATE_REMOVED, +} NMDeviceSysIfaceState; + +typedef enum { + NM_DEVICE_MTU_SOURCE_NONE, + NM_DEVICE_MTU_SOURCE_PARENT, + NM_DEVICE_MTU_SOURCE_IP_CONFIG, + NM_DEVICE_MTU_SOURCE_CONNECTION, +} NMDeviceMtuSource; + +static inline NMDeviceStateReason +nm_device_state_reason_check(NMDeviceStateReason reason) +{ + /* the device-state-reason serves mostly informational purpose during a state + * change. In some cases however, decisions are made based on the reason. + * I tend to think that interpreting the state reason to derive some behaviors + * is confusing, because the cause and effect are so far apart. + * + * This function is here to mark source that inspects the reason to make + * a decision -- contrary to places that set the reason. Thus, by grepping + * for nm_device_state_reason_check() you can find the "effect" to a certain + * reason. + */ + return reason; +} + +#define NM_PENDING_ACTION_AUTOACTIVATE "autoactivate" +#define NM_PENDING_ACTION_IN_STATE_CHANGE "in-state-change" +#define NM_PENDING_ACTION_RECHECK_AVAILABLE "recheck-available" +#define NM_PENDING_ACTION_CARRIER_WAIT "carrier-wait" +#define NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT "waiting-for-supplicant" +#define NM_PENDING_ACTION_WIFI_SCAN "wifi-scan" +#define NM_PENDING_ACTION_WAITING_FOR_COMPANION "waiting-for-companion" +#define NM_PENDING_ACTION_LINK_INIT "link-init" + +#define NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "queued-state-change-" +#define NM_PENDING_ACTIONPREFIX_ACTIVATION "activation-" + +/* Properties */ +#define NM_DEVICE_UDI "udi" +#define NM_DEVICE_PATH "path" +#define NM_DEVICE_IFACE "interface" +#define NM_DEVICE_IP_IFACE "ip-interface" +#define NM_DEVICE_DRIVER "driver" +#define NM_DEVICE_DRIVER_VERSION "driver-version" +#define NM_DEVICE_FIRMWARE_VERSION "firmware-version" +#define NM_DEVICE_CAPABILITIES "capabilities" +#define NM_DEVICE_CARRIER "carrier" +#define NM_DEVICE_IP4_ADDRESS "ip4-address" +#define NM_DEVICE_IP4_CONFIG "ip4-config" +#define NM_DEVICE_DHCP4_CONFIG "dhcp4-config" +#define NM_DEVICE_IP6_CONFIG "ip6-config" +#define NM_DEVICE_DHCP6_CONFIG "dhcp6-config" +#define NM_DEVICE_STATE "state" +#define NM_DEVICE_STATE_REASON "state-reason" +#define NM_DEVICE_ACTIVE_CONNECTION "active-connection" +#define NM_DEVICE_DEVICE_TYPE "device-type" /* ugh */ +#define NM_DEVICE_LINK_TYPE "link-type" +#define NM_DEVICE_MANAGED "managed" +#define NM_DEVICE_AUTOCONNECT "autoconnect" +#define NM_DEVICE_FIRMWARE_MISSING "firmware-missing" +#define NM_DEVICE_NM_PLUGIN_MISSING "nm-plugin-missing" +#define NM_DEVICE_AVAILABLE_CONNECTIONS "available-connections" +#define NM_DEVICE_PHYSICAL_PORT_ID "physical-port-id" +#define NM_DEVICE_MTU "mtu" +#define NM_DEVICE_HW_ADDRESS "hw-address" + +/* "perm-hw-address" is exposed on D-Bus both for NMDeviceEthernet + * and NMDeviceWifi. */ +#define NM_DEVICE_PERM_HW_ADDRESS "perm-hw-address" + +#define NM_DEVICE_METERED "metered" +#define NM_DEVICE_LLDP_NEIGHBORS "lldp-neighbors" +#define NM_DEVICE_REAL "real" + +/* "parent" is exposed on D-Bus by subclasses like NMDeviceIPTunnel */ +#define NM_DEVICE_PARENT "parent" + +/* the "slaves" property is internal in the parent class, but exposed + * by the derived classes NMDeviceBond, NMDeviceBridge, NMDeviceTeam, + * NMDeviceOvsBridge and NMDeviceOvsPort. */ +#define NM_DEVICE_SLAVES "slaves" /* partially internal */ + +#define NM_DEVICE_TYPE_DESC "type-desc" /* Internal only */ +#define NM_DEVICE_RFKILL_TYPE "rfkill-type" /* Internal only */ +#define NM_DEVICE_IFINDEX "ifindex" /* Internal only */ +#define NM_DEVICE_MASTER "master" /* Internal only */ +#define NM_DEVICE_HAS_PENDING_ACTION "has-pending-action" /* Internal only */ + +/* Internal signals */ +#define NM_DEVICE_DNS_LOOKUP_DONE "dns-lookup-done" +#define NM_DEVICE_IP4_CONFIG_CHANGED "ip4-config-changed" +#define NM_DEVICE_IP6_CONFIG_CHANGED "ip6-config-changed" +#define NM_DEVICE_IP6_PREFIX_DELEGATED "ip6-prefix-delegated" +#define NM_DEVICE_IP6_SUBNET_NEEDED "ip6-subnet-needed" +#define NM_DEVICE_REMOVED "removed" +#define NM_DEVICE_RECHECK_AUTO_ACTIVATE "recheck-auto-activate" +#define NM_DEVICE_RECHECK_ASSUME "recheck-assume" +#define NM_DEVICE_STATE_CHANGED "state-changed" +#define NM_DEVICE_LINK_INITIALIZED "link-initialized" +#define NM_DEVICE_AUTOCONNECT_ALLOWED "autoconnect-allowed" + +#define NM_DEVICE_STATISTICS_REFRESH_RATE_MS "refresh-rate-ms" +#define NM_DEVICE_STATISTICS_TX_BYTES "tx-bytes" +#define NM_DEVICE_STATISTICS_RX_BYTES "rx-bytes" + +#define NM_DEVICE_IP4_CONNECTIVITY "ip4-connectivity" +#define NM_DEVICE_IP6_CONNECTIVITY "ip6-connectivity" +#define NM_DEVICE_INTERFACE_FLAGS "interface-flags" + +#define NM_TYPE_DEVICE (nm_device_get_type()) +#define NM_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE, NMDevice)) +#define NM_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE, NMDeviceClass)) +#define NM_IS_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE)) +#define NM_IS_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE)) +#define NM_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE, NMDeviceClass)) + +typedef enum NMActStageReturn NMActStageReturn; + +/* These flags affect whether a connection is considered available on a device + * (check_connection_available()). The flags should have the meaning of relaxing + * a condition, so that adding a flag might make a connection available that would + * not be available otherwise. Adding a flag should never make a connection + * not available if it would be available otherwise. */ +typedef enum { /*< skip >*/ + NM_DEVICE_CHECK_CON_AVAILABLE_NONE = 0, + + /* since NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST is a collection of flags with more fine grained + * parts, this flag in general indicates that this is a user-request. */ + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST = (1L << 0), + + /* we also consider devices which have no carrier but are still waiting for the driver + * to detect carrier. Usually, such devices are not yet available, however for a user-request + * they are. They might fail later if carrier doesn't come. */ + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER = (1L << 1), + + /* usually, a profile is only available if the Wi-Fi AP is in range. For an + * explicit user request, we also consider profiles for APs that are not (yet) + * visible. */ + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP = (1L << 2), + + /* a device can be marked as unmanaged for various reasons. Some of these reasons + * are authoritative, others not. Non-authoritative reasons can be overruled by + * `nmcli device set $DEVICE managed yes`. Also, for an explicit user activation + * request we may want to consider the device as managed. This flag makes devices + * that are unmanaged appear available. */ + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED = (1L << 3), + + /* a collection of flags, that are commonly set for an explicit user-request. */ + NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST = + _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST + | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER + | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP + | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED, + + NM_DEVICE_CHECK_CON_AVAILABLE_ALL = (1L << 4) - 1, +} NMDeviceCheckConAvailableFlags; + +struct _NMDevicePrivate; + +struct _NMDevice { + NMDBusObject parent; + struct _NMDevicePrivate *_priv; + CList devices_lst; +}; + +/* The flags have an relaxing meaning, that means, specifying more flags, can make + * a device appear more available. It can never make a device less available. */ +typedef enum { /*< skip >*/ + NM_DEVICE_CHECK_DEV_AVAILABLE_NONE = 0, + + /* the device is considered available, even if it has no carrier. + * + * For various device types (software devices) we ignore carrier based + * on the type. So, for them, this flag has no effect anyway. */ + _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER = (1L << 0), + + NM_DEVICE_CHECK_DEV_AVAILABLE_FOR_USER_REQUEST = + _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER, + + NM_DEVICE_CHECK_DEV_AVAILABLE_ALL = (1L << 1) - 1, +} NMDeviceCheckDevAvailableFlags; + +typedef void (*NMDeviceDeactivateCallback)(NMDevice *self, GError *error, gpointer user_data); + +typedef struct _NMDeviceClass { + NMDBusObjectClass parent; + + struct _NMDeviceClass *default_type_description_klass; + const char * default_type_description; + + const char *connection_type_supported; + + /* most device types, can only handle profiles of a particular type. This + * is the connection.type setting, as checked by nm_device_check_connection_compatible() */ + const char *connection_type_check_compatible; + + const NMLinkType *link_types; + + /* if the device MTU is set based on parent's one, this specifies + * a delta in the MTU allowed value due the encapsulation overhead */ + guint16 mtu_parent_delta; + + /* Whether the device type is a master-type. This depends purely on the + * type (NMDeviceClass), not the actual device instance. */ + bool is_master : 1; + + /* Force setting the MTU actually means first setting the MTU + * to (desired_MTU-1) and then setting the desired_MTU + * so that kernel actually applies the MTU, otherwise + * kernel will ignore the request if the link's MTU is the + * same as the desired one. + * + * This is just a workaround made for bridges (ATM) that employ + * a auto-MTU adjust mechanism if no MTU is manually set. + */ + bool mtu_force_set : 1; + + /* Control whether to call stage1 and stage2 callbacks also for assuming + * a device or for external activations. In this case, the callback must + * take care not to touch the device's configuration. */ + bool act_stage1_prepare_also_for_external_or_assume : 1; + bool act_stage2_config_also_for_external_or_assume : 1; + + bool act_stage1_prepare_set_hwaddr_ethernet : 1; + + bool can_reapply_change_ovs_external_ids : 1; + + void (*state_changed)(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason); + + void (*link_changed)(NMDevice *self, const NMPlatformLink *pllink); + + /** + * create_and_realize(): + * @self: the #NMDevice + * @connection: the #NMConnection being activated + * @parent: the parent #NMDevice, if any + * @out_plink: on success, a backing kernel network device if one exists. + * The returned pointer is owned by platform and only valid until the + * next platform operation. + * @error: location to store error, or %NULL + * + * Create any backing resources (kernel devices, etc) required for this + * device to activate @connection. If the device is backed by a kernel + * network device, that device should be returned in @out_plink after + * being created. + * + * Returns: %TRUE on success, %FALSE on error + */ + gboolean (*create_and_realize)(NMDevice * self, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error); + + /** + * realize_start_notify(): + * @self: the #NMDevice + * @pllink: the #NMPlatformLink if backed by a kernel netdevice + * + * Hook for derived classes to be notfied during realize_start_setup() + * and perform additional setup. + * + * The default implementation of NMDevice calls link_changed(). + */ + void (*realize_start_notify)(NMDevice *self, const NMPlatformLink *pllink); + + /** + * unrealize(): + * @self: the #NMDevice + * + * Remove the device backing resources. + */ + gboolean (*unrealize)(NMDevice *self, GError **error); + + /** + * unrealize_notify(): + * @self: the #NMDevice + * + * Hook for derived classes to clear any properties that depend on backing resources + * (kernel devices, etc). This is called by nm_device_unrealize() during unrealization. + */ + void (*unrealize_notify)(NMDevice *self); + + /* Hardware state (IFF_UP) */ + gboolean (*can_unmanaged_external_down)(NMDevice *self); + + /* Carrier state (IFF_LOWER_UP) */ + void (*carrier_changed_notify)(NMDevice *, gboolean carrier); + + gboolean (*get_ip_iface_identifier)(NMDevice *self, NMUtilsIPv6IfaceId *out_iid); + + NMDeviceCapabilities (*get_generic_capabilities)(NMDevice *self); + + gboolean (*is_available)(NMDevice *self, NMDeviceCheckDevAvailableFlags flags); + + gboolean (*get_enabled)(NMDevice *self); + + void (*set_enabled)(NMDevice *self, gboolean enabled); + + /* let the subclass return additional NMPlatformRoutingRule (in form of NMPObject + * pointers) that shall be added to the rules provided by this device. + * The returned GPtrArray will be g_ptr_array_unref()'ed. The subclass may or + * may not keep an additional reference and return this array again and again. */ + GPtrArray *(*get_extra_rules)(NMDevice *self); + + /* allow derived classes to override the result of nm_device_autoconnect_allowed(). + * If the value changes, the class should call nm_device_emit_recheck_auto_activate(), + * which emits NM_DEVICE_RECHECK_AUTO_ACTIVATE signal. */ + gboolean (*get_autoconnect_allowed)(NMDevice *self); + + gboolean (*can_auto_connect)(NMDevice * self, + NMSettingsConnection *sett_conn, + char ** specific_object); + + guint32 (*get_configured_mtu)(NMDevice * self, + NMDeviceMtuSource *out_source, + gboolean * out_force); + + /* allow the subclass to overwrite the routing table. This is mainly useful + * to change from partial mode (route-table=0) to full-sync mode (route-table=254). */ + guint32 (*coerce_route_table)(NMDevice *self, + int addr_family, + guint32 route_table, + gboolean is_user_config); + + const char *(*get_auto_ip_config_method)(NMDevice *self, int addr_family); + + /* Checks whether the connection is compatible with the device using + * only the devices type and characteristics. Does not use any live + * network information like Wi-Fi scan lists etc. + */ + gboolean (*check_connection_compatible)(NMDevice * self, + NMConnection *connection, + GError ** error); + + /* Checks whether the connection is likely available to be activated, + * including any live network information like scan lists. The connection + * is checked against the object defined by @specific_object, if given. + * Returns TRUE if the connection is available; FALSE if not. + * + * The passed @flags affect whether a connection is considered + * available or not. Adding more flags, means the connection is + * *more* available. + * + * Specifying @specific_object can only reduce the availability of a connection. + */ + gboolean (*check_connection_available)(NMDevice * self, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error); + + gboolean (*complete_connection)(NMDevice * self, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error); + + NMActStageReturn (*act_stage1_prepare)(NMDevice *self, NMDeviceStateReason *out_failure_reason); + NMActStageReturn (*act_stage2_config)(NMDevice *self, NMDeviceStateReason *out_failure_reason); + NMActStageReturn (*act_stage3_ip_config_start)(NMDevice * self, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason); + NMActStageReturn (*act_stage4_ip_config_timeout)(NMDevice * self, + int addr_family, + NMDeviceStateReason *out_failure_reason); + + void (*ip4_config_pre_commit)(NMDevice *self, NMIP4Config *config); + + /* Async deactivating (in the DEACTIVATING phase) */ + void (*deactivate_async)(NMDevice * self, + GCancellable * cancellable, + NMDeviceDeactivateCallback callback, + gpointer user_data); + + void (*deactivate_reset_hw_addr)(NMDevice *self); + + /* Sync deactivating (in the DISCONNECTED phase) */ + void (*deactivate)(NMDevice *self); + + const char *(*get_type_description)(NMDevice *self); + + const char *(*get_s390_subchannels)(NMDevice *self); + + /* Update the connection with currently configured L2 settings */ + void (*update_connection)(NMDevice *device, NMConnection *connection); + + gboolean (*master_update_slave_connection)(NMDevice * self, + NMDevice * slave, + NMConnection *connection, + GError ** error); + + gboolean (*enslave_slave)(NMDevice * self, + NMDevice * slave, + NMConnection *connection, + gboolean configure); + + void (*release_slave)(NMDevice *self, NMDevice *slave, gboolean configure); + + void (*parent_changed_notify)(NMDevice *self, + int old_ifindex, + NMDevice *old_parent, + int new_ifindex, + NMDevice *new_parent); + + gboolean (*owns_iface)(NMDevice *self, const char *iface); + + NMConnection *(*new_default_connection)(NMDevice *self); + + gboolean (*unmanaged_on_quit)(NMDevice *self); + + gboolean (*can_reapply_change)(NMDevice * self, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error); + + void (*reapply_connection)(NMDevice *self, NMConnection *con_old, NMConnection *con_new); + + guint32 (*get_dhcp_timeout_for_device)(NMDevice *self, int addr_family); + + gboolean (*get_guessed_metered)(NMDevice *self); + + gboolean (*can_update_from_platform_link)(NMDevice *self, const NMPlatformLink *plink); + + gboolean (*set_platform_mtu)(NMDevice *self, guint32 mtu); + +} NMDeviceClass; + +GType nm_device_get_type(void); + +struct _NMDedupMultiIndex *nm_device_get_multi_index(NMDevice *self); +NMNetns * nm_device_get_netns(NMDevice *self); +NMPlatform * nm_device_get_platform(NMDevice *self); + +const char *nm_device_get_udi(NMDevice *dev); +const char *nm_device_get_iface(NMDevice *dev); + +static inline const char * +_nm_device_get_iface(NMDevice *device) +{ + /* like nm_device_get_iface(), but gracefully accept NULL without + * asserting. */ + return device ? nm_device_get_iface(device) : NULL; +} + +int nm_device_get_ifindex(NMDevice *dev); +gboolean nm_device_is_software(NMDevice *dev); +gboolean nm_device_is_real(NMDevice *dev); +const char * nm_device_get_ip_iface(NMDevice *dev); +const char * nm_device_get_ip_iface_from_platform(NMDevice *dev); +int nm_device_get_ip_ifindex(const NMDevice *dev); +const char * nm_device_get_driver(NMDevice *dev); +const char * nm_device_get_driver_version(NMDevice *dev); +const char * nm_device_get_type_desc(NMDevice *dev); +const char * nm_device_get_type_description(NMDevice *dev); +NMDeviceType nm_device_get_device_type(NMDevice *dev); +NMLinkType nm_device_get_link_type(NMDevice *dev); +NMMetered nm_device_get_metered(NMDevice *dev); + +guint32 nm_device_get_route_table(NMDevice *self, int addr_family); +guint32 nm_device_get_route_metric(NMDevice *dev, int addr_family); + +guint32 nm_device_get_route_metric_default(NMDeviceType device_type); + +const char *nm_device_get_hw_address(NMDevice *dev); +const char *nm_device_get_permanent_hw_address(NMDevice *self); +const char *nm_device_get_permanent_hw_address_full(NMDevice *self, + gboolean force_freeze, + gboolean *out_is_fake); +const char *nm_device_get_initial_hw_address(NMDevice *dev); + +NMProxyConfig *nm_device_get_proxy_config(NMDevice *dev); + +NMDhcpConfig *nm_device_get_dhcp_config(NMDevice *dev, int addr_family); +NMIP4Config * nm_device_get_ip4_config(NMDevice *dev); +void nm_device_replace_vpn4_config(NMDevice *dev, NMIP4Config *old, NMIP4Config *config); + +NMIP6Config *nm_device_get_ip6_config(NMDevice *dev); +void nm_device_replace_vpn6_config(NMDevice *dev, NMIP6Config *old, NMIP6Config *config); + +void nm_device_capture_initial_config(NMDevice *dev); + +int nm_device_parent_get_ifindex(NMDevice *dev); +NMDevice *nm_device_parent_get_device(NMDevice *dev); +void nm_device_parent_set_ifindex(NMDevice *self, int parent_ifindex); +gboolean nm_device_parent_notify_changed(NMDevice *self, + NMDevice *change_candidate, + gboolean device_removed); + +const char *nm_device_parent_find_for_connection(NMDevice * self, + const char *current_setting_parent); + +/* Master */ +gboolean nm_device_is_master(NMDevice *dev); + +/* Slave */ +NMDevice *nm_device_get_master(NMDevice *dev); + +NMActRequest * nm_device_get_act_request(NMDevice *dev); +NMSettingsConnection * nm_device_get_settings_connection(NMDevice *dev); +NMConnection * nm_device_get_settings_connection_get_connection(NMDevice *self); +NMConnection * nm_device_get_applied_connection(NMDevice *dev); +gboolean nm_device_has_unmodified_applied_connection(NMDevice * self, + NMSettingCompareFlags compare_flags); +NMActivationStateFlags nm_device_get_activation_state_flags(NMDevice *self); + +gpointer /* (NMSetting *) */ nm_device_get_applied_setting(NMDevice *dev, GType setting_type); + +void nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config); + +gboolean nm_device_ignore_carrier_by_default(NMDevice *self); + +gboolean nm_device_is_available(NMDevice *dev, NMDeviceCheckDevAvailableFlags flags); +gboolean nm_device_has_carrier(NMDevice *dev); + +NMConnection *nm_device_generate_connection(NMDevice *self, + NMDevice *master, + gboolean *out_maybe_later, + GError ** error); + +gboolean nm_device_master_update_slave_connection(NMDevice * master, + NMDevice * slave, + NMConnection *connection, + GError ** error); + +gboolean +nm_device_can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object); + +gboolean nm_device_complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error); + +gboolean +nm_device_check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error); + +gboolean nm_device_check_slave_connection_compatible(NMDevice *device, NMConnection *connection); + +gboolean nm_device_unmanage_on_quit(NMDevice *self); + +gboolean nm_device_spec_match_list(NMDevice *device, const GSList *specs); +int nm_device_spec_match_list_full(NMDevice *self, const GSList *specs, int no_match_value); + +gboolean nm_device_is_activating(NMDevice *dev); +gboolean nm_device_autoconnect_allowed(NMDevice *self); + +NMDeviceState nm_device_get_state(NMDevice *device); + +gboolean nm_device_get_enabled(NMDevice *device); + +void nm_device_set_enabled(NMDevice *device, gboolean enabled); + +RfKillType nm_device_get_rfkill_type(NMDevice *device); + +/* IPv6 prefix delegation */ + +void nm_device_request_ip6_prefixes(NMDevice *self, int needed_prefixes); + +gboolean nm_device_needs_ip6_subnet(NMDevice *self); + +void nm_device_use_ip6_subnet(NMDevice *self, const NMPlatformIP6Address *subnet); + +void nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device); + +/** + * NMUnmanagedFlags: + * @NM_UNMANAGED_NONE: placeholder value + * @NM_UNMANAGED_SLEEPING: %TRUE when unmanaged because NM is sleeping. + * @NM_UNMANAGED_QUITTING: %TRUE when unmanaged because NM is shutting down. + * @NM_UNMANAGED_PARENT: %TRUE when unmanaged due to parent device being unmanaged + * @NM_UNMANAGED_BY_TYPE: %TRUE for unmanaging device by type, like loopback. + * @NM_UNMANAGED_PLATFORM_INIT: %TRUE when unmanaged because platform link not + * yet initialized. Unrealized device are also unmanaged for this reason. + * @NM_UNMANAGED_USER_EXPLICIT: %TRUE when unmanaged by explicit user decision + * (e.g. via a D-Bus command) + * @NM_UNMANAGED_USER_SETTINGS: %TRUE when unmanaged by user decision via + * the settings plugin (for example keyfile.unmanaged-devices or ifcfg-rh's + * NM_CONTROLLED=no). Although this is user-configuration (provided from + * the settings plugins, such as NM_CONTROLLED=no in ifcfg-rh), it cannot + * be overruled and is authoritative. That is because users may depend on + * dropping a ifcfg-rh file to ensure the device is unmanaged. + * @NM_UNMANAGED_USER_CONF: %TRUE when unmanaged by user decision via + * the NetworkManager.conf ("unmanaged" in the [device] section). + * Contray to @NM_UNMANAGED_USER_SETTINGS, this can be overwritten via + * D-Bus. + * @NM_UNMANAGED_BY_DEFAULT: %TRUE for certain device types where we unmanage + * them by default + * @NM_UNMANAGED_USER_UDEV: %TRUE when unmanaged by user decision (via UDev rule) + * @NM_UNMANAGED_EXTERNAL_DOWN: %TRUE when unmanaged because !IFF_UP and not created by NM + * @NM_UNMANAGED_IS_SLAVE: indicates that the device is enslaved. Note that + * setting the NM_UNMANAGED_IS_SLAVE to %TRUE makes no sense, this flag has only + * meaning to set a slave device as managed if the parent is managed too. + */ +typedef enum { /*< skip >*/ + NM_UNMANAGED_NONE = 0, + + /* these flags are authoritative. If one of them is set, + * the device cannot be managed. */ + NM_UNMANAGED_SLEEPING = (1LL << 0), + NM_UNMANAGED_QUITTING = (1LL << 1), + NM_UNMANAGED_PARENT = (1LL << 2), + NM_UNMANAGED_BY_TYPE = (1LL << 3), + NM_UNMANAGED_PLATFORM_INIT = (1LL << 4), + NM_UNMANAGED_USER_EXPLICIT = (1LL << 5), + NM_UNMANAGED_USER_SETTINGS = (1LL << 6), + + /* These flags can be non-effective and be overwritten + * by other flags. */ + NM_UNMANAGED_BY_DEFAULT = (1LL << 8), + NM_UNMANAGED_USER_CONF = (1LL << 9), + NM_UNMANAGED_USER_UDEV = (1LL << 10), + NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 11), + NM_UNMANAGED_IS_SLAVE = (1LL << 12), + +} NMUnmanagedFlags; + +typedef enum { + NM_UNMAN_FLAG_OP_SET_MANAGED = FALSE, + NM_UNMAN_FLAG_OP_SET_UNMANAGED = TRUE, + NM_UNMAN_FLAG_OP_FORGET = 2, +} NMUnmanFlagOp; + +const char *nm_unmanaged_flags2str(NMUnmanagedFlags flags, char *buf, gsize len); + +gboolean nm_device_get_managed(NMDevice *device, gboolean for_user_request); +NMUnmanagedFlags nm_device_get_unmanaged_mask(NMDevice *device, NMUnmanagedFlags flag); +NMUnmanagedFlags nm_device_get_unmanaged_flags(NMDevice *device, NMUnmanagedFlags flag); +void nm_device_set_unmanaged_flags(NMDevice *device, NMUnmanagedFlags flags, NMUnmanFlagOp set_op); +void nm_device_set_unmanaged_by_flags(NMDevice * device, + NMUnmanagedFlags flags, + NMUnmanFlagOp set_op, + NMDeviceStateReason reason); +void nm_device_set_unmanaged_by_flags_queue(NMDevice * self, + NMUnmanagedFlags flags, + NMUnmanFlagOp set_op, + NMDeviceStateReason reason); +void nm_device_set_unmanaged_by_user_settings(NMDevice *self); +void nm_device_set_unmanaged_by_user_udev(NMDevice *self); +void nm_device_set_unmanaged_by_user_conf(NMDevice *self); +void nm_device_set_unmanaged_by_quitting(NMDevice *device); + +gboolean nm_device_check_unrealized_device_managed(NMDevice *self); + +gboolean nm_device_is_nm_owned(NMDevice *device); + +gboolean nm_device_has_capability(NMDevice *self, NMDeviceCapabilities caps); + +/*****************************************************************************/ + +void nm_device_assume_state_get(NMDevice * self, + gboolean * out_assume_state_guess_assume, + const char **out_assume_state_connection_uuid); +void nm_device_assume_state_reset(NMDevice *self); + +/*****************************************************************************/ + +gboolean nm_device_realize_start(NMDevice * device, + const NMPlatformLink *plink, + gboolean assume_state_guess_assume, + const char * assume_state_connection_uuid, + gboolean set_nm_owned, + NMUnmanFlagOp unmanaged_user_explicit, + gboolean * out_compatible, + GError ** error); +void nm_device_realize_finish(NMDevice *self, const NMPlatformLink *plink); +gboolean nm_device_create_and_realize(NMDevice * self, + NMConnection *connection, + NMDevice * parent, + GError ** error); +gboolean nm_device_unrealize(NMDevice *device, gboolean remove_resources, GError **error); + +void nm_device_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink); + +typedef enum { + NM_DEVICE_AUTOCONNECT_BLOCKED_NONE = 0, + + NM_DEVICE_AUTOCONNECT_BLOCKED_USER = (1LL << 0), + + NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN = (1LL << 1), + NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT = (1LL << 2), + NM_DEVICE_AUTOCONNECT_BLOCKED_SIM_MISSING = (1LL << 3), + NM_DEVICE_AUTOCONNECT_BLOCKED_INIT_FAILED = (1LL << 4), + + _NM_DEVICE_AUTOCONNECT_BLOCKED_LAST, + + NM_DEVICE_AUTOCONNECT_BLOCKED_ALL = (((_NM_DEVICE_AUTOCONNECT_BLOCKED_LAST - 1) << 1) - 1), + + NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL = + NM_DEVICE_AUTOCONNECT_BLOCKED_ALL & ~NM_DEVICE_AUTOCONNECT_BLOCKED_USER, +} NMDeviceAutoconnectBlockedFlags; + +NMDeviceAutoconnectBlockedFlags +nm_device_autoconnect_blocked_get(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask); + +void nm_device_autoconnect_blocked_set_full(NMDevice * device, + NMDeviceAutoconnectBlockedFlags mask, + NMDeviceAutoconnectBlockedFlags values); + +static inline void +nm_device_autoconnect_blocked_set(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask) +{ + nm_device_autoconnect_blocked_set_full(device, mask, mask); +} + +static inline void +nm_device_autoconnect_blocked_unset(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask) +{ + nm_device_autoconnect_blocked_set_full(device, mask, NM_DEVICE_AUTOCONNECT_BLOCKED_NONE); +} + +void nm_device_emit_recheck_auto_activate(NMDevice *device); + +NMDeviceSysIfaceState nm_device_sys_iface_state_get(NMDevice *device); + +gboolean nm_device_sys_iface_state_is_external(NMDevice *self); +gboolean nm_device_sys_iface_state_is_external_or_assume(NMDevice *self); + +void nm_device_sys_iface_state_set(NMDevice *device, NMDeviceSysIfaceState sys_iface_state); + +void nm_device_state_changed(NMDevice *device, NMDeviceState state, NMDeviceStateReason reason); + +void nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason); + +gboolean nm_device_get_firmware_missing(NMDevice *self); + +void nm_device_disconnect_active_connection(NMActiveConnection * active, + NMDeviceStateReason device_reason, + NMActiveConnectionStateReason active_reason); + +void nm_device_queue_activation(NMDevice *device, NMActRequest *req); + +gboolean nm_device_supports_vlans(NMDevice *device); + +gboolean +nm_device_add_pending_action(NMDevice *device, const char *action, gboolean assert_not_yet_pending); +gboolean +nm_device_remove_pending_action(NMDevice *device, const char *action, gboolean assert_is_pending); +const char *nm_device_has_pending_action_reason(NMDevice *device); + +static inline gboolean +nm_device_has_pending_action(NMDevice *device) +{ + return !!nm_device_has_pending_action_reason(device); +} + +NMSettingsConnection * +nm_device_get_best_connection(NMDevice *device, const char *specific_object, GError **error); + +gboolean nm_device_check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error); + +void nm_device_notify_availability_maybe_changed(NMDevice *self); + +gboolean nm_device_owns_iface(NMDevice *device, const char *iface); + +NMConnection *nm_device_new_default_connection(NMDevice *self); + +const NMPObject *nm_device_get_best_default_route(NMDevice *self, int addr_family); + +void nm_device_spawn_iface_helper(NMDevice *self); + +gboolean nm_device_reapply(NMDevice *self, NMConnection *connection, GError **error); +void nm_device_reapply_settings_immediately(NMDevice *self); + +void nm_device_update_firewall_zone(NMDevice *self); +void nm_device_update_metered(NMDevice *self); +void nm_device_reactivate_ip_config(NMDevice * device, + int addr_family, + NMSettingIPConfig *s_ip_old, + NMSettingIPConfig *s_ip_new); + +gboolean nm_device_update_hw_address(NMDevice *self); +void nm_device_update_initial_hw_address(NMDevice *self); +void nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_freeze); +void nm_device_update_dynamic_ip_setup(NMDevice *self); +guint nm_device_get_supplicant_timeout(NMDevice *self); + +gboolean nm_device_auth_retries_try_next(NMDevice *self); + +gboolean nm_device_hw_addr_get_cloned(NMDevice * self, + NMConnection *connection, + gboolean is_wifi, + char ** hwaddr, + gboolean * preserve, + GError ** error); + +typedef struct _NMDeviceConnectivityHandle NMDeviceConnectivityHandle; + +typedef void (*NMDeviceConnectivityCallback)(NMDevice * self, + NMDeviceConnectivityHandle *handle, + NMConnectivityState state, + GError * error, + gpointer user_data); + +void nm_device_check_connectivity_update_interval(NMDevice *self); + +NMDeviceConnectivityHandle *nm_device_check_connectivity(NMDevice * self, + int addr_family, + NMDeviceConnectivityCallback callback, + gpointer user_data); + +void nm_device_check_connectivity_cancel(NMDeviceConnectivityHandle *handle); + +NMConnectivityState nm_device_get_connectivity_state(NMDevice *self, int addr_family); + +typedef struct _NMBtVTableNetworkServer NMBtVTableNetworkServer; + +typedef void (*NMBtVTableRegisterCallback)(GError *error, gpointer user_data); + +struct _NMBtVTableNetworkServer { + gboolean (*is_available)(const NMBtVTableNetworkServer *vtable, + const char * addr, + NMDevice * device_accept_busy); + + gboolean (*register_bridge)(const NMBtVTableNetworkServer *vtable, + const char * addr, + NMDevice * device, + GCancellable * cancellable, + NMBtVTableRegisterCallback callback, + gpointer callback_user_data, + GError ** error); + gboolean (*unregister_bridge)(const NMBtVTableNetworkServer *vtable, NMDevice *device); +}; + +const char *nm_device_state_to_str(NMDeviceState state); +const char *nm_device_state_reason_to_str(NMDeviceStateReason reason); + +gboolean nm_device_is_vpn(NMDevice *self); + +const char * +nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean *out_pending); + +void nm_device_clear_dns_lookup_data(NMDevice *self); + +#endif /* __NETWORKMANAGER_DEVICE_H__ */ diff --git a/src/core/devices/nm-lldp-listener.c b/src/core/devices/nm-lldp-listener.c new file mode 100644 index 00000000..c60fb3ad --- /dev/null +++ b/src/core/devices/nm-lldp-listener.c @@ -0,0 +1,1122 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-lldp-listener.h" + +#include <net/ethernet.h> + +#include "nm-std-aux/unaligned.h" +#include "platform/nm-platform.h" +#include "nm-glib-aux/nm-c-list.h" +#include "nm-utils.h" + +#include "systemd/nm-sd.h" + +#define MAX_NEIGHBORS 128 +#define MIN_UPDATE_INTERVAL_NSEC (2 * NM_UTILS_NSEC_PER_SEC) + +#define LLDP_MAC_NEAREST_BRIDGE \ + (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e}})) +#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE \ + (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x03}})) +#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE \ + (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x00}})) + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMLldpListener, PROP_NEIGHBORS, ); + +typedef struct { + sd_lldp * lldp_handle; + GHashTable *lldp_neighbors; + GVariant * variant; + + /* the timestamp in nsec until which we delay updates. */ + gint64 ratelimit_next_nsec; + guint ratelimit_id; + + int ifindex; +} NMLldpListenerPrivate; + +struct _NMLldpListener { + GObject parent; + NMLldpListenerPrivate _priv; +}; + +struct _NMLldpListenerClass { + GObjectClass parent; +}; + +G_DEFINE_TYPE(NMLldpListener, nm_lldp_listener, G_TYPE_OBJECT) + +#define NM_LLDP_LISTENER_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMLldpListener, NM_IS_LLDP_LISTENER) + +/*****************************************************************************/ + +typedef struct { + GVariant * variant; + sd_lldp_neighbor *neighbor_sd; + char * chassis_id; + char * port_id; + guint8 chassis_id_type; + guint8 port_id_type; +} LldpNeighbor; + +/*****************************************************************************/ + +#define _NMLOG_PREFIX_NAME "lldp" +#define _NMLOG_DOMAIN LOGD_DEVICE +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) { \ + char _sbuf[64]; \ + int _ifindex = (self) ? NM_LLDP_LISTENER_GET_PRIVATE(self)->ifindex : 0; \ + \ + _nm_log(_level, \ + _NMLOG_DOMAIN, \ + 0, \ + _ifindex > 0 ? nm_platform_link_get_name(NM_PLATFORM_GET, _ifindex) : NULL, \ + NULL, \ + "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + ((_ifindex > 0) ? nm_sprintf_buf(_sbuf, "[%p,%d]", (self), _ifindex) \ + : ((self) ? nm_sprintf_buf(_sbuf, "[%p]", (self)) : "")) \ + _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +#define LOG_NEIGH_FMT "CHASSIS=%u/%s PORT=%u/%s" +#define LOG_NEIGH_ARG(neigh) \ + (neigh)->chassis_id_type, (neigh)->chassis_id, (neigh)->port_id_type, (neigh)->port_id + +/*****************************************************************************/ + +static void +lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *out_raw_len) +{ + gconstpointer raw_data = NULL; + gsize raw_len = 0; + int r; + + nm_assert(neigh); + + r = sd_lldp_neighbor_get_raw(neigh->neighbor_sd, &raw_data, &raw_len); + + nm_assert(r >= 0); + nm_assert(raw_data); + nm_assert(raw_len > 0); + + *out_raw_data = raw_data; + *out_raw_len = raw_len; +} + +static gboolean +lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd, + guint8 * out_chassis_id_type, + const guint8 ** out_chassis_id, + gsize * out_chassis_id_len, + guint8 * out_port_id_type, + const guint8 ** out_port_id, + gsize * out_port_id_len) +{ + int r; + + r = sd_lldp_neighbor_get_chassis_id(neighbor_sd, + out_chassis_id_type, + (gconstpointer *) out_chassis_id, + out_chassis_id_len); + if (r < 0) + return FALSE; + + r = sd_lldp_neighbor_get_port_id(neighbor_sd, + out_port_id_type, + (gconstpointer *) out_port_id, + out_port_id_len); + if (r < 0) + return FALSE; + + return TRUE; +} + +static guint +lldp_neighbor_id_hash(gconstpointer ptr) +{ + const LldpNeighbor *neigh = ptr; + guint8 chassis_id_type; + guint8 port_id_type; + const guint8 * chassis_id; + const guint8 * port_id; + gsize chassis_id_len; + gsize port_id_len; + NMHashState h; + + if (!lldp_neighbor_id_get(neigh->neighbor_sd, + &chassis_id_type, + &chassis_id, + &chassis_id_len, + &port_id_type, + &port_id, + &port_id_len)) { + nm_assert_not_reached(); + return 0; + } + + nm_hash_init(&h, 23423423u); + nm_hash_update_vals(&h, chassis_id_len, port_id_len, chassis_id_type, port_id_type); + nm_hash_update(&h, chassis_id, chassis_id_len); + nm_hash_update(&h, port_id, port_id_len); + return nm_hash_complete(&h); +} + +static int +lldp_neighbor_id_cmp(const LldpNeighbor *a, const LldpNeighbor *b) +{ + guint8 a_chassis_id_type; + guint8 b_chassis_id_type; + guint8 a_port_id_type; + guint8 b_port_id_type; + const guint8 *a_chassis_id; + const guint8 *b_chassis_id; + const guint8 *a_port_id; + const guint8 *b_port_id; + gsize a_chassis_id_len; + gsize b_chassis_id_len; + gsize a_port_id_len; + gsize b_port_id_len; + + NM_CMP_SELF(a, b); + + if (!lldp_neighbor_id_get(a->neighbor_sd, + &a_chassis_id_type, + &a_chassis_id, + &a_chassis_id_len, + &a_port_id_type, + &a_port_id, + &a_port_id_len)) { + nm_assert_not_reached(); + return FALSE; + } + + if (!lldp_neighbor_id_get(b->neighbor_sd, + &b_chassis_id_type, + &b_chassis_id, + &b_chassis_id_len, + &b_port_id_type, + &b_port_id, + &b_port_id_len)) { + nm_assert_not_reached(); + return FALSE; + } + + NM_CMP_DIRECT(a_chassis_id_type, b_chassis_id_type); + NM_CMP_DIRECT(a_port_id_type, b_port_id_type); + NM_CMP_DIRECT(a_chassis_id_len, b_chassis_id_len); + NM_CMP_DIRECT(a_port_id_len, b_port_id_len); + NM_CMP_DIRECT_MEMCMP(a_chassis_id, b_chassis_id, a_chassis_id_len); + NM_CMP_DIRECT_MEMCMP(a_port_id, b_port_id, a_port_id_len); + return 0; +} + +static int +lldp_neighbor_id_cmp_p(gconstpointer a, gconstpointer b, gpointer user_data) +{ + return lldp_neighbor_id_cmp(*((const LldpNeighbor *const *) a), + *((const LldpNeighbor *const *) b)); +} + +static gboolean +lldp_neighbor_id_equal(gconstpointer a, gconstpointer b) +{ + return lldp_neighbor_id_cmp(a, b) == 0; +} + +static void +lldp_neighbor_free(LldpNeighbor *neighbor) +{ + if (!neighbor) + return; + + g_free(neighbor->chassis_id); + g_free(neighbor->port_id); + nm_g_variant_unref(neighbor->variant); + sd_lldp_neighbor_unref(neighbor->neighbor_sd); + nm_g_slice_free(neighbor); +} + +static void +lldp_neighbor_freep(LldpNeighbor **ptr) +{ + lldp_neighbor_free(*ptr); +} + +static gboolean +lldp_neighbor_equal(LldpNeighbor *a, LldpNeighbor *b) +{ + const guint8 *raw_data_a; + const guint8 *raw_data_b; + gsize raw_len_a; + gsize raw_len_b; + + if (a->neighbor_sd == b->neighbor_sd) + return TRUE; + + lldp_neighbor_get_raw(a, &raw_data_a, &raw_len_a); + lldp_neighbor_get_raw(b, &raw_data_b, &raw_len_b); + return raw_len_a == raw_len_b && (memcmp(raw_data_a, raw_data_b, raw_len_a) == 0); +} + +static GVariant * +parse_management_address_tlv(const uint8_t *data, gsize len) +{ + GVariantBuilder builder; + gsize addr_len; + const guint8 * v_object_id_arr; + gsize v_object_id_len; + const guint8 * v_address_arr; + gsize v_address_len; + guint32 v_interface_number; + guint32 v_interface_number_subtype; + guint32 v_address_subtype; + + /* 802.1AB-2009 - Figure 8-11 + * + * - TLV type / length (2 bytes) + * - address string length (1 byte) + * - address subtype (1 byte) + * - address (1 to 31 bytes) + * - interface number subtype (1 byte) + * - interface number (4 bytes) + * - OID string length (1 byte) + * - OID (0 to 128 bytes) + */ + + if (len < 11) + return NULL; + + nm_assert((data[0] >> 1) == SD_LLDP_TYPE_MGMT_ADDRESS); + nm_assert((((data[0] & 1) << 8) + data[1]) + 2 == len); + + data += 2; + len -= 2; + addr_len = *data; /* length of (address subtype + address) */ + + if (addr_len < 2 || addr_len > 32) + return NULL; + if (len < (1 /* address stringth length */ + + addr_len /* address subtype + address */ + + 5 /* interface */ + + 1)) /* oid */ + return NULL; + + data++; + len--; + v_address_subtype = *data; + v_address_arr = &data[1]; + v_address_len = addr_len - 1; + + data += addr_len; + len -= addr_len; + v_interface_number_subtype = *data; + + data++; + len--; + v_interface_number = unaligned_read_be32(data); + + data += 4; + len -= 4; + v_object_id_len = *data; + if (len < (1 + v_object_id_len)) + return NULL; + data++; + v_object_id_arr = data; + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + nm_g_variant_builder_add_sv_uint32(&builder, "address-subtype", v_address_subtype); + nm_g_variant_builder_add_sv_bytearray(&builder, "address", v_address_arr, v_address_len); + nm_g_variant_builder_add_sv_uint32(&builder, + "interface-number-subtype", + v_interface_number_subtype); + nm_g_variant_builder_add_sv_uint32(&builder, "interface-number", v_interface_number); + if (v_object_id_len > 0) + nm_g_variant_builder_add_sv_bytearray(&builder, + "object-id", + v_object_id_arr, + v_object_id_len); + return g_variant_builder_end(&builder); +} + +static char * +format_network_address(const guint8 *data, gsize sz) +{ + NMIPAddr a; + int family; + + if (sz == 5 && data[0] == 1 /* LLDP_MGMT_ADDR_IP4 */) { + memcpy(&a, &data[1], sizeof(a.addr4)); + family = AF_INET; + } else if (sz == 17 && data[0] == 2 /* LLDP_MGMT_ADDR_IP6 */) { + memcpy(&a, &data[1], sizeof(a.addr6)); + family = AF_INET6; + } else + return NULL; + + return nm_utils_inet_ntop_dup(family, &a); +} + +static const char * +format_string(const guint8 *data, gsize len, gboolean allow_trim, char **out_to_free) +{ + gboolean is_null_terminated = FALSE; + + nm_assert(out_to_free && !*out_to_free); + + if (allow_trim) { + while (len > 0 && data[len - 1] == '\0') { + is_null_terminated = TRUE; + len--; + } + } + + if (len == 0) + return NULL; + + if (memchr(data, len, '\0')) + return NULL; + + return nm_utils_buf_utf8safe_escape(data, + is_null_terminated ? -1 : (gssize) len, + NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL + | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII, + out_to_free); +} + +static char * +format_string_cp(const guint8 *data, gsize len, gboolean allow_trim) +{ + char * s_free = NULL; + const char *s; + + s = format_string(data, len, allow_trim, &s_free); + nm_assert(!s_free || s == s_free); + return s ? (s_free ?: g_strdup(s)) : NULL; +} + +static LldpNeighbor * +lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd) +{ + LldpNeighbor *neigh; + guint8 chassis_id_type; + guint8 port_id_type; + const guint8 *chassis_id; + const guint8 *port_id; + gsize chassis_id_len; + gsize port_id_len; + gs_free char *s_chassis_id = NULL; + gs_free char *s_port_id = NULL; + + if (!lldp_neighbor_id_get(neighbor_sd, + &chassis_id_type, + &chassis_id, + &chassis_id_len, + &port_id_type, + &port_id, + &port_id_len)) + return NULL; + + switch (chassis_id_type) { + case SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT: + case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS: + case SD_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT: + case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME: + case SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED: + s_chassis_id = format_string_cp(chassis_id, chassis_id_len, FALSE); + break; + case SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS: + s_chassis_id = nm_utils_hwaddr_ntoa(chassis_id, chassis_id_len); + break; + case SD_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS: + s_chassis_id = format_network_address(chassis_id, chassis_id_len); + break; + } + if (!s_chassis_id) { + /* Invalid/unsupported chassis_id? Expose as hex string. This format is not stable, and + * in the future we may add a better string representation for these case (thus + * changing the API). */ + s_chassis_id = nm_utils_bin2hexstr_full(chassis_id, chassis_id_len, '\0', FALSE, NULL); + } + + switch (port_id_type) { + case SD_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS: + case SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT: + case SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME: + case SD_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED: + s_port_id = format_string_cp(port_id, port_id_len, FALSE); + break; + case SD_LLDP_PORT_SUBTYPE_MAC_ADDRESS: + s_port_id = nm_utils_hwaddr_ntoa(port_id, port_id_len); + break; + case SD_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS: + s_port_id = format_network_address(port_id, port_id_len); + break; + } + if (!s_port_id) { + /* Invalid/unsupported port_id? Expose as hex string. This format is not stable, and + * in the future we may add a better string representation for these case (thus + * changing the API). */ + s_port_id = nm_utils_bin2hexstr_full(port_id, port_id_len, '\0', FALSE, NULL); + } + + neigh = g_slice_new(LldpNeighbor); + *neigh = (LldpNeighbor){ + .neighbor_sd = sd_lldp_neighbor_ref(neighbor_sd), + .chassis_id_type = chassis_id_type, + .chassis_id = g_steal_pointer(&s_chassis_id), + .port_id_type = port_id_type, + .port_id = g_steal_pointer(&s_port_id), + }; + return neigh; +} + +static GVariant * +lldp_neighbor_to_variant(LldpNeighbor *neigh) +{ + struct ether_addr destination_address; + GVariantBuilder builder; + const char * str; + const guint8 * raw_data; + gsize raw_len; + uint16_t u16; + uint8_t * data8; + gsize len; + int r; + + if (neigh->variant) + return neigh->variant; + + lldp_neighbor_get_raw(neigh, &raw_data, &raw_len); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + + nm_g_variant_builder_add_sv_bytearray(&builder, NM_LLDP_ATTR_RAW, raw_data, raw_len); + nm_g_variant_builder_add_sv_uint32(&builder, + NM_LLDP_ATTR_CHASSIS_ID_TYPE, + neigh->chassis_id_type); + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_CHASSIS_ID, neigh->chassis_id); + nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_PORT_ID_TYPE, neigh->port_id_type); + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_ID, neigh->port_id); + + r = sd_lldp_neighbor_get_destination_address(neigh->neighbor_sd, &destination_address); + if (r < 0) + str = NULL; + else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE)) + str = NM_LLDP_DEST_NEAREST_BRIDGE; + else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_NON_TPMR_BRIDGE)) + str = NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE; + else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_CUSTOMER_BRIDGE)) + str = NM_LLDP_DEST_NEAREST_CUSTOMER_BRIDGE; + else + str = NULL; + if (str) + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_DESTINATION, str); + + if (sd_lldp_neighbor_get_port_description(neigh->neighbor_sd, &str) == 0) + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_DESCRIPTION, str); + + if (sd_lldp_neighbor_get_system_name(neigh->neighbor_sd, &str) == 0) + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_NAME, str); + + if (sd_lldp_neighbor_get_system_description(neigh->neighbor_sd, &str) == 0) + nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, str); + + if (sd_lldp_neighbor_get_system_capabilities(neigh->neighbor_sd, &u16) == 0) + nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, u16); + + r = sd_lldp_neighbor_tlv_rewind(neigh->neighbor_sd); + if (r < 0) + nm_assert_not_reached(); + else { + gboolean v_management_addresses_has = FALSE; + GVariantBuilder v_management_addresses; + GVariant * v_ieee_802_1_pvid = NULL; + GVariant * v_ieee_802_1_ppvid = NULL; + GVariant * v_ieee_802_1_ppvid_flags = NULL; + GVariantBuilder v_ieee_802_1_ppvids; + GVariant * v_ieee_802_1_vid = NULL; + GVariant * v_ieee_802_1_vlan_name = NULL; + GVariantBuilder v_ieee_802_1_vlans; + GVariant * v_ieee_802_3_mac_phy_conf = NULL; + GVariant * v_ieee_802_3_power_via_mdi = NULL; + GVariant * v_ieee_802_3_max_frame_size = NULL; + GVariant * v_mud_url = NULL; + GVariantBuilder tmp_builder; + GVariant * tmp_variant; + + do { + guint8 oui[3]; + guint8 type; + guint8 subtype; + + if (sd_lldp_neighbor_tlv_get_type(neigh->neighbor_sd, &type) < 0) + continue; + + if (sd_lldp_neighbor_tlv_get_raw(neigh->neighbor_sd, (void *) &data8, &len) < 0) + continue; + + switch (type) { + case SD_LLDP_TYPE_MGMT_ADDRESS: + tmp_variant = parse_management_address_tlv(data8, len); + if (tmp_variant) { + if (!v_management_addresses_has) { + v_management_addresses_has = TRUE; + g_variant_builder_init(&v_management_addresses, G_VARIANT_TYPE("aa{sv}")); + } + g_variant_builder_add_value(&v_management_addresses, tmp_variant); + } + continue; + case SD_LLDP_TYPE_PRIVATE: + break; + default: + continue; + } + + r = sd_lldp_neighbor_tlv_get_oui(neigh->neighbor_sd, oui, &subtype); + if (r < 0) { + if (r == -ENXIO) + continue; + + /* in other cases, something is seriously wrong. Abort, but + * keep what we parsed so far. */ + break; + } + + if (len <= 6) + continue; + + /* skip over leading TLV, OUI and subtype */ +#if NM_MORE_ASSERTS > 5 + { + guint8 check_hdr[] = {0xfe | (((len - 2) >> 8) & 0x01), + ((len - 2) & 0xFF), + oui[0], + oui[1], + oui[2], + subtype}; + + nm_assert(len > 2 + 3 + 1); + nm_assert(memcmp(data8, check_hdr, sizeof check_hdr) == 0); + } +#endif + data8 += 6; + len -= 6; + + if (memcmp(oui, SD_LLDP_OUI_802_1, sizeof(oui)) == 0) { + switch (subtype) { + case SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID: + if (len != 2) + continue; + if (!v_ieee_802_1_pvid) + v_ieee_802_1_pvid = g_variant_new_uint32(unaligned_read_be16(data8)); + break; + case SD_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID: + if (len != 3) + continue; + if (!v_ieee_802_1_ppvid) { + v_ieee_802_1_ppvid_flags = g_variant_new_uint32(data8[0]); + v_ieee_802_1_ppvid = g_variant_new_uint32(unaligned_read_be16(&data8[1])); + g_variant_builder_init(&v_ieee_802_1_ppvids, G_VARIANT_TYPE("aa{sv}")); + } + g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}")); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, + "ppvid", + unaligned_read_be16(&data8[1])); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, "flags", data8[0]); + g_variant_builder_add_value(&v_ieee_802_1_ppvids, + g_variant_builder_end(&tmp_builder)); + break; + case SD_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME: + { + gs_free char *name_to_free = NULL; + const char * name; + guint32 vid; + gsize l; + + if (len <= 3) + continue; + + l = data8[2]; + if (len != 3 + l) + continue; + if (l > 32) + continue; + + name = format_string(&data8[3], l, TRUE, &name_to_free); + if (!name) + continue; + + vid = unaligned_read_be16(&data8[0]); + if (!v_ieee_802_1_vid) { + v_ieee_802_1_vid = g_variant_new_uint32(vid); + v_ieee_802_1_vlan_name = g_variant_new_string(name); + g_variant_builder_init(&v_ieee_802_1_vlans, G_VARIANT_TYPE("aa{sv}")); + } + g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}")); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, "vid", vid); + nm_g_variant_builder_add_sv_str(&tmp_builder, "name", name); + g_variant_builder_add_value(&v_ieee_802_1_vlans, + g_variant_builder_end(&tmp_builder)); + break; + } + default: + continue; + } + } else if (memcmp(oui, SD_LLDP_OUI_802_3, sizeof(oui)) == 0) { + switch (subtype) { + case SD_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS: + if (len != 5) + continue; + + if (!v_ieee_802_3_mac_phy_conf) { + g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}")); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, "autoneg", data8[0]); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, + "pmd-autoneg-cap", + unaligned_read_be16(&data8[1])); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, + "operational-mau-type", + unaligned_read_be16(&data8[3])); + v_ieee_802_3_mac_phy_conf = g_variant_builder_end(&tmp_builder); + } + break; + case SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI: + if (len != 3) + continue; + + if (!v_ieee_802_3_power_via_mdi) { + g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}")); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, + "mdi-power-support", + data8[0]); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, + "pse-power-pair", + data8[1]); + nm_g_variant_builder_add_sv_uint32(&tmp_builder, "power-class", data8[2]); + v_ieee_802_3_power_via_mdi = g_variant_builder_end(&tmp_builder); + } + break; + case SD_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE: + if (len != 2) + continue; + if (!v_ieee_802_3_max_frame_size) + v_ieee_802_3_max_frame_size = + g_variant_new_uint32(unaligned_read_be16(data8)); + break; + } + } else if (memcmp(oui, SD_LLDP_OUI_MUD, sizeof(oui)) == 0) { + switch (subtype) { + case SD_LLDP_OUI_SUBTYPE_MUD_USAGE_DESCRIPTION: + if (!v_mud_url) { + gs_free char *s_free = NULL; + const char * s; + + s = format_string(data8, len, TRUE, &s_free); + if (s) + v_mud_url = g_variant_new_string(s); + } + break; + } + } + } while (sd_lldp_neighbor_tlv_next(neigh->neighbor_sd) > 0); + + if (v_management_addresses_has) + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_MANAGEMENT_ADDRESSES, + g_variant_builder_end(&v_management_addresses)); + if (v_ieee_802_1_pvid) + nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_PVID, v_ieee_802_1_pvid); + if (v_ieee_802_1_ppvid) { + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_1_PPVID, + v_ieee_802_1_ppvid); + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS, + v_ieee_802_1_ppvid_flags); + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_1_PPVIDS, + g_variant_builder_end(&v_ieee_802_1_ppvids)); + } + if (v_ieee_802_1_vid) { + nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_VID, v_ieee_802_1_vid); + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME, + v_ieee_802_1_vlan_name); + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_1_VLANS, + g_variant_builder_end(&v_ieee_802_1_vlans)); + } + if (v_ieee_802_3_mac_phy_conf) + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF, + v_ieee_802_3_mac_phy_conf); + if (v_ieee_802_3_power_via_mdi) + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI, + v_ieee_802_3_power_via_mdi); + if (v_ieee_802_3_max_frame_size) + nm_g_variant_builder_add_sv(&builder, + NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE, + v_ieee_802_3_max_frame_size); + if (v_mud_url) + nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_MUD_URL, v_mud_url); + } + + return (neigh->variant = g_variant_ref_sink(g_variant_builder_end(&builder))); +} + +/*****************************************************************************/ + +GVariant * +nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len) +{ + nm_auto(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *neighbor_sd = NULL; + nm_auto(lldp_neighbor_freep) LldpNeighbor * neigh = NULL; + GVariant * variant; + int r; + + g_assert(raw_data); + g_assert(raw_len > 0); + + r = sd_lldp_neighbor_from_raw(&neighbor_sd, raw_data, raw_len); + g_assert(r >= 0); + + neigh = lldp_neighbor_new(neighbor_sd); + g_assert(neigh); + + variant = lldp_neighbor_to_variant(neigh); + g_assert(variant); + + return g_variant_ref(variant); +} + +/*****************************************************************************/ + +static void +data_changed_notify(NMLldpListener *self, NMLldpListenerPrivate *priv) +{ + nm_clear_g_variant(&priv->variant); + _notify(self, PROP_NEIGHBORS); +} + +static gboolean +data_changed_timeout(gpointer user_data) +{ + NMLldpListener * self = user_data; + NMLldpListenerPrivate *priv; + + g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), G_SOURCE_REMOVE); + + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + priv->ratelimit_id = 0; + priv->ratelimit_next_nsec = nm_utils_get_monotonic_timestamp_nsec() + MIN_UPDATE_INTERVAL_NSEC; + data_changed_notify(self, priv); + return G_SOURCE_REMOVE; +} + +static void +data_changed_schedule(NMLldpListener *self) +{ + NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + gint64 now_nsec; + + if (priv->ratelimit_id != 0) + return; + + now_nsec = nm_utils_get_monotonic_timestamp_nsec(); + if (now_nsec < priv->ratelimit_next_nsec) { + priv->ratelimit_id = + g_timeout_add_full(G_PRIORITY_LOW, + NM_UTILS_NSEC_TO_MSEC_CEIL(priv->ratelimit_next_nsec - now_nsec), + data_changed_timeout, + self, + NULL); + return; + } + + priv->ratelimit_id = g_idle_add_full(G_PRIORITY_LOW, data_changed_timeout, self, NULL); +} + +static void +process_lldp_neighbor(NMLldpListener *self, sd_lldp_neighbor *neighbor_sd, gboolean remove) +{ + NMLldpListenerPrivate * priv; + nm_auto(lldp_neighbor_freep) LldpNeighbor *neigh = NULL; + LldpNeighbor * neigh_old; + + g_return_if_fail(NM_IS_LLDP_LISTENER(self)); + + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + g_return_if_fail(priv->lldp_handle); + g_return_if_fail(neighbor_sd); + + nm_assert(priv->lldp_neighbors); + + neigh = lldp_neighbor_new(neighbor_sd); + if (!neigh) { + _LOGT("process: failed to parse neighbor"); + return; + } + + neigh_old = g_hash_table_lookup(priv->lldp_neighbors, neigh); + + if (remove) { + if (neigh_old) { + _LOGT("process: %s neigh: " LOG_NEIGH_FMT, "remove", LOG_NEIGH_ARG(neigh)); + + g_hash_table_remove(priv->lldp_neighbors, neigh_old); + goto handle_changed; + } + return; + } + + if (neigh_old && lldp_neighbor_equal(neigh_old, neigh)) + return; + + _LOGD("process: %s neigh: " LOG_NEIGH_FMT, neigh_old ? "update" : "new", LOG_NEIGH_ARG(neigh)); + + g_hash_table_add(priv->lldp_neighbors, g_steal_pointer(&neigh)); + +handle_changed: + data_changed_schedule(self); +} + +static void +lldp_event_handler(sd_lldp *lldp, sd_lldp_event event, sd_lldp_neighbor *n, void *userdata) +{ + process_lldp_neighbor( + userdata, + n, + !NM_IN_SET(event, SD_LLDP_EVENT_ADDED, SD_LLDP_EVENT_UPDATED, SD_LLDP_EVENT_REFRESHED)); +} + +gboolean +nm_lldp_listener_start(NMLldpListener *self, int ifindex, GError **error) +{ + NMLldpListenerPrivate *priv; + int ret; + + g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE); + g_return_val_if_fail(ifindex > 0, FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + if (priv->lldp_handle) { + g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "already running"); + return FALSE; + } + + ret = sd_lldp_new(&priv->lldp_handle); + if (ret < 0) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "initialization failed"); + return FALSE; + } + + ret = sd_lldp_set_ifindex(priv->lldp_handle, ifindex); + if (ret < 0) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "failed setting ifindex"); + goto err; + } + + ret = sd_lldp_set_callback(priv->lldp_handle, lldp_event_handler, self); + if (ret < 0) { + g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "set callback failed"); + goto err; + } + + ret = sd_lldp_set_neighbors_max(priv->lldp_handle, MAX_NEIGHBORS); + nm_assert(ret == 0); + + priv->ifindex = ifindex; + + ret = sd_lldp_attach_event(priv->lldp_handle, NULL, 0); + if (ret < 0) { + g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "attach event failed"); + goto err_free; + } + + ret = sd_lldp_start(priv->lldp_handle); + if (ret < 0) { + g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "start failed"); + goto err; + } + + priv->lldp_neighbors = g_hash_table_new_full(lldp_neighbor_id_hash, + lldp_neighbor_id_equal, + (GDestroyNotify) lldp_neighbor_free, + NULL); + + _LOGD("start"); + + return TRUE; + +err: + sd_lldp_detach_event(priv->lldp_handle); +err_free: + sd_lldp_unref(priv->lldp_handle); + priv->lldp_handle = NULL; + priv->ifindex = 0; + return FALSE; +} + +void +nm_lldp_listener_stop(NMLldpListener *self) +{ + NMLldpListenerPrivate *priv; + guint size; + gboolean changed = FALSE; + + g_return_if_fail(NM_IS_LLDP_LISTENER(self)); + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + if (priv->lldp_handle) { + _LOGD("stop"); + sd_lldp_stop(priv->lldp_handle); + sd_lldp_detach_event(priv->lldp_handle); + sd_lldp_unref(priv->lldp_handle); + priv->lldp_handle = NULL; + + size = g_hash_table_size(priv->lldp_neighbors); + g_hash_table_remove_all(priv->lldp_neighbors); + nm_clear_pointer(&priv->lldp_neighbors, g_hash_table_unref); + if (size > 0 || priv->ratelimit_id != 0) + changed = TRUE; + } + + nm_clear_g_source(&priv->ratelimit_id); + priv->ratelimit_next_nsec = 0; + priv->ifindex = 0; + + if (changed) + data_changed_notify(self, priv); +} + +gboolean +nm_lldp_listener_is_running(NMLldpListener *self) +{ + NMLldpListenerPrivate *priv; + + g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE); + + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + return !!priv->lldp_handle; +} + +GVariant * +nm_lldp_listener_get_neighbors(NMLldpListener *self) +{ + NMLldpListenerPrivate *priv; + + g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE); + + priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + if (G_UNLIKELY(!priv->variant)) { + gs_free LldpNeighbor **neighbors = NULL; + GVariantBuilder array_builder; + guint i, n; + + g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aa{sv}")); + neighbors = (LldpNeighbor **) + nm_utils_hash_keys_to_array(priv->lldp_neighbors, lldp_neighbor_id_cmp_p, NULL, &n); + for (i = 0; i < n; i++) + g_variant_builder_add_value(&array_builder, lldp_neighbor_to_variant(neighbors[i])); + priv->variant = g_variant_ref_sink(g_variant_builder_end(&array_builder)); + } + return priv->variant; +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMLldpListener *self = NM_LLDP_LISTENER(object); + + switch (prop_id) { + case PROP_NEIGHBORS: + g_value_set_variant(value, nm_lldp_listener_get_neighbors(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +nm_lldp_listener_init(NMLldpListener *self) +{ + _LOGT("lldp listener created"); +} + +NMLldpListener * +nm_lldp_listener_new(void) +{ + return g_object_new(NM_TYPE_LLDP_LISTENER, NULL); +} + +static void +dispose(GObject *object) +{ + nm_lldp_listener_stop(NM_LLDP_LISTENER(object)); + + G_OBJECT_CLASS(nm_lldp_listener_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMLldpListener * self = NM_LLDP_LISTENER(object); + NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self); + + nm_lldp_listener_stop(self); + + nm_clear_g_variant(&priv->variant); + + _LOGT("lldp listener destroyed"); + + G_OBJECT_CLASS(nm_lldp_listener_parent_class)->finalize(object); +} + +static void +nm_lldp_listener_class_init(NMLldpListenerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dispose; + object_class->finalize = finalize; + object_class->get_property = get_property; + + obj_properties[PROP_NEIGHBORS] = + g_param_spec_variant(NM_LLDP_LISTENER_NEIGHBORS, + "", + "", + G_VARIANT_TYPE("aa{sv}"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/nm-lldp-listener.h b/src/core/devices/nm-lldp-listener.h new file mode 100644 index 00000000..9d3e2436 --- /dev/null +++ b/src/core/devices/nm-lldp-listener.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#ifndef __NM_LLDP_LISTENER__ +#define __NM_LLDP_LISTENER__ + +#define NM_TYPE_LLDP_LISTENER (nm_lldp_listener_get_type()) +#define NM_LLDP_LISTENER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LLDP_LISTENER, NMLldpListener)) +#define NM_LLDP_LISTENER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_LLDP_LISTENER, NMLldpListenerClass)) +#define NM_IS_LLDP_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_LLDP_LISTENER)) +#define NM_IS_LLDP_LISTENER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_LLDP_LISTENER)) +#define NM_LLDP_LISTENER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_LLDP_LISTENER, NMLldpListenerClass)) + +#define NM_LLDP_LISTENER_NEIGHBORS "neighbors" + +typedef struct _NMLldpListenerClass NMLldpListenerClass; + +GType nm_lldp_listener_get_type(void); +NMLldpListener *nm_lldp_listener_new(void); +gboolean nm_lldp_listener_start(NMLldpListener *self, int ifindex, GError **error); +void nm_lldp_listener_stop(NMLldpListener *self); +gboolean nm_lldp_listener_is_running(NMLldpListener *self); + +GVariant *nm_lldp_listener_get_neighbors(NMLldpListener *self); + +GVariant *nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len); + +#endif /* __NM_LLDP_LISTENER__ */ diff --git a/src/core/devices/ovs/meson.build b/src/core/devices/ovs/meson.build new file mode 100644 index 00000000..81c29bd6 --- /dev/null +++ b/src/core/devices/ovs/meson.build @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +libnm_device_plugin_ovs = shared_module( + 'nm-device-plugin-ovs', + sources: files( + 'nm-device-ovs-bridge.c', + 'nm-device-ovs-interface.c', + 'nm-device-ovs-port.c', + 'nm-ovsdb.c', + 'nm-ovs-factory.c', + ), + dependencies: [ + core_plugin_dep, + jansson_dep, + ], + c_args: daemon_c_flags, + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, +) + +core_plugins += libnm_device_plugin_ovs + +test( + 'check-local-devices-ovs', + check_exports, + args: [ + libnm_device_plugin_ovs.full_path(), + linker_script_devices, + ], +) diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c new file mode 100644 index 00000000..3ae8a481 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-bridge.c @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ovs-bridge.h" + +#include "nm-device-ovs-interface.h" +#include "nm-device-ovs-port.h" +#include "nm-ovsdb.h" + +#include "devices/nm-device-private.h" +#include "nm-active-connection.h" +#include "nm-setting-connection.h" +#include "nm-setting-ovs-bridge.h" +#include "nm-setting-ovs-external-ids.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceOvsBridge +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceOvsBridge { + NMDevice parent; +}; + +struct _NMDeviceOvsBridgeClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceOvsBridge, nm_device_ovs_bridge, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +static const char * +get_type_description(NMDevice *device) +{ + return "ovs-bridge"; +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + /* The actual backing resources will be created on enslavement by the port + * when it can identify the port and the bridge. */ + + return TRUE; +} + +static gboolean +unrealize(NMDevice *device, GError **error) +{ + return TRUE; +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + return NM_ACT_STAGE_RETURN_IP_FAIL; +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + if (!configure) + return TRUE; + + if (!NM_IS_DEVICE_OVS_PORT(slave)) + return FALSE; + + return TRUE; +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{} + +void +nm_device_ovs_reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new) +{ + NMDeviceType device_type; + GType type; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(g_type_parent(G_TYPE_FROM_INSTANCE(self)) == NM_TYPE_DEVICE); + + /* NMDevice's reapply_connection() doesn't do anything. No need to call the parent + * implementation. */ + + _LOGD(LOGD_DEVICE, "reapplying settings for OVS device"); + + type = G_OBJECT_TYPE(self); + if (type == NM_TYPE_DEVICE_OVS_INTERFACE) + device_type = NM_DEVICE_TYPE_OVS_INTERFACE; + else if (type == NM_TYPE_DEVICE_OVS_PORT) + device_type = NM_DEVICE_TYPE_OVS_PORT; + else { + nm_assert(type == NM_TYPE_DEVICE_OVS_BRIDGE); + device_type = NM_DEVICE_TYPE_OVS_BRIDGE; + } + + nm_ovsdb_set_external_ids( + nm_ovsdb_get(), + device_type, + nm_device_get_ip_iface(self), + nm_connection_get_uuid(con_new), + _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_EXTERNAL_IDS), + _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_EXTERNAL_IDS)); +} + +/*****************************************************************************/ + +static void +nm_device_ovs_bridge_init(NMDeviceOvsBridge *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_ovs_bridge = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_OVS_BRIDGE, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ovs_bridge_class_init(NMDeviceOvsBridgeClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_bridge); + + device_class->connection_type_supported = NM_SETTING_OVS_BRIDGE_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_OVS_BRIDGE_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(); + + device_class->is_master = TRUE; + device_class->get_type_description = get_type_description; + device_class->create_and_realize = create_and_realize; + device_class->unrealize = unrealize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + device_class->can_reapply_change_ovs_external_ids = TRUE; + device_class->reapply_connection = nm_device_ovs_reapply_connection; +} diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.h b/src/core/devices/ovs/nm-device-ovs-bridge.h new file mode 100644 index 00000000..2b893343 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-bridge.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__ +#define __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__ + +#define NM_TYPE_DEVICE_OVS_BRIDGE (nm_device_ovs_bridge_get_type()) +#define NM_DEVICE_OVS_BRIDGE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridge)) +#define NM_DEVICE_OVS_BRIDGE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridgeClass)) +#define NM_IS_DEVICE_OVS_BRIDGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_BRIDGE)) +#define NM_IS_DEVICE_OVS_BRIDGE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_BRIDGE)) +#define NM_DEVICE_OVS_BRIDGE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridgeClass)) + +typedef struct _NMDeviceOvsBridge NMDeviceOvsBridge; +typedef struct _NMDeviceOvsBridgeClass NMDeviceOvsBridgeClass; + +GType nm_device_ovs_bridge_get_type(void); + +/*****************************************************************************/ + +void +nm_device_ovs_reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new); + +#endif /* __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__ */ diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c new file mode 100644 index 00000000..5d07c211 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-interface.c @@ -0,0 +1,445 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ovs-interface.h" + +#include "nm-device-ovs-bridge.h" +#include "nm-ovsdb.h" + +#include "devices/nm-device-private.h" +#include "nm-active-connection.h" +#include "nm-setting-connection.h" +#include "nm-setting-ovs-interface.h" +#include "nm-setting-ovs-port.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceOvsInterface +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +typedef struct { + NMOvsdb *ovsdb; + bool waiting_for_interface : 1; +} NMDeviceOvsInterfacePrivate; + +struct _NMDeviceOvsInterface { + NMDevice parent; + NMDeviceOvsInterfacePrivate _priv; +}; + +struct _NMDeviceOvsInterfaceClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceOvsInterface, nm_device_ovs_interface, NM_TYPE_DEVICE) + +#define NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceOvsInterface, NM_IS_DEVICE_OVS_INTERFACE, NMDevice) + +/*****************************************************************************/ + +static const char * +get_type_description(NMDevice *device) +{ + return "ovs-interface"; +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + /* The actual backing resources will be created once an interface is + * added to a port of ours, since there can be neither an empty port nor + * an empty bridge. */ + + return TRUE; +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(device); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + + return nm_ovsdb_is_ready(priv->ovsdb); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMSettingOvsInterface *s_ovs_iface; + + if (!NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_ovs_iface = nm_connection_get_setting_ovs_interface(connection); + + if (!NM_IN_STRSET(nm_setting_ovs_interface_get_interface_type(s_ovs_iface), + "dpdk", + "internal", + "patch")) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "unsupported OVS interface type in profile"); + return FALSE; + } + + return TRUE; +} + +static void +link_changed(NMDevice *device, const NMPlatformLink *pllink) +{ + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device); + + if (!pllink || !priv->waiting_for_interface) + return; + + priv->waiting_for_interface = FALSE; + + if (nm_device_get_state(device) == NM_DEVICE_STATE_IP_CONFIG) { + if (!nm_device_hw_addr_set_cloned(device, + nm_device_get_applied_connection(device), + FALSE)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_CONFIG_FAILED); + return; + } + nm_device_bring_up(device, TRUE, NULL); + nm_device_activate_schedule_stage3_ip_config_start(device); + } +} + +static gboolean +_is_internal_interface(NMDevice *device) +{ + NMSettingOvsInterface *s_ovs_iface; + + s_ovs_iface = nm_device_get_applied_setting(device, NM_TYPE_SETTING_OVS_INTERFACE); + + g_return_val_if_fail(s_ovs_iface, FALSE); + + return nm_streq(nm_setting_ovs_interface_get_interface_type(s_ovs_iface), "internal"); +} + +static void +set_platform_mtu_cb(GError *error, gpointer user_data) +{ + NMDevice * device = user_data; + NMDeviceOvsInterface *self = NM_DEVICE_OVS_INTERFACE(device); + + if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) { + _LOGW(LOGD_DEVICE, + "could not change mtu of '%s': %s", + nm_device_get_iface(device), + error->message); + } + + g_object_unref(device); +} + +static gboolean +set_platform_mtu(NMDevice *device, guint32 mtu) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(device); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + + /* + * If the MTU is not set in ovsdb, Open vSwitch will change + * the MTU of an internal interface to match the minimum of + * the other interfaces in the bridge. + */ + /* FIXME(shutdown): the function should become cancellable so + * that it doesn't need to hold a reference to the device, and + * it can be stopped during shutdown. + */ + if (_is_internal_interface(device)) { + nm_ovsdb_set_interface_mtu(priv->ovsdb, + nm_device_get_ip_iface(device), + mtu, + set_platform_mtu_cb, + g_object_ref(device)); + } + + return NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class)->set_platform_mtu(device, mtu); +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(device); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device); + + if (!_is_internal_interface(device)) + return NM_ACT_STAGE_RETURN_IP_FAIL; + + if (nm_device_get_ip_ifindex(device) <= 0) { + _LOGT(LOGD_DEVICE, "waiting for link to appear"); + priv->waiting_for_interface = TRUE; + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) { + *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static gboolean +can_unmanaged_external_down(NMDevice *self) +{ + return FALSE; +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(device); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + + priv->waiting_for_interface = FALSE; +} + +typedef struct { + NMDeviceOvsInterface * self; + GCancellable * cancellable; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + gulong link_changed_id; + gulong cancelled_id; + guint link_timeout_id; +} DeactivateData; + +static void +deactivate_invoke_cb(DeactivateData *data, GError *error) +{ + NMDeviceOvsInterface *self = data->self; + + _LOGT(LOGD_CORE, "deactivate: async callback (%s)", error ? error->message : "success"); + data->callback(NM_DEVICE(data->self), error, data->callback_user_data); + + nm_clear_g_signal_handler(nm_device_get_platform(NM_DEVICE(data->self)), + &data->link_changed_id); + nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id); + nm_clear_g_source(&data->link_timeout_id); + g_object_unref(data->self); + g_object_unref(data->cancellable); + nm_g_slice_free(data); +} + +static void +deactivate_link_changed_cb(NMPlatform * platform, + int obj_type_i, + int ifindex, + NMPlatformLink *info, + int change_type_i, + DeactivateData *data) +{ + NMDeviceOvsInterface * self = data->self; + const NMPlatformSignalChangeType change_type = change_type_i; + + if (change_type == NM_PLATFORM_SIGNAL_REMOVED + && nm_streq0(info->name, nm_device_get_iface(NM_DEVICE(self)))) { + _LOGT(LOGD_DEVICE, "deactivate: link removed, proceeding"); + nm_device_update_from_platform_link(NM_DEVICE(self), NULL); + deactivate_invoke_cb(data, NULL); + return; + } +} + +static gboolean +deactivate_link_timeout(gpointer user_data) +{ + DeactivateData * data = user_data; + NMDeviceOvsInterface *self = data->self; + + _LOGT(LOGD_DEVICE, "deactivate: timeout waiting link removal"); + deactivate_invoke_cb(data, NULL); + return G_SOURCE_REMOVE; +} + +static void +deactivate_cancelled_cb(GCancellable *cancellable, gpointer user_data) +{ + gs_free_error GError *error = NULL; + + nm_utils_error_set_cancelled(&error, FALSE, NULL); + deactivate_invoke_cb((DeactivateData *) user_data, error); +} + +static void +deactivate_cb_on_idle(gpointer user_data, GCancellable *cancellable) +{ + DeactivateData *data = user_data; + gs_free_error GError *cancelled_error = NULL; + + g_cancellable_set_error_if_cancelled(data->cancellable, &cancelled_error); + deactivate_invoke_cb(data, cancelled_error); +} + +static void +deactivate_async(NMDevice * device, + GCancellable * cancellable, + NMDeviceDeactivateCallback callback, + gpointer callback_user_data) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(device); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + DeactivateData * data; + + _LOGT(LOGD_CORE, "deactivate: start async"); + + /* We want to ensure that the kernel link for this device is + * removed upon disconnection so that it will not interfere with + * later activations of the same device. Unfortunately there is + * no synchronization mechanism with vswitchd, we only update + * ovsdb and wait that changes are picked up. + */ + + data = g_slice_new(DeactivateData); + *data = (DeactivateData){ + .self = g_object_ref(self), + .cancellable = g_object_ref(cancellable), + .callback = callback, + .callback_user_data = callback_user_data, + }; + + if (!priv->waiting_for_interface + && !nm_platform_link_get_by_ifname(nm_device_get_platform(device), + nm_device_get_iface(device))) { + _LOGT(LOGD_CORE, "deactivate: link not present, proceeding"); + nm_device_update_from_platform_link(NM_DEVICE(self), NULL); + nm_utils_invoke_on_idle(cancellable, deactivate_cb_on_idle, data); + return; + } + + if (priv->waiting_for_interface) { + /* At this point we have issued an INSERT and a DELETE + * command for the interface to ovsdb. We don't know if + * vswitchd will see the two updates or only one. We + * must add a timeout to avoid waiting forever in case + * the link doesn't appear. + */ + data->link_timeout_id = g_timeout_add(6000, deactivate_link_timeout, data); + _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear in 6 seconds"); + } else + _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear"); + + data->cancelled_id = + g_cancellable_connect(cancellable, G_CALLBACK(deactivate_cancelled_cb), data, NULL); + data->link_changed_id = g_signal_connect(nm_device_get_platform(device), + NM_PLATFORM_SIGNAL_LINK_CHANGED, + G_CALLBACK(deactivate_link_changed_cb), + data); +} + +static gboolean +can_update_from_platform_link(NMDevice *device, const NMPlatformLink *plink) +{ + /* If the device is deactivating, we already sent the + * deletion command to ovsdb and we don't want to deal + * with any new link appearing from the previous + * activation. + */ + return !plink || nm_device_get_state(device) != NM_DEVICE_STATE_DEACTIVATING; +} + +/*****************************************************************************/ + +static void +ovsdb_ready(NMOvsdb *ovsdb, NMDeviceOvsInterface *self) +{ + NMDevice *device = NM_DEVICE(self); + + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_NONE); + nm_device_recheck_available_connections(device); + nm_device_emit_recheck_auto_activate(device); +} + +static void +nm_device_ovs_interface_init(NMDeviceOvsInterface *self) +{ + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + + priv->ovsdb = g_object_ref(nm_ovsdb_get()); + + if (!nm_ovsdb_is_ready(priv->ovsdb)) + g_signal_connect(priv->ovsdb, NM_OVSDB_READY, G_CALLBACK(ovsdb_ready), self); +} + +static void +dispose(GObject *object) +{ + NMDeviceOvsInterface * self = NM_DEVICE_OVS_INTERFACE(object); + NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self); + + if (priv->ovsdb) { + g_signal_handlers_disconnect_by_func(priv->ovsdb, G_CALLBACK(ovsdb_ready), self); + g_clear_object(&priv->ovsdb); + } + + G_OBJECT_CLASS(nm_device_ovs_interface_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_ovs_interface = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_OVS_INTERFACE, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ovs_interface_class_init(NMDeviceOvsInterfaceClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->dispose = dispose; + + dbus_object_class->interface_infos = + NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_interface); + + device_class->connection_type_supported = NM_SETTING_OVS_INTERFACE_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_OVS_INTERFACE_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_OPENVSWITCH); + + device_class->can_update_from_platform_link = can_update_from_platform_link; + device_class->deactivate = deactivate; + device_class->deactivate_async = deactivate_async; + device_class->get_type_description = get_type_description; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->is_available = is_available; + device_class->check_connection_compatible = check_connection_compatible; + device_class->link_changed = link_changed; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->can_unmanaged_external_down = can_unmanaged_external_down; + device_class->set_platform_mtu = set_platform_mtu; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + device_class->can_reapply_change_ovs_external_ids = TRUE; + device_class->reapply_connection = nm_device_ovs_reapply_connection; +} diff --git a/src/core/devices/ovs/nm-device-ovs-interface.h b/src/core/devices/ovs/nm-device-ovs-interface.h new file mode 100644 index 00000000..03b0e9a4 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-interface.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__ +#define __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__ + +#define NM_TYPE_DEVICE_OVS_INTERFACE (nm_device_ovs_interface_get_type()) +#define NM_DEVICE_OVS_INTERFACE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterface)) +#define NM_DEVICE_OVS_INTERFACE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterfaceClass)) +#define NM_IS_DEVICE_OVS_INTERFACE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_INTERFACE)) +#define NM_IS_DEVICE_OVS_INTERFACE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_INTERFACE)) +#define NM_DEVICE_OVS_INTERFACE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterfaceClass)) + +typedef struct _NMDeviceOvsInterface NMDeviceOvsInterface; +typedef struct _NMDeviceOvsInterfaceClass NMDeviceOvsInterfaceClass; + +GType nm_device_ovs_interface_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__ */ diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c new file mode 100644 index 00000000..2ecb95e8 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-port.c @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-ovs-port.h" + +#include "nm-device-ovs-interface.h" +#include "nm-device-ovs-bridge.h" +#include "nm-ovsdb.h" + +#include "devices/nm-device-private.h" +#include "nm-active-connection.h" +#include "nm-setting-connection.h" +#include "nm-setting-ovs-port.h" +#include "nm-setting-ovs-port.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceOvsPort +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +struct _NMDeviceOvsPort { + NMDevice parent; +}; + +struct _NMDeviceOvsPortClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceOvsPort, nm_device_ovs_port, NM_TYPE_DEVICE) + +/*****************************************************************************/ + +static const char * +get_type_description(NMDevice *device) +{ + return "ovs-port"; +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + /* The port will be added to ovsdb when an interface is enslaved, + * because there's no such thing like an empty port. */ + + return TRUE; +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_SOFTWARE; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + return NM_ACT_STAGE_RETURN_IP_FAIL; +} + +static void +add_iface_cb(GError *error, gpointer user_data) +{ + NMDevice *slave = user_data; + + if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) { + nm_log_warn(LOGD_DEVICE, + "device %s could not be added to a ovs port: %s", + nm_device_get_iface(slave), + error->message); + nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED); + } + + g_object_unref(slave); +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + NMDeviceOvsPort * self = NM_DEVICE_OVS_PORT(device); + NMActiveConnection *ac_port = NULL; + NMActiveConnection *ac_bridge = NULL; + NMDevice * bridge_device; + + if (!configure) + return TRUE; + + ac_port = NM_ACTIVE_CONNECTION(nm_device_get_act_request(device)); + ac_bridge = nm_active_connection_get_master(ac_port); + if (!ac_bridge) { + _LOGW(LOGD_DEVICE, + "can't enslave %s: bridge active-connection not found", + nm_device_get_iface(slave)); + return FALSE; + } + + bridge_device = nm_active_connection_get_device(ac_bridge); + if (!bridge_device) { + _LOGW(LOGD_DEVICE, "can't enslave %s: bridge device not found", nm_device_get_iface(slave)); + return FALSE; + } + + nm_ovsdb_add_interface(nm_ovsdb_get(), + nm_active_connection_get_applied_connection(ac_bridge), + nm_device_get_applied_connection(device), + nm_device_get_applied_connection(slave), + bridge_device, + slave, + add_iface_cb, + g_object_ref(slave)); + + return TRUE; +} + +static void +del_iface_cb(GError *error, gpointer user_data) +{ + NMDevice *slave = user_data; + + if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) { + nm_log_warn(LOGD_DEVICE, + "device %s could not be removed from a ovs port: %s", + nm_device_get_iface(slave), + error->message); + nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED); + } + + g_object_unref(slave); +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{ + NMDeviceOvsPort *self = NM_DEVICE_OVS_PORT(device); + + if (configure) { + _LOGI(LOGD_DEVICE, "releasing ovs interface %s", nm_device_get_ip_iface(slave)); + nm_ovsdb_del_interface(nm_ovsdb_get(), + nm_device_get_iface(slave), + del_iface_cb, + g_object_ref(slave)); + /* Open VSwitch is going to delete this one. We must ignore what happens + * next with the interface. */ + if (NM_IS_DEVICE_OVS_INTERFACE(slave)) + nm_device_update_from_platform_link(slave, NULL); + } else + _LOGI(LOGD_DEVICE, "ovs interface %s was released", nm_device_get_ip_iface(slave)); +} + +/*****************************************************************************/ + +static void +nm_device_ovs_port_init(NMDeviceOvsPort *self) +{} + +static const NMDBusInterfaceInfoExtended interface_info_device_ovs_port = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_OVS_PORT, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_ovs_port_class_init(NMDeviceOvsPortClass *klass) +{ + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_port); + + device_class->connection_type_supported = NM_SETTING_OVS_PORT_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_OVS_PORT_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(); + + device_class->is_master = TRUE; + device_class->get_type_description = get_type_description; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + device_class->can_reapply_change_ovs_external_ids = TRUE; + device_class->reapply_connection = nm_device_ovs_reapply_connection; +} diff --git a/src/core/devices/ovs/nm-device-ovs-port.h b/src/core/devices/ovs/nm-device-ovs-port.h new file mode 100644 index 00000000..f4516c01 --- /dev/null +++ b/src/core/devices/ovs/nm-device-ovs-port.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_OVS_PORT_H__ +#define __NETWORKMANAGER_DEVICE_OVS_PORT_H__ + +#define NM_TYPE_DEVICE_OVS_PORT (nm_device_ovs_port_get_type()) +#define NM_DEVICE_OVS_PORT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPort)) +#define NM_DEVICE_OVS_PORT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPortClass)) +#define NM_IS_DEVICE_OVS_PORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_PORT)) +#define NM_IS_DEVICE_OVS_PORT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_PORT)) +#define NM_DEVICE_OVS_PORT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPortClass)) + +typedef struct _NMDeviceOvsPort NMDeviceOvsPort; +typedef struct _NMDeviceOvsPortClass NMDeviceOvsPortClass; + +GType nm_device_ovs_port_get_type(void); + +#endif /* __NETWORKMANAGER_DEVICE_OVS_PORT_H__ */ diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c new file mode 100644 index 00000000..e7af38d8 --- /dev/null +++ b/src/core/devices/ovs/nm-ovs-factory.c @@ -0,0 +1,324 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-manager.h" +#include "nm-ovsdb.h" +#include "nm-device-ovs-interface.h" +#include "nm-device-ovs-port.h" +#include "nm-device-ovs-bridge.h" +#include "platform/nm-platform.h" +#include "nm-core-internal.h" +#include "settings/nm-settings.h" +#include "devices/nm-device-factory.h" +#include "devices/nm-device-private.h" + +/*****************************************************************************/ + +typedef struct { + NMDeviceFactory parent; +} NMOvsFactory; + +typedef struct { + NMDeviceFactoryClass parent; +} NMOvsFactoryClass; + +#define NM_TYPE_OVS_FACTORY (nm_ovs_factory_get_type()) +#define NM_OVS_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVS_FACTORY, NMOvsFactory)) +#define NM_OVS_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVS_FACTORY, NMOvsFactoryClass)) +#define NM_IS_OVS_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVS_FACTORY)) +#define NM_IS_OVS_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_OVS_FACTORY)) +#define NM_OVS_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_OVS_FACTORY, NMOvsFactoryClass)) + +static GType nm_ovs_factory_get_type(void); +G_DEFINE_TYPE(NMOvsFactory, nm_ovs_factory, NM_TYPE_DEVICE_FACTORY) + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_DEVICE +#define _NMLOG(level, ifname, con_uuid, ...) \ + G_STMT_START \ + { \ + nm_log((level), \ + _NMLOG_DOMAIN, \ + (ifname), \ + (con_uuid), \ + "ovs: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES( + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_OPENVSWITCH) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_OVS_BRIDGE_SETTING_NAME, + NM_SETTING_OVS_INTERFACE_SETTING_NAME, + NM_SETTING_OVS_PORT_SETTING_NAME)) + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + nm_manager_set_capability(NM_MANAGER_GET, NM_CAPABILITY_OVS); + return g_object_new(NM_TYPE_OVS_FACTORY, NULL); +} + +static NMDevice * +new_device_from_type(const char *name, NMDeviceType device_type) +{ + GType type; + const char *type_desc; + NMLinkType link_type = NM_LINK_TYPE_NONE; + + if (nm_manager_get_device(NM_MANAGER_GET, name, device_type)) + return NULL; + + if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE) { + type = NM_TYPE_DEVICE_OVS_INTERFACE; + type_desc = "Open vSwitch Interface"; + link_type = NM_LINK_TYPE_OPENVSWITCH; + } else if (device_type == NM_DEVICE_TYPE_OVS_PORT) { + type = NM_TYPE_DEVICE_OVS_PORT; + type_desc = "Open vSwitch Port"; + } else if (device_type == NM_DEVICE_TYPE_OVS_BRIDGE) { + type = NM_TYPE_DEVICE_OVS_BRIDGE; + type_desc = "Open vSwitch Bridge"; + } else { + return NULL; + } + + return g_object_new(type, + NM_DEVICE_IFACE, + name, + NM_DEVICE_DRIVER, + "openvswitch", + NM_DEVICE_DEVICE_TYPE, + device_type, + NM_DEVICE_TYPE_DESC, + type_desc, + NM_DEVICE_LINK_TYPE, + link_type, + NULL); +} + +static void +ovsdb_device_added(NMOvsdb * ovsdb, + const char * name, + guint device_type_i, + const char * subtype, + NMDeviceFactory *self) +{ + const NMDeviceType device_type = device_type_i; + NMDevice * device; + + if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE + && !NM_IN_STRSET(subtype, "internal", "patch")) { + /* system interfaces refer to kernel devices and + * don't need to be created by this factory. Ignore + * anything that is not an internal or patch + * interface. */ + return; + } + + device = new_device_from_type(name, device_type); + if (!device) + return; + + g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device); + g_object_unref(device); +} + +static void +ovsdb_device_removed(NMOvsdb * ovsdb, + const char * name, + guint device_type_i, + const char * subtype, + NMDeviceFactory *self) +{ + const NMDeviceType device_type = device_type_i; + NMDevice * device = NULL; + NMDeviceState device_state; + gboolean is_system_interface = FALSE; + + if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE + && !NM_IN_STRSET(subtype, "internal", "patch", "system")) + return; + + if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE && nm_streq0(subtype, "system")) { + NMDevice * d; + const CList * list; + NMSettingOvsInterface *s_ovs_int; + + /* The device associated to an OVS system interface can be of + * any kind. Find an interface with the same name and which has + * the OVS-interface setting. */ + is_system_interface = TRUE; + nm_manager_for_each_device (NM_MANAGER_GET, d, list) { + if (!nm_streq0(nm_device_get_iface(d), name)) + continue; + s_ovs_int = nm_device_get_applied_setting(d, NM_TYPE_SETTING_OVS_INTERFACE); + if (!s_ovs_int) + continue; + if (!nm_streq0(nm_setting_ovs_interface_get_interface_type(s_ovs_int), "system")) + continue; + /* Failing the system interface device is almost always the right + * thing to do when the ovsdb entry is removed. However, to avoid + * that a late device-removed signal tears down a different, + * newly-activated connection, let's also check that we have a master. + * Or in alternative, that the device is assumed/external: in such + * case it's always fine to fail the device. + */ + if (!nm_device_get_master(d) && !nm_device_sys_iface_state_is_external_or_assume(d)) + continue; + + device = d; + } + } else { + device = nm_manager_get_device(NM_MANAGER_GET, name, device_type); + } + + if (!device) + return; + + device_state = nm_device_get_state(device); + + if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE && nm_device_get_act_request(device) + && device_state < NM_DEVICE_STATE_DEACTIVATING) { + nm_device_state_changed(device, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_REMOVED); + return; + } + + /* OVS system interfaces still exist even without the ovsdb entry */ + if (!is_system_interface && device_state == NM_DEVICE_STATE_UNMANAGED) { + nm_device_unrealize(device, TRUE, NULL); + } +} + +static void +ovsdb_interface_failed(NMOvsdb * ovsdb, + const char * name, + const char * connection_uuid, + const char * error, + NMDeviceFactory *self) +{ + NMDevice * device = NULL; + NMSettingsConnection * connection = NULL; + NMConnection * c; + const char * type; + NMSettingOvsInterface *s_ovs_int; + gboolean is_patch = FALSE; + gboolean ignore; + + device = nm_manager_get_device(NM_MANAGER_GET, name, NM_DEVICE_TYPE_OVS_INTERFACE); + if (device && connection_uuid) { + connection = + nm_settings_get_connection_by_uuid(nm_device_get_settings(device), connection_uuid); + } + + /* The patch interface which gets created first is expected to + * fail because the second patch doesn't exist yet. Ignore all + * failures of patch interfaces. */ + if (connection && (c = nm_settings_connection_get_connection(connection)) + && (type = nm_connection_get_connection_type(c)) + && nm_streq0(type, NM_SETTING_OVS_INTERFACE_SETTING_NAME) + && (s_ovs_int = nm_connection_get_setting_ovs_interface(c)) + && nm_streq0(nm_setting_ovs_interface_get_interface_type(s_ovs_int), "patch")) + is_patch = TRUE; + + ignore = !device || is_patch; + + _NMLOG(ignore ? LOGL_DEBUG : LOGL_INFO, + name, + connection_uuid, + "ovs interface \"%s\" (%s) failed%s: %s", + name, + connection_uuid, + ignore ? " (ignored)" : "", + error); + + if (ignore) + return; + + if (connection) { + nm_settings_connection_autoconnect_blocked_reason_set( + connection, + NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED, + TRUE); + } + + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED); +} + +static void +start(NMDeviceFactory *self) +{ + NMOvsdb *ovsdb; + + ovsdb = nm_ovsdb_get(); + + g_signal_connect_object(ovsdb, + NM_OVSDB_DEVICE_ADDED, + G_CALLBACK(ovsdb_device_added), + self, + (GConnectFlags) 0); + g_signal_connect_object(ovsdb, + NM_OVSDB_DEVICE_REMOVED, + G_CALLBACK(ovsdb_device_removed), + self, + (GConnectFlags) 0); + g_signal_connect_object(ovsdb, + NM_OVSDB_INTERFACE_FAILED, + G_CALLBACK(ovsdb_interface_failed), + self, + (GConnectFlags) 0); +} + +static NMDevice * +create_device(NMDeviceFactory * self, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + NMDeviceType device_type = NM_DEVICE_TYPE_UNKNOWN; + const char * connection_type = NULL; + + if (g_strcmp0(iface, "ovs-system") == 0) { + *out_ignore = TRUE; + return NULL; + } + + if (connection) + connection_type = nm_connection_get_connection_type(connection); + + if (plink) + device_type = NM_DEVICE_TYPE_OVS_INTERFACE; + else if (g_strcmp0(connection_type, NM_SETTING_OVS_INTERFACE_SETTING_NAME) == 0) + device_type = NM_DEVICE_TYPE_OVS_INTERFACE; + else if (g_strcmp0(connection_type, NM_SETTING_OVS_PORT_SETTING_NAME) == 0) + device_type = NM_DEVICE_TYPE_OVS_PORT; + else if (g_strcmp0(connection_type, NM_SETTING_OVS_BRIDGE_SETTING_NAME) == 0) + device_type = NM_DEVICE_TYPE_OVS_BRIDGE; + + return new_device_from_type(iface, device_type); +} + +static void +nm_ovs_factory_init(NMOvsFactory *self) +{} + +static void +nm_ovs_factory_class_init(NMOvsFactoryClass *klass) +{ + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + factory_class->get_supported_types = get_supported_types; + factory_class->start = start; + factory_class->create_device = create_device; +} diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c new file mode 100644 index 00000000..da3a7989 --- /dev/null +++ b/src/core/devices/ovs/nm-ovsdb.c @@ -0,0 +1,2658 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-ovsdb.h" + +#include <gmodule.h> +#include <gio/gunixsocketaddress.h> + +#include "nm-glib-aux/nm-jansson.h" +#include "nm-glib-aux/nm-str-buf.h" +#include "nm-core-utils.h" +#include "nm-core-internal.h" +#include "devices/nm-device.h" +#include "nm-manager.h" +#include "nm-setting-ovs-external-ids.h" + +/*****************************************************************************/ + +#define OVSDB_MAX_FAILURES 3 + +/*****************************************************************************/ + +#if JANSSON_VERSION_HEX < 0x020400 + #warning "requires at least libjansson 2.4" +#endif + +typedef struct { + char * port_uuid; + char * name; + char * connection_uuid; + GPtrArray *interfaces; /* interface uuids */ + GArray * external_ids; +} OpenvswitchPort; + +typedef struct { + char * bridge_uuid; + char * name; + char * connection_uuid; + GPtrArray *ports; /* port uuids */ + GArray * external_ids; +} OpenvswitchBridge; + +typedef struct { + char * interface_uuid; + char * name; + char * type; + char * connection_uuid; + GArray *external_ids; +} OpenvswitchInterface; + +/*****************************************************************************/ + +typedef void (*OvsdbMethodCallback)(NMOvsdb *self, + json_t * response, + GError * error, + gpointer user_data); + +typedef enum { + OVSDB_MONITOR, + OVSDB_ADD_INTERFACE, + OVSDB_DEL_INTERFACE, + OVSDB_SET_INTERFACE_MTU, + OVSDB_SET_EXTERNAL_IDS, +} OvsdbCommand; + +#define CALL_ID_UNSPEC G_MAXUINT64 + +typedef union { + struct { + } monitor; + struct { + NMConnection *bridge; + NMConnection *port; + NMConnection *interface; + NMDevice * bridge_device; + NMDevice * interface_device; + } add_interface; + struct { + char *ifname; + } del_interface; + struct { + char * ifname; + guint32 mtu; + } set_interface_mtu; + struct { + NMDeviceType device_type; + char * ifname; + char * connection_uuid; + GHashTable * exid_old; + GHashTable * exid_new; + } set_external_ids; +} OvsdbMethodPayload; + +typedef struct { + NMOvsdb * self; + CList calls_lst; + guint64 call_id; + OvsdbCommand command; + OvsdbMethodCallback callback; + gpointer user_data; + OvsdbMethodPayload payload; +} OvsdbMethodCall; + +/*****************************************************************************/ + +enum { + DEVICE_ADDED, + DEVICE_REMOVED, + INTERFACE_FAILED, + READY, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct { + GSocketClient * client; + GSocketConnection *conn; + GCancellable * cancellable; + char buf[4096]; /* Input buffer */ + size_t bufp; /* Last decoded byte in the input buffer. */ + GString * input; /* JSON stream waiting for decoding. */ + GString * output; /* JSON stream to be sent. */ + guint64 call_id_counter; + + CList calls_lst_head; + + GHashTable *interfaces; /* interface uuid => OpenvswitchInterface */ + GHashTable *ports; /* port uuid => OpenvswitchPort */ + GHashTable *bridges; /* bridge uuid => OpenvswitchBridge */ + char * db_uuid; + guint num_failures; + guint num_pending_deletions; + bool ready : 1; +} NMOvsdbPrivate; + +struct _NMOvsdb { + GObject parent; + NMOvsdbPrivate _priv; +}; + +struct _NMOvsdbClass { + GObjectClass parent; +}; + +G_DEFINE_TYPE(NMOvsdb, nm_ovsdb, G_TYPE_OBJECT) + +#define NM_OVSDB_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMOvsdb, NM_IS_OVSDB) + +NM_DEFINE_SINGLETON_GETTER(NMOvsdb, nm_ovsdb_get, NM_TYPE_OVSDB); + +/*****************************************************************************/ + +static void ovsdb_try_connect(NMOvsdb *self); +static void ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing); +static void ovsdb_read(NMOvsdb *self); +static void ovsdb_write(NMOvsdb *self); +static void ovsdb_next_command(NMOvsdb *self); + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_DEVICE +#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "ovsdb", __VA_ARGS__) + +#define _NMLOG_call(level, call, ...) \ + _NMLOG((level), \ + "call[" NM_HASH_OBFUSCATE_PTR_FMT "]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + NM_HASH_OBFUSCATE_PTR((call)) _NM_UTILS_MACRO_REST(__VA_ARGS__)) + +#define _LOGT_call(call, ...) _NMLOG_call(LOGL_TRACE, (call), __VA_ARGS__) + +/*****************************************************************************/ + +#define OVSDB_METHOD_PAYLOAD_MONITOR() \ + (&((const OvsdbMethodPayload){ \ + .monitor = {}, \ + })) + +#define OVSDB_METHOD_PAYLOAD_ADD_INTERFACE(xbridge, \ + xport, \ + xinterface, \ + xbridge_device, \ + xinterface_device) \ + (&((const OvsdbMethodPayload){ \ + .add_interface = \ + { \ + .bridge = (xbridge), \ + .port = (xport), \ + .interface = (xinterface), \ + .bridge_device = (xbridge_device), \ + .interface_device = (xinterface_device), \ + }, \ + })) + +#define OVSDB_METHOD_PAYLOAD_DEL_INTERFACE(xifname) \ + (&((const OvsdbMethodPayload){ \ + .del_interface = \ + { \ + .ifname = (char *) NM_CONSTCAST(char, (xifname)), \ + }, \ + })) + +#define OVSDB_METHOD_PAYLOAD_SET_INTERFACE_MTU(xifname, xmtu) \ + (&((const OvsdbMethodPayload){ \ + .set_interface_mtu = \ + { \ + .ifname = (char *) NM_CONSTCAST(char, (xifname)), \ + .mtu = (xmtu), \ + }, \ + })) + +#define OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(xdevice_type, \ + xifname, \ + xconnection_uuid, \ + xexid_old, \ + xexid_new) \ + (&((const OvsdbMethodPayload){ \ + .set_external_ids = \ + { \ + .device_type = xdevice_type, \ + .ifname = (char *) NM_CONSTCAST(char, (xifname)), \ + .connection_uuid = (char *) NM_CONSTCAST(char, (xconnection_uuid)), \ + .exid_old = (xexid_old), \ + .exid_new = (xexid_new), \ + }, \ + })) + +/*****************************************************************************/ + +static NM_UTILS_LOOKUP_STR_DEFINE(_device_type_to_table, + NMDeviceType, + NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_BRIDGE, "Bridge"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_PORT, "Port"), + NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_INTERFACE, + "Interface"), + NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(), ); + +/*****************************************************************************/ + +static void +_call_complete(OvsdbMethodCall *call, json_t *response, GError *error) +{ + if (response) { + gs_free char *str = NULL; + + str = json_dumps(response, 0); + if (error) + _LOGT_call(call, "completed: %s ; error: %s", str, error->message); + else + _LOGT_call(call, "completed: %s", str); + } else { + nm_assert(error); + _LOGT_call(call, "completed: error: %s", error->message); + } + + c_list_unlink_stale(&call->calls_lst); + + if (call->callback) + call->callback(call->self, response, error, call->user_data); + + switch (call->command) { + case OVSDB_MONITOR: + break; + case OVSDB_ADD_INTERFACE: + g_clear_object(&call->payload.add_interface.bridge); + g_clear_object(&call->payload.add_interface.port); + g_clear_object(&call->payload.add_interface.interface); + g_clear_object(&call->payload.add_interface.bridge_device); + g_clear_object(&call->payload.add_interface.interface_device); + break; + case OVSDB_DEL_INTERFACE: + nm_clear_g_free(&call->payload.del_interface.ifname); + break; + case OVSDB_SET_INTERFACE_MTU: + nm_clear_g_free(&call->payload.set_interface_mtu.ifname); + break; + case OVSDB_SET_EXTERNAL_IDS: + nm_clear_g_free(&call->payload.set_external_ids.ifname); + nm_clear_g_free(&call->payload.set_external_ids.connection_uuid); + nm_clear_pointer(&call->payload.set_external_ids.exid_old, g_hash_table_destroy); + nm_clear_pointer(&call->payload.set_external_ids.exid_new, g_hash_table_destroy); + break; + } + + nm_g_slice_free(call); +} + +/*****************************************************************************/ + +static void +_free_bridge(OpenvswitchBridge *ovs_bridge) +{ + g_free(ovs_bridge->bridge_uuid); + g_free(ovs_bridge->name); + g_free(ovs_bridge->connection_uuid); + g_ptr_array_free(ovs_bridge->ports, TRUE); + nm_g_array_unref(ovs_bridge->external_ids); + nm_g_slice_free(ovs_bridge); +} + +static void +_free_port(OpenvswitchPort *ovs_port) +{ + g_free(ovs_port->port_uuid); + g_free(ovs_port->name); + g_free(ovs_port->connection_uuid); + g_ptr_array_free(ovs_port->interfaces, TRUE); + nm_g_array_unref(ovs_port->external_ids); + nm_g_slice_free(ovs_port); +} + +static void +_free_interface(OpenvswitchInterface *ovs_interface) +{ + g_free(ovs_interface->interface_uuid); + g_free(ovs_interface->name); + g_free(ovs_interface->connection_uuid); + g_free(ovs_interface->type); + nm_g_array_unref(ovs_interface->external_ids); + nm_g_slice_free(ovs_interface); +} + +/*****************************************************************************/ + +static void +_signal_emit_device_added(NMOvsdb * self, + const char * name, + NMDeviceType device_type, + const char * device_subtype) +{ + g_signal_emit(self, signals[DEVICE_ADDED], 0, name, (guint) device_type, device_subtype); +} + +static void +_signal_emit_device_removed(NMOvsdb * self, + const char * name, + NMDeviceType device_type, + const char * device_subtype) +{ + g_signal_emit(self, signals[DEVICE_REMOVED], 0, name, (guint) device_type, device_subtype); +} + +static void +_signal_emit_interface_failed(NMOvsdb * self, + const char *name, + const char *connection_uuid, + const char *error) +{ + g_signal_emit(self, signals[INTERFACE_FAILED], 0, name, connection_uuid, error); +} + +/*****************************************************************************/ + +/** + * ovsdb_call_method: + * + * Queues the ovsdb command. Eventually fires the command right away if + * there's no command pending completion. + */ +static void +ovsdb_call_method(NMOvsdb * self, + OvsdbMethodCallback callback, + gpointer user_data, + gboolean add_first, + OvsdbCommand command, + const OvsdbMethodPayload *payload) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + OvsdbMethodCall *call; + + /* Ensure we're not unsynchronized before we queue the method call. */ + ovsdb_try_connect(self); + + call = g_slice_new(OvsdbMethodCall); + *call = (OvsdbMethodCall){ + .self = self, + .call_id = CALL_ID_UNSPEC, + .command = command, + .callback = callback, + .user_data = user_data, + }; + + if (add_first) + c_list_link_front(&priv->calls_lst_head, &call->calls_lst); + else + c_list_link_tail(&priv->calls_lst_head, &call->calls_lst); + + /* Migrate the arguments from @payload to @call->payload. Technically, + * this is not a plain copy, because + * - call->payload is not initialized (thus no need to free the previous data). + * - payload does not own the data. It is merely initialized using the + * OVSDB_METHOD_PAYLOAD_*() macros. */ + switch (command) { + case OVSDB_MONITOR: + _LOGT_call(call, "new: monitor"); + break; + case OVSDB_ADD_INTERFACE: + /* FIXME(applied-connection-immutable): we should not modify the applied + * connection, consequently there is no need to clone the connections. */ + call->payload.add_interface.bridge = + nm_simple_connection_new_clone(payload->add_interface.bridge); + call->payload.add_interface.port = + nm_simple_connection_new_clone(payload->add_interface.port); + call->payload.add_interface.interface = + nm_simple_connection_new_clone(payload->add_interface.interface); + call->payload.add_interface.bridge_device = + g_object_ref(payload->add_interface.bridge_device); + call->payload.add_interface.interface_device = + g_object_ref(payload->add_interface.interface_device); + _LOGT_call(call, + "new: add-interface bridge=%s port=%s interface=%s", + nm_connection_get_interface_name(call->payload.add_interface.bridge), + nm_connection_get_interface_name(call->payload.add_interface.port), + nm_connection_get_interface_name(call->payload.add_interface.interface)); + break; + case OVSDB_DEL_INTERFACE: + call->payload.del_interface.ifname = g_strdup(payload->del_interface.ifname); + _LOGT_call(call, "new: del-interface interface=%s", call->payload.del_interface.ifname); + break; + case OVSDB_SET_INTERFACE_MTU: + call->payload.set_interface_mtu.ifname = g_strdup(payload->set_interface_mtu.ifname); + call->payload.set_interface_mtu.mtu = payload->set_interface_mtu.mtu; + _LOGT_call(call, + "new: set-interface-mtu interface=%s mtu=%u", + call->payload.set_interface_mtu.ifname, + call->payload.set_interface_mtu.mtu); + break; + case OVSDB_SET_EXTERNAL_IDS: + call->payload.set_external_ids.device_type = payload->set_external_ids.device_type; + call->payload.set_external_ids.ifname = g_strdup(payload->set_external_ids.ifname); + call->payload.set_external_ids.connection_uuid = + g_strdup(payload->set_external_ids.connection_uuid); + call->payload.set_external_ids.exid_old = + nm_g_hash_table_ref(payload->set_external_ids.exid_old); + call->payload.set_external_ids.exid_new = + nm_g_hash_table_ref(payload->set_external_ids.exid_new); + _LOGT_call(call, + "new: set-external-ids con-uuid=%s, interface=%s", + call->payload.set_external_ids.connection_uuid, + call->payload.set_external_ids.ifname); + break; + } + + ovsdb_next_command(self); +} + +/*****************************************************************************/ + +/* Create and process the JSON-RPC messages from ovsdb. */ + +/** + * _expect_ovs_bridges: + * + * Return a command that will fail the transaction if the actual set of + * bridges doesn't match @bridges. This is a way of detecting race conditions + * with other ovsdb clients that might be adding or removing bridges + * at the same time. + */ +static void +_expect_ovs_bridges(json_t *params, const char *db_uuid, json_t *bridges) +{ + json_array_append_new( + params, + json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, [s, s]]]}", + "op", + "wait", + "table", + "Open_vSwitch", + "timeout", + 0, + "columns", + "bridges", + "until", + "==", + "rows", + "bridges", + "set", + bridges, + "where", + "_uuid", + "==", + "uuid", + db_uuid)); +} + +/** + * _set_ovs_bridges: + * + * Return a command that will update the list of bridges in @db_uuid + * database to @new_bridges. + */ +static void +_set_ovs_bridges(json_t *params, const char *db_uuid, json_t *new_bridges) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, [s, s]]]}", + "op", + "update", + "table", + "Open_vSwitch", + "row", + "bridges", + "set", + new_bridges, + "where", + "_uuid", + "==", + "uuid", + db_uuid)); +} + +/** + * _expect_bridge_ports: + * + * Return a command that will fail the transaction if the actual set of + * ports in bridge @ifname doesn't match @ports. This is a way of detecting + * race conditions with other ovsdb clients that might be adding or removing + * bridge ports at the same time. + */ +static void +_expect_bridge_ports(json_t *params, const char *ifname, json_t *ports) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, s]]}", + "op", + "wait", + "table", + "Bridge", + "timeout", + 0, + "columns", + "ports", + "until", + "==", + "rows", + "ports", + "set", + ports, + "where", + "name", + "==", + ifname)); +} + +/** + * _set_bridge_ports: + * + * Return a command that will update the list of ports of bridge + * @ifname to @new_ports. + */ +static void +_set_bridge_ports(json_t *params, const char *ifname, json_t *new_ports) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, s]]}", + "op", + "update", + "table", + "Bridge", + "row", + "ports", + "set", + new_ports, + "where", + "name", + "==", + ifname)); +} + +static void +_set_bridge_mac(json_t *params, const char *ifname, const char *mac) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:{s:[s, [[s, s]]]}, s:[[s, s, s]]}", + "op", + "update", + "table", + "Bridge", + "row", + "other_config", + "map", + "hwaddr", + mac, + "where", + "name", + "==", + ifname)); +} + +/** + * _expect_port_interfaces: + * + * Return a command that will fail the transaction if the actual set of + * interfaces in port @ifname doesn't match @interfaces. This is a way of + * detecting race conditions with other ovsdb clients that might be adding + * or removing port interfaces at the same time. + */ +static void +_expect_port_interfaces(json_t *params, const char *ifname, json_t *interfaces) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, s]]}", + "op", + "wait", + "table", + "Port", + "timeout", + 0, + "columns", + "interfaces", + "until", + "==", + "rows", + "interfaces", + "set", + interfaces, + "where", + "name", + "==", + ifname)); +} + +/** + * _set_port_interfaces: + * + * Return a command that will update the list of interfaces of port @ifname + * to @new_interfaces. + */ +static void +_set_port_interfaces(json_t *params, const char *ifname, json_t *new_interfaces) +{ + json_array_append_new(params, + json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, s]]}", + "op", + "update", + "table", + "Port", + "row", + "interfaces", + "set", + new_interfaces, + "where", + "name", + "==", + ifname)); +} + +static json_t * +_j_create_external_ids_array_new(NMConnection *connection) +{ + json_t * array; + const char *const * external_ids = NULL; + guint n_external_ids = 0; + guint i; + const char * uuid; + NMSettingOvsExternalIDs *s_exid; + + nm_assert(NM_IS_CONNECTION(connection)); + + array = json_array(); + + uuid = nm_connection_get_uuid(connection); + nm_assert(uuid); + json_array_append_new(array, json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, uuid)); + + s_exid = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_EXTERNAL_IDS); + if (s_exid) + external_ids = nm_setting_ovs_external_ids_get_data_keys(s_exid, &n_external_ids); + for (i = 0; i < n_external_ids; i++) { + const char *k = external_ids[i]; + + json_array_append_new( + array, + json_pack("[s, s]", k, nm_setting_ovs_external_ids_get_data(s_exid, k))); + } + + return json_pack("[s, o]", "map", array); +} + +static json_t * +_j_create_external_ids_array_update(const char *connection_uuid, + GHashTable *exid_old, + GHashTable *exid_new) +{ + GHashTableIter iter; + json_t * mutations; + json_t * array; + const char * key; + const char * val; + + nm_assert(connection_uuid); + + mutations = json_array(); + + if (exid_old) { + array = NULL; + g_hash_table_iter_init(&iter, exid_old); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) { + if (nm_g_hash_table_contains(exid_new, key)) + continue; + if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX)) + continue; + + if (!array) + array = json_array(); + + json_array_append_new(array, json_string(key)); + } + if (array) { + json_array_append_new( + mutations, + json_pack("[s, s, [s, o]]", "external_ids", "delete", "set", array)); + } + } + + array = json_array(); + + json_array_append_new( + array, + json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, connection_uuid)); + + if (exid_new) { + g_hash_table_iter_init(&iter, exid_new); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val)) { + if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX)) + continue; + json_array_append_new(array, json_pack("[s, s]", key, val)); + } + } + + json_array_append_new(mutations, + json_pack("[s, s, [s, o]]", "external_ids", "insert", "map", array)); + return mutations; +} + +/** + * _insert_interface: + * + * Returns an commands that adds new interface from a given connection. + */ +static void +_insert_interface(json_t * params, + NMConnection *interface, + NMDevice * interface_device, + const char * cloned_mac) +{ + const char * type = NULL; + NMSettingOvsInterface *s_ovs_iface; + NMSettingOvsDpdk * s_ovs_dpdk; + NMSettingOvsPatch * s_ovs_patch; + json_t * options = json_array(); + json_t * row; + guint32 mtu = 0; + + s_ovs_iface = nm_connection_get_setting_ovs_interface(interface); + if (s_ovs_iface) + type = nm_setting_ovs_interface_get_interface_type(s_ovs_iface); + + if (nm_streq0(type, "internal")) { + NMSettingWired *s_wired; + + s_wired = _nm_connection_get_setting(interface, NM_TYPE_SETTING_WIRED); + if (s_wired) + mtu = nm_setting_wired_get_mtu(s_wired); + } + + json_array_append_new(options, json_string("map")); + + s_ovs_dpdk = + (NMSettingOvsDpdk *) nm_connection_get_setting(interface, NM_TYPE_SETTING_OVS_DPDK); + if (!s_ovs_dpdk) + s_ovs_patch = nm_connection_get_setting_ovs_patch(interface); + + if (s_ovs_dpdk) { + json_array_append_new( + options, + json_pack("[[s, s]]", "dpdk-devargs", nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk))); + } else if (s_ovs_patch) { + json_array_append_new( + options, + json_pack("[[s, s]]", "peer", nm_setting_ovs_patch_get_peer(s_ovs_patch))); + } else { + json_array_append_new(options, json_array()); + } + + row = json_pack("{s:s, s:s, s:o, s:o}", + "name", + nm_connection_get_interface_name(interface), + "type", + type ?: "", + "options", + options, + "external_ids", + _j_create_external_ids_array_new(interface)); + + if (cloned_mac) + json_object_set_new(row, "mac", json_string(cloned_mac)); + + if (mtu != 0) + json_object_set_new(row, "mtu_request", json_integer(mtu)); + + json_array_append_new(params, + json_pack("{s:s, s:s, s:o, s:s}", + "op", + "insert", + "table", + "Interface", + "row", + row, + "uuid-name", + "rowInterface")); +} + +/** + * _insert_port: + * + * Returns an commands that adds new port from a given connection. + */ +static void +_insert_port(json_t *params, NMConnection *port, json_t *new_interfaces) +{ + NMSettingOvsPort *s_ovs_port; + const char * vlan_mode = NULL; + guint tag = 0; + const char * lacp = NULL; + const char * bond_mode = NULL; + guint bond_updelay = 0; + guint bond_downdelay = 0; + json_t * row; + + s_ovs_port = nm_connection_get_setting_ovs_port(port); + + row = json_object(); + + if (s_ovs_port) { + vlan_mode = nm_setting_ovs_port_get_vlan_mode(s_ovs_port); + tag = nm_setting_ovs_port_get_tag(s_ovs_port); + lacp = nm_setting_ovs_port_get_lacp(s_ovs_port); + bond_mode = nm_setting_ovs_port_get_bond_mode(s_ovs_port); + bond_updelay = nm_setting_ovs_port_get_bond_updelay(s_ovs_port); + bond_downdelay = nm_setting_ovs_port_get_bond_downdelay(s_ovs_port); + } + + if (vlan_mode) + json_object_set_new(row, "vlan_mode", json_string(vlan_mode)); + if (tag) + json_object_set_new(row, "tag", json_integer(tag)); + if (lacp) + json_object_set_new(row, "lacp", json_string(lacp)); + if (bond_mode) + json_object_set_new(row, "bond_mode", json_string(bond_mode)); + if (bond_updelay) + json_object_set_new(row, "bond_updelay", json_integer(bond_updelay)); + if (bond_downdelay) + json_object_set_new(row, "bond_downdelay", json_integer(bond_downdelay)); + + json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(port))); + json_object_set_new(row, "interfaces", json_pack("[s, O]", "set", new_interfaces)); + json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(port)); + + /* Create a new one. */ + json_array_append_new(params, + json_pack("{s:s, s:s, s:o, s:s}", + "op", + "insert", + "table", + "Port", + "row", + row, + "uuid-name", + "rowPort")); +} + +/** + * _insert_bridge: + * + * Returns an commands that adds new bridge from a given connection. + */ +static void +_insert_bridge(json_t * params, + NMConnection *bridge, + NMDevice * bridge_device, + json_t * new_ports, + const char * cloned_mac) +{ + NMSettingOvsBridge *s_ovs_bridge; + const char * fail_mode = NULL; + gboolean mcast_snooping_enable = FALSE; + gboolean rstp_enable = FALSE; + gboolean stp_enable = FALSE; + const char * datapath_type = NULL; + json_t * row; + + s_ovs_bridge = nm_connection_get_setting_ovs_bridge(bridge); + + row = json_object(); + + if (s_ovs_bridge) { + fail_mode = nm_setting_ovs_bridge_get_fail_mode(s_ovs_bridge); + mcast_snooping_enable = nm_setting_ovs_bridge_get_mcast_snooping_enable(s_ovs_bridge); + rstp_enable = nm_setting_ovs_bridge_get_rstp_enable(s_ovs_bridge); + stp_enable = nm_setting_ovs_bridge_get_stp_enable(s_ovs_bridge); + datapath_type = nm_setting_ovs_bridge_get_datapath_type(s_ovs_bridge); + } + + if (fail_mode) + json_object_set_new(row, "fail_mode", json_string(fail_mode)); + if (mcast_snooping_enable) + json_object_set_new(row, "mcast_snooping_enable", json_boolean(mcast_snooping_enable)); + if (rstp_enable) + json_object_set_new(row, "rstp_enable", json_boolean(rstp_enable)); + if (stp_enable) + json_object_set_new(row, "stp_enable", json_boolean(stp_enable)); + if (datapath_type) + json_object_set_new(row, "datapath_type", json_string(datapath_type)); + + json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(bridge))); + json_object_set_new(row, "ports", json_pack("[s, O]", "set", new_ports)); + json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(bridge)); + + if (cloned_mac) { + json_object_set_new(row, + "other_config", + json_pack("[s, [[s, s]]]", "map", "hwaddr", cloned_mac)); + } + + /* Create a new one. */ + json_array_append_new(params, + json_pack("{s:s, s:s, s:o, s:s}", + "op", + "insert", + "table", + "Bridge", + "row", + row, + "uuid-name", + "rowBridge")); +} + +/** + * _inc_next_cfg: + * + * Returns an mutate command that bumps next_cfg upon successful completion + * of the transaction it is in. + */ +static json_t * +_inc_next_cfg(const char *db_uuid) +{ + return json_pack("{s:s, s:s, s:[[s, s, i]], s:[[s, s, [s, s]]]}", + "op", + "mutate", + "table", + "Open_vSwitch", + "mutations", + "next_cfg", + "+=", + 1, + "where", + "_uuid", + "==", + "uuid", + db_uuid); +} + +/** + * _add_interface: + * + * Adds an interface as specified by @interface connection, optionally creating + * a parent @port and @bridge if needed. + */ +static void +_add_interface(NMOvsdb * self, + json_t * params, + NMConnection *bridge, + NMConnection *port, + NMConnection *interface, + NMDevice * bridge_device, + NMDevice * interface_device) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + GHashTableIter iter; + const char * port_uuid; + const char * interface_uuid; + const char * bridge_name; + const char * port_name; + const char * interface_name; + OpenvswitchBridge * ovs_bridge = NULL; + OpenvswitchPort * ovs_port = NULL; + OpenvswitchInterface *ovs_interface = NULL; + nm_auto_decref_json json_t *bridges = NULL; + nm_auto_decref_json json_t *new_bridges = NULL; + nm_auto_decref_json json_t *ports = NULL; + nm_auto_decref_json json_t *new_ports = NULL; + nm_auto_decref_json json_t *interfaces = NULL; + nm_auto_decref_json json_t *new_interfaces = NULL; + gboolean has_interface = FALSE; + gboolean interface_is_local; + gs_free char * bridge_cloned_mac = NULL; + gs_free char * interface_cloned_mac = NULL; + GError * error = NULL; + int pi; + int ii; + + bridges = json_array(); + ports = json_array(); + interfaces = json_array(); + new_bridges = json_array(); + new_ports = json_array(); + new_interfaces = json_array(); + + bridge_name = nm_connection_get_interface_name(bridge); + port_name = nm_connection_get_interface_name(port); + interface_name = nm_connection_get_interface_name(interface); + interface_is_local = nm_streq0(bridge_name, interface_name); + + /* Determine cloned MAC addresses */ + if (!nm_device_hw_addr_get_cloned(bridge_device, + bridge, + FALSE, + &bridge_cloned_mac, + NULL, + &error)) { + _LOGW("Cannot determine cloned MAC for OVS %s '%s': %s", + "bridge", + bridge_name, + error->message); + g_clear_error(&error); + } + + if (!nm_device_hw_addr_get_cloned(interface_device, + interface, + FALSE, + &interface_cloned_mac, + NULL, + &error)) { + _LOGW("Cannot determine cloned MAC for OVS %s '%s': %s", + "interface", + interface_name, + error->message); + g_clear_error(&error); + } + + /* For local interfaces, ovs complains if it finds a + * MAC address in the Interface table because it only takes + * the MAC from the Bridge table. + * Set any cloned MAC present in a local interface connection + * into the Bridge table, unless conflicting with the bridge MAC. */ + if (interface_is_local && interface_cloned_mac) { + if (bridge_cloned_mac && !nm_streq(interface_cloned_mac, bridge_cloned_mac)) { + _LOGW("Cloned MAC '%s' of local ovs-interface '%s' conflicts with MAC '%s' of bridge " + "'%s'", + interface_cloned_mac, + interface_name, + bridge_cloned_mac, + bridge_name); + nm_clear_g_free(&interface_cloned_mac); + } else { + nm_clear_g_free(&bridge_cloned_mac); + bridge_cloned_mac = g_steal_pointer(&interface_cloned_mac); + _LOGT("'%s' is a local ovs-interface, the MAC will be set on ovs-bridge '%s'", + interface_name, + bridge_name); + } + } + + g_hash_table_iter_init(&iter, priv->bridges); + while (g_hash_table_iter_next(&iter, (gpointer) &ovs_bridge, NULL)) { + json_array_append_new(bridges, json_pack("[s, s]", "uuid", ovs_bridge->bridge_uuid)); + + if (!nm_streq0(ovs_bridge->name, bridge_name) + || !nm_streq0(ovs_bridge->connection_uuid, nm_connection_get_uuid(bridge))) + continue; + + for (pi = 0; pi < ovs_bridge->ports->len; pi++) { + port_uuid = g_ptr_array_index(ovs_bridge->ports, pi); + ovs_port = g_hash_table_lookup(priv->ports, &port_uuid); + + json_array_append_new(ports, json_pack("[s, s]", "uuid", port_uuid)); + + if (!ovs_port) { + /* This would be a violation of ovsdb's reference integrity (a bug). */ + _LOGW("Unknown port '%s' in bridge '%s'", port_uuid, ovs_bridge->bridge_uuid); + continue; + } + + if (!nm_streq(ovs_port->name, port_name) + || !nm_streq0(ovs_port->connection_uuid, nm_connection_get_uuid(port))) + continue; + + for (ii = 0; ii < ovs_port->interfaces->len; ii++) { + interface_uuid = g_ptr_array_index(ovs_port->interfaces, ii); + ovs_interface = g_hash_table_lookup(priv->interfaces, &interface_uuid); + + json_array_append_new(interfaces, json_pack("[s, s]", "uuid", interface_uuid)); + + if (!ovs_interface) { + /* This would be a violation of ovsdb's reference integrity (a bug). */ + _LOGW("Unknown interface '%s' in port '%s'", interface_uuid, port_uuid); + continue; + } + if (nm_streq(ovs_interface->name, interface_name) + && nm_streq0(ovs_interface->connection_uuid, nm_connection_get_uuid(interface))) + has_interface = TRUE; + } + + break; + } + + break; + } + + json_array_extend(new_bridges, bridges); + json_array_extend(new_ports, ports); + json_array_extend(new_interfaces, interfaces); + + if (json_array_size(interfaces) == 0) { + /* Need to create a port. */ + if (json_array_size(ports) == 0) { + /* Need to create a bridge. */ + _expect_ovs_bridges(params, priv->db_uuid, bridges); + json_array_append_new(new_bridges, json_pack("[s, s]", "named-uuid", "rowBridge")); + _set_ovs_bridges(params, priv->db_uuid, new_bridges); + _insert_bridge(params, bridge, bridge_device, new_ports, bridge_cloned_mac); + } else { + /* Bridge already exists. */ + g_return_if_fail(ovs_bridge); + _expect_bridge_ports(params, ovs_bridge->name, ports); + _set_bridge_ports(params, bridge_name, new_ports); + if (bridge_cloned_mac && interface_is_local) + _set_bridge_mac(params, bridge_name, bridge_cloned_mac); + } + + json_array_append_new(new_ports, json_pack("[s, s]", "named-uuid", "rowPort")); + _insert_port(params, port, new_interfaces); + } else { + /* Port already exists */ + g_return_if_fail(ovs_port); + _expect_port_interfaces(params, ovs_port->name, interfaces); + _set_port_interfaces(params, port_name, new_interfaces); + } + + if (!has_interface) { + _insert_interface(params, interface, interface_device, interface_cloned_mac); + json_array_append_new(new_interfaces, json_pack("[s, s]", "named-uuid", "rowInterface")); + } +} + +/** + * _delete_interface: + * + * Removes an interface of @ifname name, collecting empty ports and bridge + * if last item is removed from them. + */ +static void +_delete_interface(NMOvsdb *self, json_t *params, const char *ifname) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + GHashTableIter iter; + char * port_uuid; + char * interface_uuid; + OpenvswitchBridge * ovs_bridge; + OpenvswitchPort * ovs_port; + OpenvswitchInterface *ovs_interface; + nm_auto_decref_json json_t *bridges = NULL; + nm_auto_decref_json json_t *new_bridges = NULL; + gboolean bridges_changed; + gboolean ports_changed; + gboolean interfaces_changed; + int pi; + int ii; + + bridges = json_array(); + new_bridges = json_array(); + bridges_changed = FALSE; + + g_hash_table_iter_init(&iter, priv->bridges); + while (g_hash_table_iter_next(&iter, (gpointer) &ovs_bridge, NULL)) { + nm_auto_decref_json json_t *ports = NULL; + nm_auto_decref_json json_t *new_ports = NULL; + + ports = json_array(); + new_ports = json_array(); + ports_changed = FALSE; + + json_array_append_new(bridges, json_pack("[s,s]", "uuid", ovs_bridge->bridge_uuid)); + + for (pi = 0; pi < ovs_bridge->ports->len; pi++) { + nm_auto_decref_json json_t *interfaces = NULL; + nm_auto_decref_json json_t *new_interfaces = NULL; + + interfaces = json_array(); + new_interfaces = json_array(); + port_uuid = g_ptr_array_index(ovs_bridge->ports, pi); + ovs_port = g_hash_table_lookup(priv->ports, &port_uuid); + + json_array_append_new(ports, json_pack("[s,s]", "uuid", port_uuid)); + + interfaces_changed = FALSE; + + if (!ovs_port) { + /* This would be a violation of ovsdb's reference integrity (a bug). */ + _LOGW("Unknown port '%s' in bridge '%s'", port_uuid, ovs_bridge->bridge_uuid); + continue; + } + + for (ii = 0; ii < ovs_port->interfaces->len; ii++) { + interface_uuid = g_ptr_array_index(ovs_port->interfaces, ii); + ovs_interface = g_hash_table_lookup(priv->interfaces, &interface_uuid); + + json_array_append_new(interfaces, json_pack("[s,s]", "uuid", interface_uuid)); + + if (ovs_interface) { + if (nm_streq(ovs_interface->name, ifname)) { + /* skip the interface */ + interfaces_changed = TRUE; + continue; + } + } else { + /* This would be a violation of ovsdb's reference integrity (a bug). */ + _LOGW("Unknown interface '%s' in port '%s'", interface_uuid, port_uuid); + } + + json_array_append_new(new_interfaces, json_pack("[s,s]", "uuid", interface_uuid)); + } + + if (json_array_size(new_interfaces) == 0) { + ports_changed = TRUE; + } else { + if (interfaces_changed) { + _expect_port_interfaces(params, ovs_port->name, interfaces); + _set_port_interfaces(params, ovs_port->name, new_interfaces); + } + json_array_append_new(new_ports, json_pack("[s,s]", "uuid", port_uuid)); + } + } + + if (json_array_size(new_ports) == 0) { + bridges_changed = TRUE; + } else { + if (ports_changed) { + _expect_bridge_ports(params, ovs_bridge->name, ports); + _set_bridge_ports(params, ovs_bridge->name, new_ports); + } + json_array_append_new(new_bridges, json_pack("[s,s]", "uuid", ovs_bridge->bridge_uuid)); + } + } + + if (bridges_changed) { + _expect_ovs_bridges(params, priv->db_uuid, bridges); + _set_ovs_bridges(params, priv->db_uuid, new_bridges); + } +} + +/** + * ovsdb_next_command: + * + * Translates a higher level operation (add/remove bridge/port) to a RFC 7047 + * command serialized into JSON ands sends it over to the database. + + * Only called when no command is waiting for a response, since the serialized + * command might depend on result of a previous one (add and remove need to + * include an up to date bridge list in their transactions to rule out races). + */ +static void +ovsdb_next_command(NMOvsdb *self) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + OvsdbMethodCall * call; + char * cmd; + nm_auto_decref_json json_t *msg = NULL; + + if (!priv->conn) + return; + + if (c_list_is_empty(&priv->calls_lst_head)) + return; + + call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst); + if (call->call_id != CALL_ID_UNSPEC) + return; + + call->call_id = ++priv->call_id_counter; + + switch (call->command) { + case OVSDB_MONITOR: + msg = json_pack("{s:I, s:s, s:[s, n, {" + " s:[{s:[s, s, s]}]," + " s:[{s:[s, s, s]}]," + " s:[{s:[s, s, s, s]}]," + " s:[{s:[]}]" + "}]}", + "id", + (json_int_t) call->call_id, + "method", + "monitor", + "params", + "Open_vSwitch", + "Bridge", + "columns", + "name", + "ports", + "external_ids", + "Port", + "columns", + "name", + "interfaces", + "external_ids", + "Interface", + "columns", + "name", + "type", + "external_ids", + "error", + "Open_vSwitch", + "columns"); + break; + default: + { + json_t *params = NULL; + + params = json_array(); + json_array_append_new(params, json_string("Open_vSwitch")); + json_array_append_new(params, _inc_next_cfg(priv->db_uuid)); + + switch (call->command) { + case OVSDB_ADD_INTERFACE: + _add_interface(self, + params, + call->payload.add_interface.bridge, + call->payload.add_interface.port, + call->payload.add_interface.interface, + call->payload.add_interface.bridge_device, + call->payload.add_interface.interface_device); + break; + case OVSDB_DEL_INTERFACE: + _delete_interface(self, params, call->payload.del_interface.ifname); + break; + case OVSDB_SET_INTERFACE_MTU: + json_array_append_new(params, + json_pack("{s:s, s:s, s:{s: I}, s:[[s, s, s]]}", + "op", + "update", + "table", + "Interface", + "row", + "mtu_request", + (json_int_t) call->payload.set_interface_mtu.mtu, + "where", + "name", + "==", + call->payload.set_interface_mtu.ifname)); + break; + case OVSDB_SET_EXTERNAL_IDS: + json_array_append_new( + params, + json_pack("{s:s, s:s, s:o, s:[[s, s, s]]}", + "op", + "mutate", + "table", + _device_type_to_table(call->payload.set_external_ids.device_type), + "mutations", + _j_create_external_ids_array_update( + call->payload.set_external_ids.connection_uuid, + call->payload.set_external_ids.exid_old, + call->payload.set_external_ids.exid_new), + "where", + "name", + "==", + call->payload.set_external_ids.ifname)); + break; + + default: + nm_assert_not_reached(); + break; + } + + msg = json_pack("{s:I, s:s, s:o}", + "id", + (json_int_t) call->call_id, + "method", + "transact", + "params", + params); + break; + } + } + + g_return_if_fail(msg); + + cmd = json_dumps(msg, 0); + _LOGT_call(call, "send: call-id=%" G_GUINT64_FORMAT ", %s", call->call_id, cmd); + g_string_append(priv->output, cmd); + free(cmd); + + ovsdb_write(self); +} + +/** + * _uuids_to_array: + * + * This tidies up the somewhat non-straightforward way ovsdb represents an array + * of UUID elements. The single element is a tuple (called <atom> in RFC7047), + * + * [ "uuid", "aa095ffb-e1f1-0fc4-8038-82c1ea7e4797" ] + * + * while the list of multiple UUIDs are turned into a set of such tuples ("atoms"): + * + * [ "set", [ [ "uuid", "aa095ffb-e1f1-0fc4-8038-82c1ea7e4797" ], + * [ "uuid", "185c93f6-0b39-424e-8587-77d074aa7ce0" ], ... ] ] + */ +static void +_uuids_to_array_inplace(GPtrArray *array, const json_t *items) +{ + const char *key; + json_t * value; + size_t index = 0; + json_t * set_value; + size_t set_index; + + while (index < json_array_size(items)) { + key = json_string_value(json_array_get(items, index)); + index++; + value = json_array_get(items, index); + index++; + + if (!value || !key) + return; + + if (nm_streq(key, "uuid")) { + if (json_is_string(value)) + g_ptr_array_add(array, g_strdup(json_string_value(value))); + continue; + } + if (nm_streq(key, "set")) { + if (json_is_array(value)) { + json_array_foreach (value, set_index, set_value) + _uuids_to_array_inplace(array, set_value); + } + continue; + } + } +} + +static GPtrArray * +_uuids_to_array(const json_t *items) +{ + GPtrArray *array; + + array = g_ptr_array_new_with_free_func(g_free); + _uuids_to_array_inplace(array, items); + return array; +} + +static void +_external_ids_extract(json_t *external_ids, GArray **out_array, const char **out_connection_uuid) +{ + json_t *array; + json_t *value; + gsize index; + + nm_assert(out_array && !*out_array); + nm_assert(!out_connection_uuid || !*out_connection_uuid); + + if (!nm_streq0("map", json_string_value(json_array_get(external_ids, 0)))) + return; + + array = json_array_get(external_ids, 1); + + json_array_foreach (array, index, value) { + const char * key = json_string_value(json_array_get(value, 0)); + const char * val = json_string_value(json_array_get(value, 1)); + NMUtilsNamedValue *v; + + if (!key || !val) + continue; + + if (!*out_array) { + *out_array = g_array_new(FALSE, FALSE, sizeof(NMUtilsNamedValue)); + g_array_set_clear_func(*out_array, + (GDestroyNotify) nm_utils_named_value_clear_with_g_free); + } + + v = nm_g_array_append_new(*out_array, NMUtilsNamedValue); + *v = (NMUtilsNamedValue){ + .name = g_strdup(key), + .value_str = g_strdup(val), + }; + + if (out_connection_uuid && nm_streq(v->name, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID)) { + *out_connection_uuid = v->value_str; + out_connection_uuid = NULL; + } + } +} + +static gboolean +_external_ids_equal(const GArray *arr1, const GArray *arr2) +{ + guint n; + guint i; + + n = nm_g_array_len(arr1); + + if (n != nm_g_array_len(arr2)) + return FALSE; + for (i = 0; i < n; i++) { + const NMUtilsNamedValue *n1 = &g_array_index(arr1, NMUtilsNamedValue, i); + const NMUtilsNamedValue *n2 = &g_array_index(arr2, NMUtilsNamedValue, i); + + if (!nm_streq0(n1->name, n2->name)) + return FALSE; + if (!nm_streq0(n1->value_str, n2->value_str)) + return FALSE; + } + return TRUE; +} + +static char * +_external_ids_to_string(const GArray *arr) +{ + NMStrBuf strbuf; + guint i; + + if (!arr) + return g_strdup("empty"); + + nm_str_buf_init(&strbuf, NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE); + nm_str_buf_append(&strbuf, "["); + for (i = 0; i < arr->len; i++) { + const NMUtilsNamedValue *n = &g_array_index(arr, NMUtilsNamedValue, i); + + if (i > 0) + nm_str_buf_append_c(&strbuf, ','); + nm_str_buf_append_printf(&strbuf, " \"%s\" = \"%s\"]", n->name, n->value_str); + } + nm_str_buf_append(&strbuf, " ]"); + + return nm_str_buf_finalize(&strbuf, NULL); +} + +/*****************************************************************************/ + +/** + * ovsdb_got_update: + * + * Called when we've got an "update" method call (we asked for it with the monitor + * command). We use it to maintain a consistent view of bridge list regardless of + * whether the changes are done by us or externally. + */ +static void +ovsdb_got_update(NMOvsdb *self, json_t *msg) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + json_t * ovs = NULL; + json_t * bridge = NULL; + json_t * port = NULL; + json_t * interface = NULL; + json_t * items; + json_t * external_ids; + json_error_t json_error = { + 0, + }; + void * iter; + const char *name; + const char *key; + const char *type; + json_t * value; + + if (json_unpack_ex(msg, + &json_error, + 0, + "{s?:o, s?:o, s?:o, s?:o}", + "Open_vSwitch", + &ovs, + "Bridge", + &bridge, + "Port", + &port, + "Interface", + &interface) + == -1) { + /* This doesn't really have to be an error; the key might + * be missing if there really are no bridges present. */ + _LOGD("Bad update: %s", json_error.text); + } + + if (ovs) { + const char *s; + + iter = json_object_iter(ovs); + s = json_object_iter_key(iter); + if (s) + nm_utils_strdup_reset(&priv->db_uuid, s); + } + + json_object_foreach (interface, key, value) { + OpenvswitchInterface *ovs_interface; + gs_unref_array GArray *external_ids_arr = NULL; + const char * connection_uuid = NULL; + json_t * error = NULL; + int r; + + r = json_unpack(value, + "{s:{s:s, s:s, s?:o, s:o}}", + "new", + "name", + &name, + "type", + &type, + "error", + &error, + "external_ids", + &external_ids); + if (r != 0) { + gpointer unused; + + r = json_unpack(value, "{s:{}}", "old"); + if (r != 0) + continue; + + if (!g_hash_table_steal_extended(priv->interfaces, + &key, + (gpointer *) &ovs_interface, + &unused)) + continue; + + _LOGT("obj[iface:%s]: removed an '%s' interface: %s%s%s", + key, + ovs_interface->type, + ovs_interface->name, + NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid, + ", ", + ovs_interface->connection_uuid, + "")); + _signal_emit_device_removed(self, + ovs_interface->name, + NM_DEVICE_TYPE_OVS_INTERFACE, + ovs_interface->type); + _free_interface(ovs_interface); + continue; + } + + ovs_interface = g_hash_table_lookup(priv->interfaces, &key); + + if (ovs_interface + && (!nm_streq0(ovs_interface->name, name) || !nm_streq0(ovs_interface->type, type))) { + if (!g_hash_table_steal(priv->interfaces, ovs_interface)) + nm_assert_not_reached(); + _signal_emit_device_removed(self, + ovs_interface->name, + NM_DEVICE_TYPE_OVS_INTERFACE, + ovs_interface->type); + nm_clear_pointer(&ovs_interface, _free_interface); + } + + _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid); + + if (ovs_interface) { + gboolean changed = FALSE; + + nm_assert(nm_streq0(ovs_interface->name, name)); + + changed |= nm_utils_strdup_reset(&ovs_interface->type, type); + changed |= nm_utils_strdup_reset(&ovs_interface->connection_uuid, connection_uuid); + if (!_external_ids_equal(ovs_interface->external_ids, external_ids_arr)) { + NM_SWAP(&ovs_interface->external_ids, &external_ids_arr); + changed = TRUE; + } + if (changed) { + gs_free char *strtmp = NULL; + + _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s", + key, + type, + ovs_interface->name, + NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid, + ", ", + ovs_interface->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_interface->external_ids))); + } + } else { + gs_free char *strtmp = NULL; + + ovs_interface = g_slice_new(OpenvswitchInterface); + *ovs_interface = (OpenvswitchInterface){ + .interface_uuid = g_strdup(key), + .name = g_strdup(name), + .type = g_strdup(type), + .connection_uuid = g_strdup(connection_uuid), + .external_ids = g_steal_pointer(&external_ids_arr), + }; + g_hash_table_add(priv->interfaces, ovs_interface); + _LOGT("obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s", + key, + ovs_interface->type, + ovs_interface->name, + NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid, + ", ", + ovs_interface->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_interface->external_ids))); + _signal_emit_device_added(self, + ovs_interface->name, + NM_DEVICE_TYPE_OVS_INTERFACE, + ovs_interface->type); + } + + /* The error is a string. No error is indicated by an empty set, + * Why not: [ "set": [] ] ? */ + if (error && json_is_string(error)) { + _signal_emit_interface_failed(self, + ovs_interface->name, + ovs_interface->connection_uuid, + json_string_value(error)); + } + } + + json_object_foreach (port, key, value) { + gs_unref_ptrarray GPtrArray *interfaces = NULL; + OpenvswitchPort * ovs_port; + gs_unref_array GArray *external_ids_arr = NULL; + const char * connection_uuid = NULL; + int r; + + r = json_unpack(value, + "{s:{s:s, s:o, s:o}}", + "new", + "name", + &name, + "external_ids", + &external_ids, + "interfaces", + &items); + if (r != 0) { + gpointer unused; + + r = json_unpack(value, "{s:{}}", "old"); + if (r != 0) + continue; + + if (!g_hash_table_steal_extended(priv->ports, &key, (gpointer *) &ovs_port, &unused)) + continue; + + _LOGT("obj[port:%s]: removed a port: %s%s%s", + key, + ovs_port->name, + NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid, + ", ", + ovs_port->connection_uuid, + "")); + _signal_emit_device_removed(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL); + _free_port(ovs_port); + continue; + } + + ovs_port = g_hash_table_lookup(priv->ports, &key); + + if (ovs_port && !nm_streq0(ovs_port->name, name)) { + if (!g_hash_table_steal(priv->ports, ovs_port)) + nm_assert_not_reached(); + _signal_emit_device_removed(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL); + nm_clear_pointer(&ovs_port, _free_port); + } + + _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid); + interfaces = _uuids_to_array(items); + + if (ovs_port) { + gboolean changed = FALSE; + + nm_assert(nm_streq0(ovs_port->name, name)); + + changed |= nm_utils_strdup_reset(&ovs_port->name, name); + changed |= nm_utils_strdup_reset(&ovs_port->connection_uuid, connection_uuid); + if (nm_strv_ptrarray_cmp(ovs_port->interfaces, interfaces) != 0) { + NM_SWAP(&ovs_port->interfaces, &interfaces); + changed = TRUE; + } + if (!_external_ids_equal(ovs_port->external_ids, external_ids_arr)) { + NM_SWAP(&ovs_port->external_ids, &external_ids_arr); + changed = TRUE; + } + if (changed) { + gs_free char *strtmp = NULL; + + _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s", + key, + ovs_port->name, + NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid, + ", ", + ovs_port->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_port->external_ids))); + } + } else { + gs_free char *strtmp = NULL; + + ovs_port = g_slice_new(OpenvswitchPort); + *ovs_port = (OpenvswitchPort){ + .port_uuid = g_strdup(key), + .name = g_strdup(name), + .connection_uuid = g_strdup(connection_uuid), + .interfaces = g_steal_pointer(&interfaces), + .external_ids = g_steal_pointer(&external_ids_arr), + }; + g_hash_table_add(priv->ports, ovs_port); + _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s", + key, + ovs_port->name, + NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid, + ", ", + ovs_port->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_port->external_ids))); + _signal_emit_device_added(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL); + } + } + + json_object_foreach (bridge, key, value) { + gs_unref_ptrarray GPtrArray *ports = NULL; + OpenvswitchBridge * ovs_bridge; + gs_unref_array GArray *external_ids_arr = NULL; + const char * connection_uuid = NULL; + int r; + + r = json_unpack(value, + "{s:{s:s, s:o, s:o}}", + "new", + "name", + &name, + "external_ids", + &external_ids, + "ports", + &items); + + if (r != 0) { + gpointer unused; + + r = json_unpack(value, "{s:{}}", "old"); + if (r != 0) + continue; + + if (!g_hash_table_steal_extended(priv->bridges, + &key, + (gpointer *) &ovs_bridge, + &unused)) + continue; + + _LOGT("obj[bridge:%s]: removed a bridge: %s%s%s", + key, + ovs_bridge->name, + NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid, + ", ", + ovs_bridge->connection_uuid, + "")); + _signal_emit_device_removed(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL); + _free_bridge(ovs_bridge); + continue; + } + + ovs_bridge = g_hash_table_lookup(priv->bridges, &key); + + if (ovs_bridge && !nm_streq0(ovs_bridge->name, name)) { + if (!g_hash_table_steal(priv->bridges, ovs_bridge)) + nm_assert_not_reached(); + _signal_emit_device_removed(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL); + nm_clear_pointer(&ovs_bridge, _free_bridge); + } + + _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid); + ports = _uuids_to_array(items); + + if (ovs_bridge) { + gboolean changed = FALSE; + + nm_assert(nm_streq0(ovs_bridge->name, name)); + + changed = nm_utils_strdup_reset(&ovs_bridge->name, name); + changed = nm_utils_strdup_reset(&ovs_bridge->connection_uuid, connection_uuid); + if (nm_strv_ptrarray_cmp(ovs_bridge->ports, ports) != 0) { + NM_SWAP(&ovs_bridge->ports, &ports); + changed = TRUE; + } + if (!_external_ids_equal(ovs_bridge->external_ids, external_ids_arr)) { + NM_SWAP(&ovs_bridge->external_ids, &external_ids_arr); + changed = TRUE; + } + if (changed) { + gs_free char *strtmp = NULL; + + _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s", + key, + ovs_bridge->name, + NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid, + ", ", + ovs_bridge->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_bridge->external_ids))); + } + } else { + gs_free char *strtmp = NULL; + + ovs_bridge = g_slice_new(OpenvswitchBridge); + *ovs_bridge = (OpenvswitchBridge){ + .bridge_uuid = g_strdup(key), + .name = g_strdup(name), + .connection_uuid = g_strdup(connection_uuid), + .ports = g_steal_pointer(&ports), + .external_ids = g_steal_pointer(&external_ids_arr), + }; + g_hash_table_add(priv->bridges, ovs_bridge); + _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s", + key, + ovs_bridge->name, + NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid, + ", ", + ovs_bridge->connection_uuid, + ""), + (strtmp = _external_ids_to_string(ovs_bridge->external_ids))); + _signal_emit_device_added(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL); + } + } +} + +/** + * ovsdb_got_echo: + * + * Only implemented because the specification mandates it. Actual ovsdb hasn't been + * seen doing this. + */ +static void +ovsdb_got_echo(NMOvsdb *self, json_int_t id, json_t *data) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + nm_auto_decref_json json_t *msg = NULL; + char * reply; + gboolean output_was_empty; + + output_was_empty = priv->output->len == 0; + + msg = json_pack("{s:I, s:O}", "id", id, "result", data); + reply = json_dumps(msg, 0); + g_string_append(priv->output, reply); + free(reply); + + if (output_was_empty) + ovsdb_write(self); +} + +/** + * ovsdb_got_msg:: + * + * Called when a complete JSON object was seen and unmarshalled. + * Either finishes a method call or processes a method call. + */ +static void +ovsdb_got_msg(NMOvsdb *self, json_t *msg) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + json_error_t json_error = { + 0, + }; + json_t * json_id = NULL; + json_int_t id = (json_int_t) -1; + const char *method = NULL; + json_t * params = NULL; + json_t * result = NULL; + json_t * error = NULL; + + if (json_unpack_ex(msg, + &json_error, + 0, + "{s?:o, s?:s, s?:o, s?:o, s?:o}", + "id", + &json_id, + "method", + &method, + "params", + ¶ms, + "result", + &result, + "error", + &error) + == -1) { + _LOGW("couldn't grok the message: %s", json_error.text); + ovsdb_disconnect(self, FALSE, FALSE); + return; + } + + if (json_is_number(json_id)) + id = json_integer_value(json_id); + + if (method) { + /* It's a method call! */ + if (!params) { + _LOGW("a method call with no params: '%s'", method); + ovsdb_disconnect(self, FALSE, FALSE); + return; + } + + if (nm_streq0(method, "update")) { + /* This is a update method call. */ + ovsdb_got_update(self, json_array_get(params, 1)); + } else if (nm_streq0(method, "echo")) { + /* This is an echo request. */ + ovsdb_got_echo(self, id, params); + } else { + _LOGW("got an unknown method call: '%s'", method); + } + return; + } + + if (id >= 0) { + OvsdbMethodCall *call; + gs_free_error GError *local = NULL; + gs_free char * msg_as_str = NULL; + + /* This is a response to a method call. */ + if (c_list_is_empty(&priv->calls_lst_head)) { + _LOGE("there are no queued calls expecting response %" G_GUINT64_FORMAT, (guint64) id); + ovsdb_disconnect(self, FALSE, FALSE); + return; + } + call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst); + if (call->call_id != id) { + _LOGE("expected a response to call %" G_GUINT64_FORMAT ", not %" G_GUINT64_FORMAT, + call->call_id, + (guint64) id); + ovsdb_disconnect(self, FALSE, FALSE); + return; + } + /* Cool, we found a corresponding call. Finish it. */ + + _LOGT_call(call, "response: %s", (msg_as_str = json_dumps(msg, 0))); + + if (!json_is_null(error)) { + /* The response contains an error. */ + g_set_error(&local, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Error call to OVSDB returned an error: %s", + json_string_value(error)); + } + + _call_complete(call, result, local); + + priv->num_failures = 0; + + /* Don't progress further commands in case the callback hit an error + * and disconnected us. */ + if (!priv->conn) + return; + + /* Now we're free to serialize and send the next command, if any. */ + ovsdb_next_command(self); + + return; + } + + /* This is a message we are not interested in. */ + _LOGW("got an unknown message, ignoring"); +} + +/*****************************************************************************/ + +/* Lower level marshalling and demarshalling of the JSON-RPC traffic on the + * ovsdb socket. */ + +static size_t +_json_callback(void *buffer, size_t buflen, void *user_data) +{ + NMOvsdb * self = NM_OVSDB(user_data); + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + if (priv->bufp == priv->input->len) { + /* No more bytes buffered for decoding. */ + return 0; + } + + /* Pass one more byte to the JSON decoder. */ + *(char *) buffer = priv->input->str[priv->bufp]; + priv->bufp++; + + return (size_t) 1; +} + +/** + * ovsdb_read_cb: + * + * Read out the data available from the ovsdb socket and try to deserialize + * the JSON. If we see a complete object, pass it upwards to ovsdb_got_msg(). + */ +static void +ovsdb_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + NMOvsdb * self = NM_OVSDB(user_data); + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + GInputStream * stream = G_INPUT_STREAM(source_object); + GError * error = NULL; + gssize size; + json_t * msg; + json_error_t json_error = { + 0, + }; + + size = g_input_stream_read_finish(stream, res, &error); + if (size == -1) { + /* ovsdb-server was possibly restarted */ + _LOGW("short read from ovsdb: %s", error->message); + priv->num_failures++; + g_clear_error(&error); + ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE); + return; + } + + g_string_append_len(priv->input, priv->buf, size); + do { + priv->bufp = 0; + /* The callback always eats up only up to a single byte. This makes + * it possible for us to identify complete JSON objects in spite of + * us not knowing the length in advance. */ + msg = json_load_callback(_json_callback, self, JSON_DISABLE_EOF_CHECK, &json_error); + if (msg) { + ovsdb_got_msg(self, msg); + g_string_erase(priv->input, 0, priv->bufp); + } + json_decref(msg); + } while (msg); + + if (!priv->conn) + return; + + if (size) + ovsdb_read(self); +} + +static void +ovsdb_read(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + g_input_stream_read_async(g_io_stream_get_input_stream(G_IO_STREAM(priv->conn)), + priv->buf, + sizeof(priv->buf), + G_PRIORITY_DEFAULT, + NULL, + ovsdb_read_cb, + self); +} + +static void +ovsdb_write_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GOutputStream * stream = G_OUTPUT_STREAM(source_object); + NMOvsdb * self = NM_OVSDB(user_data); + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + GError * error = NULL; + gssize size; + + size = g_output_stream_write_finish(stream, res, &error); + if (size == -1) { + /* ovsdb-server was possibly restarted */ + _LOGW("short write to ovsdb: %s", error->message); + priv->num_failures++; + g_clear_error(&error); + ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE); + return; + } + + if (!priv->conn) + return; + + g_string_erase(priv->output, 0, size); + + ovsdb_write(self); +} + +static void +ovsdb_write(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + GOutputStream * stream; + + if (!priv->output->len) + return; + + stream = g_io_stream_get_output_stream(G_IO_STREAM(priv->conn)); + if (g_output_stream_has_pending(stream)) + return; + + g_output_stream_write_async(stream, + priv->output->str, + priv->output->len, + G_PRIORITY_DEFAULT, + NULL, + ovsdb_write_cb, + self); +} + +/*****************************************************************************/ + +/* Routines to maintain the ovsdb connection. */ + +/** + * ovsdb_disconnect: + * + * Clean up the internal state to the point equivalent to before connecting. + * Apart from clean shutdown this is a good response to unexpected trouble, + * since the next method call attempt a will trigger reconnect which hopefully + * puts us back in sync. + */ +static void +ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + OvsdbMethodCall *call; + + nm_assert(!retry || !is_disposing); + + if (!priv->client) + return; + + _LOGD("disconnecting from ovsdb, retry %d", retry); + + /* FIXME(shutdown): NMOvsdb should process the pending calls before + * shutting down, and cancel the remaining calls after the timeout. */ + + if (retry) { + if (!c_list_is_empty(&priv->calls_lst_head)) { + call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst); + call->call_id = CALL_ID_UNSPEC; + } + } else { + gs_free_error GError *error = NULL; + + if (is_disposing) + nm_utils_error_set_cancelled(&error, is_disposing, "NMOvsdb"); + else + nm_utils_error_set(&error, NM_UTILS_ERROR_NOT_READY, "disconnected from ovsdb"); + while ((call = c_list_last_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst))) + _call_complete(call, NULL, error); + } + + priv->bufp = 0; + g_string_truncate(priv->input, 0); + g_string_truncate(priv->output, 0); + g_clear_object(&priv->client); + g_clear_object(&priv->conn); + nm_clear_g_free(&priv->db_uuid); + nm_clear_g_cancellable(&priv->cancellable); + + if (retry) + ovsdb_try_connect(self); +} + +static void +_check_ready(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + nm_assert(!priv->ready); + + if (priv->num_pending_deletions == 0) { + priv->ready = TRUE; + g_signal_emit(self, signals[READY], 0); + nm_manager_unblock_failed_ovs_interfaces(nm_manager_get()); + } +} + +static void +_del_initial_iface_cb(GError *error, gpointer user_data) +{ + NMOvsdb * self; + gs_free char * ifname = NULL; + NMOvsdbPrivate *priv; + + nm_utils_user_data_unpack(user_data, &self, &ifname); + + if (nm_utils_error_is_cancelled_or_disposing(error)) + return; + + priv = NM_OVSDB_GET_PRIVATE(self); + nm_assert(priv->num_pending_deletions > 0); + priv->num_pending_deletions--; + + _LOGD("delete initial interface '%s': %s %s%s%s, pending %u", + ifname, + error ? "error" : "success", + error ? "(" : "", + error ? error->message : "", + error ? ")" : "", + priv->num_pending_deletions); + + _check_ready(self); +} + +static void +ovsdb_cleanup_initial_interfaces(NMOvsdb *self) +{ + NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self); + const OpenvswitchInterface *interface; + NMUtilsUserData * data; + GHashTableIter iter; + + if (priv->ready || priv->num_pending_deletions != 0) + return; + + /* Delete OVS interfaces added by NM. Bridges and ports and + * not considered because they are deleted automatically + * when no interface is present. */ + g_hash_table_iter_init(&iter, self->_priv.interfaces); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &interface)) { + if (interface->connection_uuid) { + priv->num_pending_deletions++; + _LOGD("deleting initial interface '%s' (pending: %u)", + interface->name, + priv->num_pending_deletions); + data = nm_utils_user_data_pack(self, g_strdup(interface->name)); + nm_ovsdb_del_interface(self, interface->name, _del_initial_iface_cb, data); + } + } + + _check_ready(self); +} + +static void +_monitor_bridges_cb(NMOvsdb *self, json_t *result, GError *error, gpointer user_data) +{ + if (error) { + if (!nm_utils_error_is_cancelled_or_disposing(error)) { + _LOGI("%s", error->message); + ovsdb_disconnect(self, FALSE, FALSE); + } + return; + } + + /* Treat the first response the same as the subsequent "update" + * messages we eventually get. */ + ovsdb_got_update(self, result); + + ovsdb_cleanup_initial_interfaces(self); +} + +static void +_client_connect_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GSocketClient * client = G_SOCKET_CLIENT(source_object); + NMOvsdb * self = NM_OVSDB(user_data); + NMOvsdbPrivate * priv; + GError * error = NULL; + GSocketConnection *conn; + + conn = g_socket_client_connect_finish(client, res, &error); + if (conn == NULL) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + _LOGI("%s", error->message); + + ovsdb_disconnect(self, FALSE, FALSE); + g_clear_error(&error); + return; + } + + priv = NM_OVSDB_GET_PRIVATE(self); + priv->conn = conn; + g_clear_object(&priv->cancellable); + + ovsdb_read(self); + ovsdb_next_command(self); +} + +/** + * ovsdb_try_connect: + * + * Establish a connection to ovsdb unless it's already established or being + * established. Queues a monitor command as a very first one so that we're in + * sync when other commands are issued. + */ +static void +ovsdb_try_connect(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + GSocketAddress *addr; + + if (priv->client) + return; + + /* TODO: This should probably be made configurable via NetworkManager.conf */ + addr = g_unix_socket_address_new(RUNSTATEDIR "/openvswitch/db.sock"); + + priv->client = g_socket_client_new(); + priv->cancellable = g_cancellable_new(); + g_socket_client_connect_async(priv->client, + G_SOCKET_CONNECTABLE(addr), + priv->cancellable, + _client_connect_cb, + self); + g_object_unref(addr); + + /* Queue a monitor call before any other command, ensuring that we have an up + * to date view of existing bridged that we need for add and remove ops. */ + ovsdb_call_method(self, + _monitor_bridges_cb, + NULL, + TRUE, + OVSDB_MONITOR, + OVSDB_METHOD_PAYLOAD_MONITOR()); +} + +/*****************************************************************************/ + +/* Public functions useful for NMDeviceOpenvswitch to maintain the life cycle of + * their ovsdb entries without having to deal with ovsdb complexities themselves. */ + +typedef struct { + NMOvsdbCallback callback; + gpointer user_data; +} OvsdbCall; + +static void +_transact_cb(NMOvsdb *self, json_t *result, GError *error, gpointer user_data) +{ + OvsdbCall * call = user_data; + const char *err; + const char *err_details; + size_t index; + json_t * value; + + if (error) + goto out; + + json_array_foreach (result, index, value) { + if (json_unpack(value, "{s:s, s:s}", "error", &err, "details", &err_details) == 0) { + g_set_error(&error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Error running the transaction: %s: %s", + err, + err_details); + goto out; + } + } + +out: + call->callback(error, call->user_data); + nm_g_slice_free(call); +} + +static OvsdbCall * +ovsdb_call_new(NMOvsdbCallback callback, gpointer user_data) +{ + OvsdbCall *call; + + call = g_slice_new(OvsdbCall); + *call = (OvsdbCall){ + .callback = callback, + .user_data = user_data, + }; + return call; +} + +gboolean +nm_ovsdb_is_ready(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + return priv->ready; +} + +void +nm_ovsdb_add_interface(NMOvsdb * self, + NMConnection * bridge, + NMConnection * port, + NMConnection * interface, + NMDevice * bridge_device, + NMDevice * interface_device, + NMOvsdbCallback callback, + gpointer user_data) +{ + ovsdb_call_method(self, + _transact_cb, + ovsdb_call_new(callback, user_data), + FALSE, + OVSDB_ADD_INTERFACE, + OVSDB_METHOD_PAYLOAD_ADD_INTERFACE(bridge, + port, + interface, + bridge_device, + interface_device)); +} + +void +nm_ovsdb_del_interface(NMOvsdb * self, + const char * ifname, + NMOvsdbCallback callback, + gpointer user_data) +{ + ovsdb_call_method(self, + _transact_cb, + ovsdb_call_new(callback, user_data), + FALSE, + OVSDB_DEL_INTERFACE, + OVSDB_METHOD_PAYLOAD_DEL_INTERFACE(ifname)); +} + +void +nm_ovsdb_set_interface_mtu(NMOvsdb * self, + const char * ifname, + guint32 mtu, + NMOvsdbCallback callback, + gpointer user_data) +{ + ovsdb_call_method(self, + _transact_cb, + ovsdb_call_new(callback, user_data), + FALSE, + OVSDB_SET_INTERFACE_MTU, + OVSDB_METHOD_PAYLOAD_SET_INTERFACE_MTU(ifname, mtu)); +} + +void +nm_ovsdb_set_external_ids(NMOvsdb * self, + NMDeviceType device_type, + const char * ifname, + const char * connection_uuid, + NMSettingOvsExternalIDs *s_exid_old, + NMSettingOvsExternalIDs *s_exid_new) +{ + gs_unref_hashtable GHashTable *exid_old = NULL; + gs_unref_hashtable GHashTable *exid_new = NULL; + + exid_old = s_exid_old + ? nm_utils_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_old)) + : NULL; + exid_new = s_exid_new + ? nm_utils_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_new)) + : NULL; + + ovsdb_call_method(self, + NULL, + NULL, + FALSE, + OVSDB_SET_EXTERNAL_IDS, + OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(device_type, + ifname, + connection_uuid, + exid_old, + exid_new)); +} + +/*****************************************************************************/ + +static void +nm_ovsdb_init(NMOvsdb *self) +{ + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + c_list_init(&priv->calls_lst_head); + + priv->input = g_string_new(NULL); + priv->output = g_string_new(NULL); + priv->bridges = + g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_bridge, NULL); + priv->ports = + g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_port, NULL); + priv->interfaces = + g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_interface, NULL); + + ovsdb_try_connect(self); +} + +static void +dispose(GObject *object) +{ + NMOvsdb * self = NM_OVSDB(object); + NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self); + + ovsdb_disconnect(self, FALSE, TRUE); + + nm_assert(c_list_is_empty(&priv->calls_lst_head)); + + if (priv->input) { + g_string_free(priv->input, TRUE); + priv->input = NULL; + } + if (priv->output) { + g_string_free(priv->output, TRUE); + priv->output = NULL; + } + + nm_clear_pointer(&priv->bridges, g_hash_table_destroy); + nm_clear_pointer(&priv->ports, g_hash_table_destroy); + nm_clear_pointer(&priv->interfaces, g_hash_table_destroy); + + G_OBJECT_CLASS(nm_ovsdb_parent_class)->dispose(object); +} + +static void +nm_ovsdb_class_init(NMOvsdbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dispose; + + signals[DEVICE_ADDED] = g_signal_new(NM_OVSDB_DEVICE_ADDED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_STRING, + G_TYPE_UINT, + G_TYPE_STRING); + + signals[DEVICE_REMOVED] = g_signal_new(NM_OVSDB_DEVICE_REMOVED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_STRING, + G_TYPE_UINT, + G_TYPE_STRING); + + signals[INTERFACE_FAILED] = g_signal_new(NM_OVSDB_INTERFACE_FAILED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); + + signals[READY] = g_signal_new(NM_OVSDB_READY, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} diff --git a/src/core/devices/ovs/nm-ovsdb.h b/src/core/devices/ovs/nm-ovsdb.h new file mode 100644 index 00000000..d0b4d19d --- /dev/null +++ b/src/core/devices/ovs/nm-ovsdb.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_OVSDB_H__ +#define __NETWORKMANAGER_OVSDB_H__ + +#define NM_TYPE_OVSDB (nm_ovsdb_get_type()) +#define NM_OVSDB(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVSDB, NMOvsdb)) +#define NM_OVSDB_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVSDB, NMOvsdbClass)) +#define NM_IS_OVSDB(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVSDB)) +#define NM_IS_OVSDB_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_OVSDB)) +#define NM_OVSDB_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_OVSDB, NMOvsdbClass)) + +#define NM_OVSDB_DEVICE_ADDED "device-added" +#define NM_OVSDB_DEVICE_REMOVED "device-removed" +#define NM_OVSDB_INTERFACE_FAILED "interface-failed" +#define NM_OVSDB_READY "ready" + +typedef struct _NMOvsdb NMOvsdb; +typedef struct _NMOvsdbClass NMOvsdbClass; + +typedef void (*NMOvsdbCallback)(GError *error, gpointer user_data); + +NMOvsdb *nm_ovsdb_get(void); + +GType nm_ovsdb_get_type(void); + +void nm_ovsdb_add_interface(NMOvsdb * self, + NMConnection * bridge, + NMConnection * port, + NMConnection * interface, + NMDevice * bridge_device, + NMDevice * interface_device, + NMOvsdbCallback callback, + gpointer user_data); + +void nm_ovsdb_del_interface(NMOvsdb * self, + const char * ifname, + NMOvsdbCallback callback, + gpointer user_data); + +void nm_ovsdb_set_interface_mtu(NMOvsdb * self, + const char * ifname, + guint32 mtu, + NMOvsdbCallback callback, + gpointer user_data); + +struct _NMSettingOvsExternalIDs; + +void nm_ovsdb_set_external_ids(NMOvsdb * self, + NMDeviceType device_type, + const char * ifname, + const char * connection_uuid, + struct _NMSettingOvsExternalIDs *s_exid_old, + struct _NMSettingOvsExternalIDs *s_exid_new); + +gboolean nm_ovsdb_is_ready(NMOvsdb *self); + +#endif /* __NETWORKMANAGER_OVSDB_H__ */ diff --git a/src/core/devices/team/meson.build b/src/core/devices/team/meson.build new file mode 100644 index 00000000..d0ff4caa --- /dev/null +++ b/src/core/devices/team/meson.build @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +libnm_device_plugin_team = shared_module( + 'nm-device-plugin-team', + sources: files( + 'nm-device-team.c', + 'nm-team-factory.c', + ), + dependencies: [ + core_plugin_dep, + jansson_dep, + libteamdctl_dep, + ], + c_args: daemon_c_flags, + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, +) + +core_plugins += libnm_device_plugin_team + +test( + 'check-local-devices-team', + check_exports, + args: [ + libnm_device_plugin_team.full_path(), + linker_script_devices, + ], +) diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c new file mode 100644 index 00000000..d2d71729 --- /dev/null +++ b/src/core/devices/team/nm-device-team.c @@ -0,0 +1,1096 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Jiri Pirko <jiri@resnulli.us> + * Copyright (C) 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-team.h" + +#include <sys/types.h> +#include <unistd.h> +#include <signal.h> +#include <sys/wait.h> +#include <teamdctl.h> +#include <stdlib.h> + +#include "nm-glib-aux/nm-jansson.h" +#include "NetworkManagerUtils.h" +#include "devices/nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-config.h" +#include "nm-core-internal.h" +#include "nm-dbus-manager.h" +#include "nm-ip4-config.h" +#include "nm-std-aux/nm-dbus-compat.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceTeam +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceTeam, PROP_CONFIG, ); + +typedef struct { + struct teamdctl * tdc; + char * config; + GPid teamd_pid; + guint teamd_process_watch; + guint teamd_timeout; + guint teamd_read_timeout; + guint teamd_dbus_watch; + bool kill_in_progress : 1; + GFileMonitor * usock_monitor; + NMDeviceStageState stage1_state : 3; +} NMDeviceTeamPrivate; + +struct _NMDeviceTeam { + NMDevice parent; + NMDeviceTeamPrivate _priv; +}; + +struct _NMDeviceTeamClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceTeam, nm_device_team, NM_TYPE_DEVICE) + +#define NM_DEVICE_TEAM_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceTeam, NM_IS_DEVICE_TEAM, NMDevice) + +/*****************************************************************************/ + +static gboolean teamd_start(NMDeviceTeam *self); + +/*****************************************************************************/ + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingTeam *s_team; + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_TEAM_SETTING_NAME, + existing_connections, + NULL, + _("Team connection"), + "team", + NULL, + TRUE); + + s_team = nm_connection_get_setting_team(connection); + if (!s_team) { + s_team = (NMSettingTeam *) nm_setting_team_new(); + nm_connection_add_setting(connection, NM_SETTING(s_team)); + } + + return TRUE; +} + +static gboolean +ensure_teamd_connection(NMDevice *device) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + int err; + + if (priv->tdc) + return TRUE; + + priv->tdc = teamdctl_alloc(); + g_assert(priv->tdc); + err = teamdctl_connect(priv->tdc, nm_device_get_iface(device), NULL, NULL); + if (err != 0) { + _LOGE(LOGD_TEAM, "failed to connect to teamd (err=%d)", err); + teamdctl_free(priv->tdc); + priv->tdc = NULL; + } + + return !!priv->tdc; +} + +static const char * +_get_config(NMDeviceTeam *self) +{ + return nm_str_not_empty(NM_DEVICE_TEAM_GET_PRIVATE(self)->config); +} + +static gboolean +teamd_read_config(NMDeviceTeam *self) +{ + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + const char * config = NULL; + int err; + + if (priv->tdc) { + err = teamdctl_config_actual_get_raw_direct(priv->tdc, (char **) &config); + if (err) + return FALSE; + if (!config) { + /* set "" to distinguish an empty result from no config at all. */ + config = ""; + } + } + + if (!nm_streq0(config, priv->config)) { + g_free(priv->config); + priv->config = g_strdup(config); + _notify(self, PROP_CONFIG); + } + + return TRUE; +} + +static gboolean +teamd_read_timeout_cb(gpointer user_data) +{ + NMDeviceTeam * self = user_data; + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + priv->teamd_read_timeout = 0; + teamd_read_config(self); + return G_SOURCE_REMOVE; +} + +static void +update_connection(NMDevice *device, NMConnection *connection) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMSettingTeam * s_team = nm_connection_get_setting_team(connection); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + struct teamdctl * tdc = priv->tdc; + + if (!s_team) { + s_team = (NMSettingTeam *) nm_setting_team_new(); + nm_connection_add_setting(connection, (NMSetting *) s_team); + } + + /* Read the configuration only if not already set */ + if (!priv->config && ensure_teamd_connection(device)) + teamd_read_config(self); + + /* Restore previous tdc state */ + if (priv->tdc && !tdc) { + teamdctl_disconnect(priv->tdc); + teamdctl_free(priv->tdc); + priv->tdc = NULL; + } + + g_object_set(G_OBJECT(s_team), NM_SETTING_TEAM_CONFIG, _get_config(self), NULL); +} + +/*****************************************************************************/ + +static gboolean +master_update_slave_connection(NMDevice * self, + NMDevice * slave, + NMConnection *connection, + GError ** error) +{ + NMSettingTeamPort *s_port; + char * port_config = NULL; + int err = 0; + struct teamdctl * tdc; + const char * team_port_config = NULL; + const char * iface = nm_device_get_iface(self); + const char * iface_slave = nm_device_get_iface(slave); + + tdc = teamdctl_alloc(); + if (!tdc) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "update slave connection for slave '%s' failed to connect to teamd for master " + "%s (out of memory?)", + iface_slave, + iface); + g_return_val_if_reached(FALSE); + } + + err = teamdctl_connect(tdc, iface, NULL, NULL); + if (err) { + teamdctl_free(tdc); + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "update slave connection for slave '%s' failed to connect to teamd for master " + "%s (err=%d)", + iface_slave, + iface, + err); + return FALSE; + } + + err = teamdctl_port_config_get_raw_direct(tdc, iface_slave, (char **) &team_port_config); + port_config = g_strdup(team_port_config); + teamdctl_disconnect(tdc); + teamdctl_free(tdc); + if (err) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + "update slave connection for slave '%s' failed to get configuration from teamd " + "master %s (err=%d)", + iface_slave, + iface, + err); + g_free(port_config); + return FALSE; + } + + s_port = nm_connection_get_setting_team_port(connection); + if (!s_port) { + s_port = (NMSettingTeamPort *) nm_setting_team_port_new(); + nm_connection_add_setting(connection, NM_SETTING(s_port)); + } + + g_object_set(G_OBJECT(s_port), NM_SETTING_TEAM_PORT_CONFIG, port_config, NULL); + g_free(port_config); + + g_object_set(nm_connection_get_setting_connection(connection), + NM_SETTING_CONNECTION_MASTER, + iface, + NM_SETTING_CONNECTION_SLAVE_TYPE, + NM_SETTING_TEAM_SETTING_NAME, + NULL); + return TRUE; +} + +/*****************************************************************************/ + +static void +teamd_kill_cb(pid_t pid, gboolean success, int child_status, void *user_data) +{ + gs_unref_object NMDeviceTeam *self = user_data; + NMDeviceTeamPrivate * priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + priv->kill_in_progress = FALSE; + + if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_PREPARE) { + _LOGT(LOGD_TEAM, "kill terminated"); + return; + } + + _LOGT(LOGD_TEAM, "kill terminated, starting teamd..."); + if (!teamd_start(self)) { + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + } +} + +static void +teamd_cleanup(NMDeviceTeam *self, gboolean free_tdc) +{ + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + nm_clear_g_source(&priv->teamd_process_watch); + nm_clear_g_source(&priv->teamd_timeout); + nm_clear_g_source(&priv->teamd_read_timeout); + + if (priv->teamd_pid > 0) { + priv->kill_in_progress = TRUE; + nm_utils_kill_child_async(priv->teamd_pid, + SIGTERM, + LOGD_TEAM, + "teamd", + 2000, + teamd_kill_cb, + g_object_ref(self)); + priv->teamd_pid = 0; + } + + if (priv->tdc && free_tdc) { + teamdctl_disconnect(priv->tdc); + teamdctl_free(priv->tdc); + priv->tdc = NULL; + } +} + +static gboolean +teamd_timeout_cb(gpointer user_data) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(user_data); + NMDevice * device = NM_DEVICE(self); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + g_return_val_if_fail(priv->teamd_timeout, FALSE); + priv->teamd_timeout = 0; + + if (priv->teamd_pid && !priv->tdc) { + /* Timed out launching our own teamd process */ + _LOGW(LOGD_TEAM, "teamd timed out"); + teamd_cleanup(self, TRUE); + + g_warn_if_fail(nm_device_is_activating(device)); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + } else { + /* Read again the configuration after the timeout since it might + * have changed. + */ + if (!teamd_read_config(self)) { + _LOGW(LOGD_TEAM, "failed to read teamd configuration"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + } + } + + return G_SOURCE_REMOVE; +} + +static void +teamd_ready(NMDeviceTeam *self) +{ + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + gboolean success; + + if (priv->kill_in_progress) { + /* If we are currently killing teamd, we are not + * interested in knowing when it becomes ready. */ + return; + } + + nm_device_queue_recheck_assume(device); + + /* Grab a teamd control handle even if we aren't going to use it + * immediately. But if we are, and grabbing it failed, fail the + * device activation. + */ + success = ensure_teamd_connection(device); + + if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE + || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING) + return; + + if (success) + success = teamd_read_config(self); + + if (!success) { + teamd_cleanup(self, TRUE); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + return; + } + + priv->stage1_state = NM_DEVICE_STAGE_STATE_COMPLETED; + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +teamd_gone(NMDeviceTeam *self) +{ + NMDevice * device = NM_DEVICE(self); + NMDeviceState state; + + teamd_cleanup(self, TRUE); + state = nm_device_get_state(device); + + /* Attempt to respawn teamd */ + if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_ACTIVATED) { + if (!teamd_start(self)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + } + } +} + +static void +teamd_dbus_appeared(GDBusConnection *connection, + const char * name, + const char * name_owner, + gpointer user_data) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(user_data); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + g_return_if_fail(priv->teamd_dbus_watch); + + _LOGI(LOGD_TEAM, "teamd appeared on D-Bus"); + + /* If another teamd grabbed the bus name while our teamd was starting, + * just ignore the death of our teamd and run with the existing one. + */ + if (priv->teamd_process_watch) { + gs_unref_variant GVariant *ret = NULL; + guint32 pid; + + ret = g_dbus_connection_call_sync(connection, + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS, + "GetConnectionUnixProcessID", + g_variant_new("(s)", name_owner), + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 2000, + NULL, + NULL); + + if (ret) { + g_variant_get(ret, "(u)", &pid); + if (pid != priv->teamd_pid) + teamd_cleanup(self, FALSE); + } else { + /* The process that registered on the bus died. If it's + * the teamd instance we just started, ignore the event + * as we already detect the failure through the process + * watch. If it's a previous instance that got killed, + * also ignore that as our new instance will register + * again. */ + _LOGD(LOGD_TEAM, "failed to determine D-Bus name owner, ignoring"); + return; + } + } + + teamd_ready(self); +} + +static void +teamd_dbus_vanished(GDBusConnection *dbus_connection, const char *name, gpointer user_data) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(user_data); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + g_return_if_fail(priv->teamd_dbus_watch); + + if (!priv->tdc) { + /* g_bus_watch_name will always raise an initial signal, to indicate whether the + * name exists/not exists initially. Do not take this as a failure if it hadn't + * previously appeared. + */ + _LOGD(LOGD_TEAM, "teamd not on D-Bus (ignored)"); + return; + } + + _LOGI(LOGD_TEAM, "teamd vanished from D-Bus"); + + teamd_gone(self); +} + +static void +monitor_changed_cb(GFileMonitor * monitor, + GFile * file, + GFile * other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + NMDeviceTeam *self = NM_DEVICE_TEAM(user_data); + + switch (event_type) { + case G_FILE_MONITOR_EVENT_CREATED: + _LOGI(LOGD_TEAM, "file %s was created", g_file_get_path(file)); + teamd_ready(self); + break; + case G_FILE_MONITOR_EVENT_DELETED: + _LOGI(LOGD_TEAM, "file %s was deleted", g_file_get_path(file)); + teamd_gone(self); + break; + default:; + } +} + +static void +teamd_process_watch_cb(GPid pid, int status, gpointer user_data) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(user_data); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMDeviceState state = nm_device_get_state(device); + + g_return_if_fail(priv->teamd_process_watch); + + _LOGD(LOGD_TEAM, "teamd %lld died with status %d", (long long) pid, status); + priv->teamd_pid = 0; + priv->teamd_process_watch = 0; + + /* If teamd quit within 5 seconds of starting, it's probably hosed + * and will just die again, so fail the activation. + */ + if (priv->teamd_timeout && (state >= NM_DEVICE_STATE_PREPARE) + && (state <= NM_DEVICE_STATE_ACTIVATED)) { + _LOGW(LOGD_TEAM, + "teamd process %lld quit unexpectedly; failing activation", + (long long) pid); + teamd_cleanup(self, TRUE); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + } +} + +static void +teamd_child_setup(gpointer user_data) +{ + nm_utils_setpgid(NULL); + signal(SIGPIPE, SIG_IGN); +} + +static const char ** +teamd_env(void) +{ + const char **env = g_new0(const char *, 2); + + if (nm_config_get_is_debug(nm_config_get())) + env[0] = "TEAM_LOG_OUTPUT=stderr"; + else + env[0] = "TEAM_LOG_OUTPUT=syslog"; + + return env; +} + +static gboolean +teamd_kill(NMDeviceTeam *self, const char *teamd_binary, GError **error) +{ + gs_unref_ptrarray GPtrArray *argv = NULL; + gs_free char * tmp_str = NULL; + gs_free const char ** envp = NULL; + + if (!teamd_binary) { + teamd_binary = nm_utils_find_helper("teamd", NULL, error); + if (!teamd_binary) { + _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: teamd binary not found"); + return FALSE; + } + } + + argv = g_ptr_array_new(); + g_ptr_array_add(argv, (gpointer) teamd_binary); + g_ptr_array_add(argv, (gpointer) "-k"); + g_ptr_array_add(argv, (gpointer) "-t"); + g_ptr_array_add(argv, (gpointer) nm_device_get_iface(NM_DEVICE(self))); + g_ptr_array_add(argv, NULL); + + envp = teamd_env(); + + _LOGD(LOGD_TEAM, "running: %s", (tmp_str = g_strjoinv(" ", (char **) argv->pdata))); + return g_spawn_sync("/", + (char **) argv->pdata, + (char **) envp, + 0, + teamd_child_setup, + NULL, + NULL, + NULL, + NULL, + error); +} + +static gboolean +teamd_start(NMDeviceTeam *self) +{ + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + const char * iface = nm_device_get_ip_iface(NM_DEVICE(self)); + NMConnection * connection; + gs_unref_ptrarray GPtrArray *argv = NULL; + gs_free_error GError * error = NULL; + gs_free char * tmp_str = NULL; + const char * teamd_binary; + const char * config; + nm_auto_free const char *config_free = NULL; + NMSettingTeam * s_team; + gs_free char * cloned_mac = NULL; + gs_free const char ** envp = NULL; + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + s_team = nm_connection_get_setting_team(connection); + if (!s_team) + g_return_val_if_reached(FALSE); + + nm_assert(iface); + + teamd_binary = nm_utils_find_helper("teamd", NULL, NULL); + if (!teamd_binary) { + _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: teamd binary not found"); + return FALSE; + } + + if (priv->teamd_process_watch || priv->teamd_pid > 0 || priv->tdc) { + g_warn_if_reached(); + if (!priv->teamd_pid) + teamd_kill(self, teamd_binary, NULL); + teamd_cleanup(self, TRUE); + } + + /* Start teamd now */ + argv = g_ptr_array_new(); + g_ptr_array_add(argv, (gpointer) teamd_binary); + g_ptr_array_add(argv, (gpointer) "-o"); + g_ptr_array_add(argv, (gpointer) "-n"); + g_ptr_array_add(argv, (gpointer) "-U"); + if (priv->teamd_dbus_watch) + g_ptr_array_add(argv, (gpointer) "-D"); + g_ptr_array_add(argv, (gpointer) "-N"); + g_ptr_array_add(argv, (gpointer) "-t"); + g_ptr_array_add(argv, (gpointer) iface); + + config = nm_setting_team_get_config(s_team); + if (!nm_device_hw_addr_get_cloned(NM_DEVICE(self), + connection, + FALSE, + &cloned_mac, + NULL, + &error)) { + _LOGW(LOGD_DEVICE, "set-hw-addr: %s", error->message); + return FALSE; + } + + if (cloned_mac) { + json_t * json, *hwaddr; + json_error_t jerror; + + /* Inject the hwaddr property into the JSON configuration. + * While doing so, detect potential conflicts */ + + json = json_loads(config ?: "{}", JSON_REJECT_DUPLICATES, &jerror); + g_return_val_if_fail(json, FALSE); + + hwaddr = json_object_get(json, "hwaddr"); + if (hwaddr) { + if (!json_is_string(hwaddr) || !nm_streq0(json_string_value(hwaddr), cloned_mac)) + _LOGW(LOGD_TEAM, + "set-hw-addr: can't set team cloned-mac-address as the JSON configuration " + "already contains \"hwaddr\""); + } else { + hwaddr = json_string(cloned_mac); + json_object_set(json, "hwaddr", hwaddr); + config = config_free = + json_dumps(json, JSON_INDENT(0) | JSON_ENSURE_ASCII | JSON_SORT_KEYS); + _LOGD(LOGD_TEAM, + "set-hw-addr: injected \"hwaddr\" \"%s\" into team configuration", + cloned_mac); + json_decref(hwaddr); + } + json_decref(json); + } + + if (config) { + g_ptr_array_add(argv, (gpointer) "-c"); + g_ptr_array_add(argv, (gpointer) config); + } + + if (nm_logging_enabled(LOGL_DEBUG, LOGD_TEAM)) + g_ptr_array_add(argv, (gpointer) "-gg"); + g_ptr_array_add(argv, NULL); + + envp = teamd_env(); + + _LOGD(LOGD_TEAM, "running: %s", (tmp_str = g_strjoinv(" ", (char **) argv->pdata))); + if (!g_spawn_async("/", + (char **) argv->pdata, + (char **) envp, + G_SPAWN_DO_NOT_REAP_CHILD, + teamd_child_setup, + NULL, + &priv->teamd_pid, + &error)) { + _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: %s", error->message); + teamd_cleanup(self, TRUE); + return FALSE; + } + + /* Start a timeout for teamd to appear at D-Bus */ + if (!priv->teamd_timeout) + priv->teamd_timeout = g_timeout_add_seconds(5, teamd_timeout_cb, self); + + /* Monitor the child process so we know when it dies */ + priv->teamd_process_watch = g_child_watch_add(priv->teamd_pid, teamd_process_watch_cb, self); + + _LOGI(LOGD_TEAM, "Activation: (team) started teamd [pid %u]...", (guint) priv->teamd_pid); + return TRUE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + gs_free_error GError *error = NULL; + NMSettingTeam * s_team; + const char * cfg; + + if (nm_device_sys_iface_state_is_external(device)) + return NM_ACT_STAGE_RETURN_SUCCESS; + + if (nm_device_sys_iface_state_is_external_or_assume(device)) { + if (ensure_teamd_connection(device)) + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + s_team = nm_device_get_applied_setting(device, NM_TYPE_SETTING_TEAM); + if (!s_team) + g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE); + + if (priv->stage1_state == NM_DEVICE_STAGE_STATE_PENDING) + return NM_ACT_STAGE_RETURN_POSTPONE; + + if (priv->stage1_state == NM_DEVICE_STAGE_STATE_COMPLETED) + return NM_ACT_STAGE_RETURN_SUCCESS; + + priv->stage1_state = NM_DEVICE_STAGE_STATE_PENDING; + + if (priv->tdc) { + /* If the existing teamd config is the same as we're about to use, + * then we can proceed. If it's not the same, and we have a PID, + * kill it so we can respawn it with the right config. If we don't + * have a PID, then we must fail. + */ + cfg = teamdctl_config_get_raw(priv->tdc); + if (cfg && nm_streq0(cfg, nm_setting_team_get_config(s_team))) { + _LOGD(LOGD_TEAM, "using existing matching teamd config"); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + if (!priv->teamd_pid) { + _LOGD(LOGD_TEAM, "existing teamd config mismatch; killing existing via teamdctl"); + if (!teamd_kill(self, NULL, &error)) { + _LOGW(LOGD_TEAM, + "existing teamd config mismatch; failed to kill existing teamd: %s", + error->message); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + + _LOGD(LOGD_TEAM, "existing teamd config mismatch; respawning..."); + teamd_cleanup(self, TRUE); + } + + if (priv->kill_in_progress) { + _LOGT(LOGD_TEAM, "kill in progress, wait before starting teamd"); + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (!teamd_start(self)) + return NM_ACT_STAGE_RETURN_FAILURE; + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT; + + if (nm_device_sys_iface_state_is_external(device)) + return; + + if (priv->teamd_pid || priv->tdc) + _LOGI(LOGD_TEAM, "deactivation: stopping teamd..."); + + if (!priv->teamd_pid) + teamd_kill(self, NULL, NULL); + + teamd_cleanup(self, TRUE); +} + +static gboolean +enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + gboolean success = TRUE; + const char * slave_iface = nm_device_get_ip_iface(slave); + NMSettingTeamPort * s_team_port; + + nm_device_master_check_slave_physical_port(device, slave, LOGD_TEAM); + + if (configure) { + nm_device_take_down(slave, TRUE); + + s_team_port = nm_connection_get_setting_team_port(connection); + if (s_team_port) { + const char *config = nm_setting_team_port_get_config(s_team_port); + + if (config) { + if (!priv->tdc) { + _LOGW(LOGD_TEAM, + "enslaved team port %s config not changed, not connected to teamd", + slave_iface); + } else { + int err; + char *sanitized_config; + + sanitized_config = g_strdelimit(g_strdup(config), "\r\n", ' '); + err = teamdctl_port_config_update_raw(priv->tdc, slave_iface, sanitized_config); + g_free(sanitized_config); + if (err != 0) { + _LOGE(LOGD_TEAM, + "failed to update config for port %s (err=%d)", + slave_iface, + err); + return FALSE; + } + } + } + } + success = nm_platform_link_enslave(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + nm_device_get_ip_ifindex(slave)); + nm_device_bring_up(slave, TRUE, NULL); + + if (!success) + return FALSE; + + nm_clear_g_source(&priv->teamd_read_timeout); + priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self); + + _LOGI(LOGD_TEAM, "enslaved team port %s", slave_iface); + } else + _LOGI(LOGD_TEAM, "team port %s was enslaved", slave_iface); + + return TRUE; +} + +static void +release_slave(NMDevice *device, NMDevice *slave, gboolean configure) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(device); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + gboolean do_release, success; + NMSettingTeamPort * s_port; + int ifindex_slave; + int ifindex; + + do_release = configure; + if (do_release) { + ifindex = nm_device_get_ifindex(device); + if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex)) + do_release = FALSE; + } + + ifindex_slave = nm_device_get_ip_ifindex(slave); + + if (ifindex_slave <= 0) { + _LOGD(LOGD_TEAM, "team port %s is already released", nm_device_get_ip_iface(slave)); + } else if (do_release) { + success = nm_platform_link_release(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + ifindex_slave); + if (success) + _LOGI(LOGD_TEAM, "released team port %s", nm_device_get_ip_iface(slave)); + else + _LOGW(LOGD_TEAM, "failed to release team port %s", nm_device_get_ip_iface(slave)); + + /* Kernel team code "closes" the port when releasing it, (which clears + * IFF_UP), so we must bring it back up here to ensure carrier changes and + * other state is noticed by the now-released port. + */ + if (!nm_device_bring_up(slave, TRUE, NULL)) { + _LOGW(LOGD_TEAM, + "released team port %s could not be brought up", + nm_device_get_ip_iface(slave)); + } + + nm_clear_g_source(&priv->teamd_read_timeout); + priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self); + } else + _LOGI(LOGD_TEAM, "team port %s was released", nm_device_get_ip_iface(slave)); + + /* Delete any port configuration we previously set */ + if (configure && priv->tdc + && (s_port = nm_device_get_applied_setting(slave, NM_TYPE_SETTING_TEAM_PORT)) + && (nm_setting_team_port_get_config(s_port))) + teamdctl_port_config_update_raw(priv->tdc, nm_device_get_ip_iface(slave), "{}"); +} + +static gboolean +create_and_realize(NMDevice * device, + NMConnection * connection, + NMDevice * parent, + const NMPlatformLink **out_plink, + GError ** error) +{ + const char *iface = nm_device_get_iface(device); + int r; + + r = nm_platform_link_team_add(nm_device_get_platform(device), iface, out_plink); + if (r < 0) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_CREATION_FAILED, + "Failed to create team master interface '%s' for '%s': %s", + iface, + nm_connection_get_id(connection), + nm_strerror(r)); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceTeam *self = NM_DEVICE_TEAM(object); + + switch (prop_id) { + case PROP_CONFIG: + g_value_set_string(value, _get_config(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_team_init(NMDeviceTeam *self) +{ + nm_assert(nm_device_is_master(NM_DEVICE(self))); +} + +static void +constructed(GObject *object) +{ + NMDevice * device = NM_DEVICE(object); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(device); + gs_free char * tmp_str = NULL; + gs_unref_object GFile *file = NULL; + GError * error; + + G_OBJECT_CLASS(nm_device_team_parent_class)->constructed(object); + + if (nm_dbus_manager_get_dbus_connection(nm_dbus_manager_get())) { + /* Register D-Bus name watcher */ + tmp_str = g_strdup_printf("org.libteam.teamd.%s", nm_device_get_ip_iface(device)); + priv->teamd_dbus_watch = g_bus_watch_name(G_BUS_TYPE_SYSTEM, + tmp_str, + G_BUS_NAME_WATCHER_FLAGS_NONE, + teamd_dbus_appeared, + teamd_dbus_vanished, + NM_DEVICE(device), + NULL); + return; + } + + /* No D-Bus, watch unix socket */ + tmp_str = g_strdup_printf("/run/teamd/%s.sock", nm_device_get_ip_iface(device)); + file = g_file_new_for_path(tmp_str); + priv->usock_monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, &error); + if (!priv->usock_monitor) { + nm_log_warn(LOGD_TEAM, "error monitoring %s: %s", tmp_str, error->message); + } else { + g_signal_connect(priv->usock_monitor, "changed", G_CALLBACK(monitor_changed_cb), object); + } +} + +NMDevice * +nm_device_team_new(const char *iface) +{ + return g_object_new(NM_TYPE_DEVICE_TEAM, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_DRIVER, + "team", + NM_DEVICE_TYPE_DESC, + "Team", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_TEAM, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_TEAM, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceTeam * self = NM_DEVICE_TEAM(object); + NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self); + + if (priv->teamd_dbus_watch) { + g_bus_unwatch_name(priv->teamd_dbus_watch); + priv->teamd_dbus_watch = 0; + } + + if (priv->usock_monitor) { + g_signal_handlers_disconnect_by_data(priv->usock_monitor, object); + g_clear_object(&priv->usock_monitor); + } + + teamd_cleanup(self, TRUE); + nm_clear_g_free(&priv->config); + + G_OBJECT_CLASS(nm_device_team_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_team = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_TEAM, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves", "ao", NM_DEVICE_SLAVES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Config", + "s", + NM_DEVICE_TEAM_CONFIG), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_team_class_init(NMDeviceTeamClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->dispose = dispose; + object_class->get_property = get_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_team); + + device_class->connection_type_supported = NM_SETTING_TEAM_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_TEAM_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_TEAM); + + device_class->is_master = TRUE; + device_class->create_and_realize = create_and_realize; + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->complete_connection = complete_connection; + device_class->update_connection = update_connection; + device_class->master_update_slave_connection = master_update_slave_connection; + + device_class->act_stage1_prepare_also_for_external_or_assume = TRUE; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired; + device_class->deactivate = deactivate; + device_class->enslave_slave = enslave_slave; + device_class->release_slave = release_slave; + + obj_properties[PROP_CONFIG] = g_param_spec_string(NM_DEVICE_TEAM_CONFIG, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/team/nm-device-team.h b/src/core/devices/team/nm-device-team.h new file mode 100644 index 00000000..6f5cff93 --- /dev/null +++ b/src/core/devices/team/nm-device-team.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 Jiri Pirko <jiri@resnulli.us> + */ + +#ifndef __NETWORKMANAGER_DEVICE_TEAM_H__ +#define __NETWORKMANAGER_DEVICE_TEAM_H__ + +#include "devices/nm-device.h" + +#define NM_TYPE_DEVICE_TEAM (nm_device_team_get_type()) +#define NM_DEVICE_TEAM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeam)) +#define NM_DEVICE_TEAM_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TEAM, NMDeviceTeamClass)) +#define NM_IS_DEVICE_TEAM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TEAM)) +#define NM_IS_DEVICE_TEAM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_TEAM)) +#define NM_DEVICE_TEAM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeamClass)) + +/* Properties */ +#define NM_DEVICE_TEAM_CONFIG "config" + +typedef struct _NMDeviceTeam NMDeviceTeam; +typedef struct _NMDeviceTeamClass NMDeviceTeamClass; + +GType nm_device_team_get_type(void); + +NMDevice *nm_device_team_new(const char *iface); + +#endif /* __NETWORKMANAGER_DEVICE_TEAM_H__ */ diff --git a/src/core/devices/team/nm-team-factory.c b/src/core/devices/team/nm-team-factory.c new file mode 100644 index 00000000..57b51bf8 --- /dev/null +++ b/src/core/devices/team/nm-team-factory.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <gmodule.h> + +#include "nm-manager.h" +#include "devices/nm-device-factory.h" +#include "nm-device-team.h" +#include "platform/nm-platform.h" +#include "nm-core-internal.h" + +/*****************************************************************************/ + +#define NM_TYPE_TEAM_FACTORY (nm_team_factory_get_type()) +#define NM_TEAM_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactory)) +#define NM_TEAM_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_TEAM_FACTORY, NMTeamFactoryClass)) +#define NM_IS_TEAM_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_TEAM_FACTORY)) +#define NM_IS_TEAM_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_TEAM_FACTORY)) +#define NM_TEAM_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactoryClass)) + +typedef struct { + NMDeviceFactory parent; +} NMTeamFactory; + +typedef struct { + NMDeviceFactoryClass parent; +} NMTeamFactoryClass; + +static GType nm_team_factory_get_type(void); + +G_DEFINE_TYPE(NMTeamFactory, nm_team_factory, NM_TYPE_DEVICE_FACTORY) + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES( + NM_LINK_TYPE_TEAM) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_TEAM_SETTING_NAME)) + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + nm_manager_set_capability(NM_MANAGER_GET, NM_CAPABILITY_TEAM); + return g_object_new(NM_TYPE_TEAM_FACTORY, NULL); +} + +/*****************************************************************************/ + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + return nm_device_team_new(iface); +} + +/*****************************************************************************/ + +static void +nm_team_factory_init(NMTeamFactory *self) +{} + +static void +nm_team_factory_class_init(NMTeamFactoryClass *klass) +{ + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + factory_class->create_device = create_device; + factory_class->get_supported_types = get_supported_types; +} diff --git a/src/core/devices/tests/meson.build b/src/core/devices/tests/meson.build new file mode 100644 index 00000000..1bc88370 --- /dev/null +++ b/src/core/devices/tests/meson.build @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +test_units = [ + 'test-acd', + 'test-lldp', +] + +foreach test_unit: test_units + exe = executable( + test_unit, + test_unit + '.c', + dependencies: libNetworkManagerTest_dep, + c_args: test_c_flags, + ) + + test( + 'devices/' + test_unit, + test_script, + args: test_args + [exe.full_path()], + timeout: default_test_timeout, + ) +endforeach diff --git a/src/core/devices/tests/test-acd.c b/src/core/devices/tests/test-acd.c new file mode 100644 index 00000000..b0af59db --- /dev/null +++ b/src/core/devices/tests/test-acd.c @@ -0,0 +1,259 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "n-acd/src/n-acd.h" + +#include <linux/if_ether.h> + +#include "devices/nm-acd-manager.h" +#include "platform/tests/test-common.h" + +#define IFACE_VETH0 "nm-test-veth0" +#define IFACE_VETH1 "nm-test-veth1" + +#define ADDR1 0x01010101 +#define ADDR2 0x02020202 +#define ADDR3 0x03030303 +#define ADDR4 0x04040404 + +/*****************************************************************************/ + +static gboolean +_skip_acd_test_check(void) +{ + NAcd * acd; + NAcdConfig * config; + const guint8 hwaddr[ETH_ALEN] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + int r; + static int skip = -1; + + if (skip == -1) { + r = n_acd_config_new(&config); + g_assert(r == 0); + + n_acd_config_set_ifindex(config, 1); + n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET); + n_acd_config_set_mac(config, hwaddr, sizeof(hwaddr)); + + r = n_acd_new(&acd, config); + n_acd_config_free(config); + if (r == 0) + n_acd_unref(acd); + + skip = (r != 0); + } + return skip; +} + +#define _skip_acd_test() \ + ({ \ + gboolean _skip = _skip_acd_test_check(); \ + \ + if (_skip) \ + g_test_skip("Cannot create NAcd. Running under valgind?"); \ + _skip; \ + }) + +/*****************************************************************************/ + +typedef struct { + int ifindex0; + int ifindex1; + const guint8 *hwaddr0; + const guint8 *hwaddr1; + size_t hwaddr0_len; + size_t hwaddr1_len; +} test_fixture; + +static void +fixture_setup(test_fixture *fixture, gconstpointer user_data) +{ + /* create veth pair. */ + fixture->ifindex0 = + nmtstp_link_veth_add(NM_PLATFORM_GET, -1, IFACE_VETH0, IFACE_VETH1)->ifindex; + fixture->ifindex1 = + nmtstp_link_get_typed(NM_PLATFORM_GET, -1, IFACE_VETH1, NM_LINK_TYPE_VETH)->ifindex; + + g_assert(nm_platform_link_set_up(NM_PLATFORM_GET, fixture->ifindex0, NULL)); + g_assert(nm_platform_link_set_up(NM_PLATFORM_GET, fixture->ifindex1, NULL)); + + fixture->hwaddr0 = + nm_platform_link_get_address(NM_PLATFORM_GET, fixture->ifindex0, &fixture->hwaddr0_len); + fixture->hwaddr1 = + nm_platform_link_get_address(NM_PLATFORM_GET, fixture->ifindex1, &fixture->hwaddr1_len); +} + +typedef struct { + in_addr_t addresses[8]; + in_addr_t peer_addresses[8]; + gboolean expected_result[8]; +} TestInfo; + +static void +acd_manager_probe_terminated(NMAcdManager *acd_manager, gpointer user_data) +{ + g_main_loop_quit(user_data); +} + +static void +test_acd_common(test_fixture *fixture, TestInfo *info) +{ + nm_auto_free_acdmgr NMAcdManager *manager = NULL; + nm_auto_unref_gmainloop GMainLoop *loop = NULL; + int i; + const guint WAIT_TIME_OPTIMISTIC = 50; + guint wait_time; + static const NMAcdCallbacks callbacks = { + .probe_terminated_callback = acd_manager_probe_terminated, + .user_data_destroy = (GDestroyNotify) g_main_loop_unref, + }; + int r; + + if (_skip_acd_test()) + return; + + /* first, try with a short waittime. We hope that this is long enough + * to successfully complete the test. Only if that's not the case, we + * assume the computer is currently busy (high load) and we retry with + * a longer timeout. */ + wait_time = WAIT_TIME_OPTIMISTIC; +again: + + nm_clear_pointer(&loop, g_main_loop_unref); + loop = g_main_loop_new(NULL, FALSE); + + nm_clear_pointer(&manager, nm_acd_manager_free); + manager = nm_acd_manager_new(fixture->ifindex0, + fixture->hwaddr0, + fixture->hwaddr0_len, + &callbacks, + g_main_loop_ref(loop)); + g_assert(manager != NULL); + + for (i = 0; info->addresses[i]; i++) + g_assert(nm_acd_manager_add_address(manager, info->addresses[i])); + + for (i = 0; info->peer_addresses[i]; i++) { + nmtstp_ip4_address_add(NULL, + FALSE, + fixture->ifindex1, + info->peer_addresses[i], + 24, + 0, + 3600, + 1800, + 0, + NULL); + } + + r = nm_acd_manager_start_probe(manager, wait_time); + g_assert_cmpint(r, ==, 0); + + g_assert(nmtst_main_loop_run(loop, 2000)); + + for (i = 0; info->addresses[i]; i++) { + gboolean val; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + val = nm_acd_manager_check_address(manager, info->addresses[i]); + if (val == info->expected_result[i]) + continue; + + if (wait_time == WAIT_TIME_OPTIMISTIC) { + /* probably we just had a glitch and the system took longer than + * expected. Re-verify with a large timeout this time. */ + wait_time = 1000; + goto again; + } + + g_error("expected check for address #%d (%s) to %s, but it didn't", + i, + _nm_utils_inet4_ntop(info->addresses[i], sbuf), + info->expected_result[i] ? "detect no duplicated" : "detect a duplicate"); + } +} + +static void +test_acd_probe_1(test_fixture *fixture, gconstpointer user_data) +{ + TestInfo info = {.addresses = {ADDR1, ADDR2, ADDR3}, + .peer_addresses = {ADDR4}, + .expected_result = {TRUE, TRUE, TRUE}}; + + test_acd_common(fixture, &info); +} + +static void +test_acd_probe_2(test_fixture *fixture, gconstpointer user_data) +{ + TestInfo info = {.addresses = {ADDR1, ADDR2, ADDR3, ADDR4}, + .peer_addresses = {ADDR3, ADDR2}, + .expected_result = {TRUE, FALSE, FALSE, TRUE}}; + + test_acd_common(fixture, &info); +} + +static void +test_acd_announce(test_fixture *fixture, gconstpointer user_data) +{ + nm_auto_free_acdmgr NMAcdManager *manager = NULL; + nm_auto_unref_gmainloop GMainLoop *loop = NULL; + int r; + + if (_skip_acd_test()) + return; + + manager = + nm_acd_manager_new(fixture->ifindex0, fixture->hwaddr0, fixture->hwaddr0_len, NULL, NULL); + g_assert(manager != NULL); + + g_assert(nm_acd_manager_add_address(manager, ADDR1)); + g_assert(nm_acd_manager_add_address(manager, ADDR2)); + + loop = g_main_loop_new(NULL, FALSE); + r = nm_acd_manager_announce_addresses(manager); + g_assert_cmpint(r, ==, 0); + g_assert(!nmtst_main_loop_run(loop, 200)); +} + +static void +fixture_teardown(test_fixture *fixture, gconstpointer user_data) +{ + nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex0); + nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex1); +} + +NMTstpSetupFunc const _nmtstp_setup_platform_func = nm_linux_platform_setup; + +void +_nmtstp_init_tests(int *argc, char ***argv) +{ + nmtst_init_with_logging(argc, argv, NULL, "ALL"); +} + +void +_nmtstp_setup_tests(void) +{ + g_test_add("/acd/probe/1", + test_fixture, + NULL, + fixture_setup, + test_acd_probe_1, + fixture_teardown); + g_test_add("/acd/probe/2", + test_fixture, + NULL, + fixture_setup, + test_acd_probe_2, + fixture_teardown); + g_test_add("/acd/announce", + test_fixture, + NULL, + fixture_setup, + test_acd_announce, + fixture_teardown); +} diff --git a/src/core/devices/tests/test-lldp.c b/src/core/devices/tests/test-lldp.c new file mode 100644 index 00000000..ef0b1549 --- /dev/null +++ b/src/core/devices/tests/test-lldp.c @@ -0,0 +1,1316 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2015 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <fcntl.h> +#include <netinet/if_ether.h> +#include <linux/if_tun.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "devices/nm-lldp-listener.h" +#include "systemd/nm-sd.h" + +#include "platform/tests/test-common.h" + +#include "nm-test-utils-core.h" + +/*****************************************************************************/ + +static GVariant * +get_lldp_neighbor(GVariant * neighbors, + int chassis_id_type, + const char *chassis_id, + int port_id_type, + const char *port_id) +{ + GVariantIter iter; + GVariant * variant; + GVariant * result = NULL; + + nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}")); + + g_assert(chassis_id_type >= -1 && chassis_id_type <= G_MAXUINT8); + g_assert(port_id_type >= -1 && port_id_type <= G_MAXUINT8); + + g_variant_iter_init(&iter, neighbors); + while (g_variant_iter_next(&iter, "@a{sv}", &variant)) { + gs_unref_variant GVariant *v_chassis_id_type = NULL; + gs_unref_variant GVariant *v_chassis_id = NULL; + gs_unref_variant GVariant *v_port_id_type = NULL; + gs_unref_variant GVariant *v_port_id = NULL; + + v_chassis_id_type = + g_variant_lookup_value(variant, NM_LLDP_ATTR_CHASSIS_ID_TYPE, G_VARIANT_TYPE_UINT32); + g_assert(v_chassis_id_type); + + v_chassis_id = + g_variant_lookup_value(variant, NM_LLDP_ATTR_CHASSIS_ID, G_VARIANT_TYPE_STRING); + g_assert(v_chassis_id); + + v_port_id_type = + g_variant_lookup_value(variant, NM_LLDP_ATTR_PORT_ID_TYPE, G_VARIANT_TYPE_UINT32); + g_assert(v_port_id_type); + + v_port_id = g_variant_lookup_value(variant, NM_LLDP_ATTR_PORT_ID, G_VARIANT_TYPE_STRING); + g_assert(v_port_id); + + if (nm_streq(g_variant_get_string(v_chassis_id, NULL), chassis_id) + && nm_streq(g_variant_get_string(v_port_id, NULL), port_id) + && NM_IN_SET(chassis_id_type, -1, g_variant_get_uint32(v_chassis_id_type)) + && NM_IN_SET(port_id_type, -1, g_variant_get_uint32(v_port_id_type))) { + g_assert(!result); + result = variant; + } else + g_variant_unref(variant); + } + + return result; +} + +typedef struct { + int ifindex; + int fd; + guint8 mac[ETH_ALEN]; +} TestRecvFixture; + +typedef struct { + gsize frame_len; + const uint8_t *frame; + const char * as_variant; +} TestRecvFrame; +#define TEST_RECV_FRAME_DEFINE(name, _as_variant, ...) \ + static const guint8 _##name##_v[] = {__VA_ARGS__}; \ + static const TestRecvFrame name = { \ + .as_variant = _as_variant, \ + .frame_len = sizeof(_##name##_v), \ + .frame = _##name##_v, \ + } + +typedef struct { + guint expected_num_called; + gsize frames_len; + const TestRecvFrame *frames[10]; + void (*check)(GMainLoop *loop, NMLldpListener *listener); +} TestRecvData; +#define TEST_RECV_DATA_DEFINE(name, _expected_num_called, _check, ...) \ + static const TestRecvData name = { \ + .expected_num_called = _expected_num_called, \ + .check = _check, \ + .frames_len = NM_NARG(__VA_ARGS__), \ + .frames = {__VA_ARGS__}, \ + } + +#define TEST_IFNAME "nm-tap-test0" + +TEST_RECV_FRAME_DEFINE( + _test_recv_data0_frame0, + "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x88, " + "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x04, 0x05, 0x31, 0x2f, " + "0x33, 0x06, 0x02, 0x00, 0x78, 0x08, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x0a, 0x03, 0x53, 0x59, " + "0x53, 0x0c, 0x04, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, " + "'chassis-id': <'00:01:02:03:04:05'>, 'port-id-type': <uint32 5>, 'port-id': <'1/3'>, " + "'destination': <'nearest-non-tpmr-bridge'>, 'port-description': <'Port'>, 'system-name': " + "<'SYS'>, 'system-description': <'foo'>}", + /* Ethernet header */ + 0x01, + 0x80, + 0xc2, + 0x00, + 0x00, + 0x03, /* Destination MAC */ + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, /* Source MAC */ + 0x88, + 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, + 0x07, + 0x04, + 0x00, + 0x01, + 0x02, /* Chassis: MAC, 00:01:02:03:04:05 */ + 0x03, + 0x04, + 0x05, + 0x04, + 0x04, + 0x05, + 0x31, + 0x2f, + 0x33, /* Port: interface name, "1/3" */ + 0x06, + 0x02, + 0x00, + 0x78, /* TTL: 120 seconds */ + /* LLDP optional TLVs */ + 0x08, + 0x04, + 0x50, + 0x6f, + 0x72, + 0x74, /* Port Description: "Port" */ + 0x0a, + 0x03, + 0x53, + 0x59, + 0x53, /* System Name: "SYS" */ + 0x0c, + 0x04, + 0x66, + 0x6f, + 0x6f, + 0x00, /* System Description: "foo" (NULL-terminated) */ + 0x00, + 0x00 /* End Of LLDPDU */ +); + +static void +_test_recv_data0_check_do(GMainLoop *loop, NMLldpListener *listener, const TestRecvFrame *frame) +{ + GVariant * neighbors, *attr; + gs_unref_variant GVariant *neighbor = NULL; + + neighbors = nm_lldp_listener_get_neighbors(listener); + nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}")); + g_assert_cmpint(g_variant_n_children(neighbors), ==, 1); + + neighbor = get_lldp_neighbor(neighbors, + SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS, + "00:01:02:03:04:05", + SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME, + "1/3"); + g_assert(neighbor); + g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 4); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING); + nmtst_assert_variant_bytestring(attr, frame->frame, frame->frame_len); + nm_clear_g_variant(&attr); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_PORT_DESCRIPTION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "Port"); + nm_clear_g_variant(&attr); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_NAME, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "SYS"); + nm_clear_g_variant(&attr); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_DESTINATION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE); + nm_clear_g_variant(&attr); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "foo"); + nm_clear_g_variant(&attr); +} + +static void +_test_recv_data0_check(GMainLoop *loop, NMLldpListener *listener) +{ + _test_recv_data0_check_do(loop, listener, &_test_recv_data0_frame0); +} + +TEST_RECV_DATA_DEFINE(_test_recv_data0, 1, _test_recv_data0_check, &_test_recv_data0_frame0); +TEST_RECV_DATA_DEFINE(_test_recv_data0_twice, + 1, + _test_recv_data0_check, + &_test_recv_data0_frame0, + &_test_recv_data0_frame0); + +TEST_RECV_FRAME_DEFINE( + _test_recv_data1_frame0, + /* lldp.detailed.pcap from + * https://wiki.wireshark.org/SampleCaptures#Link_Layer_Discovery_Protocol_.28LLDP.29 */ + "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x01, 0x30, 0xf9, 0xad, 0xa0, 0x88, " + "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x30, 0xf9, 0xad, 0xa0, 0x04, 0x04, 0x05, 0x31, 0x2f, " + "0x31, 0x06, 0x02, 0x00, 0x78, 0x08, 0x17, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, " + "0x30, 0x2d, 0x34, 0x38, 0x2d, 0x50, 0x6f, 0x72, 0x74, 0x20, 0x31, 0x30, 0x30, 0x31, 0x00, " + "0x0a, 0x0d, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, 0x30, 0x2d, 0x34, 0x38, 0x00, " + "0x0c, 0x4c, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, 0x30, 0x2d, 0x34, 0x38, 0x20, " + "0x2d, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x37, 0x2e, 0x34, 0x65, 0x2e, " + "0x31, 0x20, 0x28, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x20, 0x35, 0x29, 0x20, 0x62, 0x79, 0x20, " + "0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, " + "0x30, 0x35, 0x2f, 0x32, 0x37, 0x2f, 0x30, 0x35, 0x20, 0x30, 0x34, 0x3a, 0x35, 0x33, 0x3a, " + "0x31, 0x31, 0x00, 0x0e, 0x04, 0x00, 0x14, 0x00, 0x14, 0x10, 0x0e, 0x07, 0x06, 0x00, 0x01, " + "0x30, 0xf9, 0xad, 0xa0, 0x02, 0x00, 0x00, 0x03, 0xe9, 0x00, 0xfe, 0x07, 0x00, 0x12, 0x0f, " + "0x02, 0x07, 0x01, 0x00, 0xfe, 0x09, 0x00, 0x12, 0x0f, 0x01, 0x03, 0x6c, 0x00, 0x00, 0x10, " + "0xfe, 0x09, 0x00, 0x12, 0x0f, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x00, 0x12, " + "0x0f, 0x04, 0x05, 0xf2, 0xfe, 0x06, 0x00, 0x80, 0xc2, 0x01, 0x01, 0xe8, 0xfe, 0x07, 0x00, " + "0x80, 0xc2, 0x02, 0x01, 0x00, 0x00, 0xfe, 0x16, 0x00, 0x80, 0xc2, 0x03, 0x01, 0xe8, 0x0f, " + "0x76, 0x32, 0x2d, 0x30, 0x34, 0x38, 0x38, 0x2d, 0x30, 0x33, 0x2d, 0x30, 0x35, 0x30, 0x35, " + "0xfe, 0x05, 0x00, 0x80, 0xc2, 0x04, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, " + "'chassis-id': <'00:01:30:F9:AD:A0'>, 'port-id-type': <uint32 5>, 'port-id': <'1/1'>, " + "'destination': <'nearest-bridge'>, 'port-description': <'Summit300-48-Port 1001'>, " + "'system-name': <'Summit300-48'>, 'system-description': <'Summit300-48 - Version 7.4e.1 (Build " + "5) by Release_Master 05/27/05 04:53:11'>, 'system-capabilities': <uint32 20>, " + "'management-addresses': <[{'address-subtype': <uint32 6>, 'address': <[byte 0x00, 0x01, 0x30, " + "0xf9, 0xad, 0xa0]>, 'interface-number-subtype': <uint32 2>, 'interface-number': <uint32 " + "1001>}]>, 'ieee-802-1-pvid': <uint32 488>, 'ieee-802-1-ppvid': <uint32 0>, " + "'ieee-802-1-ppvid-flags': <uint32 1>, 'ieee-802-1-ppvids': <[{'ppvid': <uint32 0>, 'flags': " + "<uint32 1>}]>, 'ieee-802-1-vid': <uint32 488>, 'ieee-802-1-vlan-name': <'v2-0488-03-0505'>, " + "'ieee-802-1-vlans': <[{'vid': <uint32 488>, 'name': <'v2-0488-03-0505'>}]>, " + "'ieee-802-3-mac-phy-conf': <{'autoneg': <uint32 3>, 'pmd-autoneg-cap': <uint32 27648>, " + "'operational-mau-type': <uint32 16>}>, 'ieee-802-3-power-via-mdi': <{'mdi-power-support': " + "<uint32 7>, 'pse-power-pair': <uint32 1>, 'power-class': <uint32 0>}>, " + "'ieee-802-3-max-frame-size': <uint32 1522>}", + /* ethernet header */ + 0x01, + 0x80, + 0xc2, + 0x00, + 0x00, + 0x0e, /* destination mac */ + 0x00, + 0x01, + 0x30, + 0xf9, + 0xad, + 0xa0, /* source mac */ + 0x88, + 0xcc, /* ethernet type */ + + 0x02, + 0x07, + 0x04, + 0x00, + 0x01, + 0x30, /* Chassis Subtype */ + 0xf9, + 0xad, + 0xa0, + 0x04, + 0x04, + 0x05, + 0x31, + 0x2f, + 0x31, /* Port Subtype */ + 0x06, + 0x02, + 0x00, + 0x78, /* Time To Live */ + 0x08, + 0x17, + 0x53, + 0x75, + 0x6d, + 0x6d, /* Port Description */ + 0x69, + 0x74, + 0x33, + 0x30, + 0x30, + 0x2d, + 0x34, + 0x38, + 0x2d, + 0x50, + 0x6f, + 0x72, + 0x74, + 0x20, + 0x31, + 0x30, + 0x30, + 0x31, + 0x00, + 0x0a, + 0x0d, + 0x53, + 0x75, + 0x6d, + 0x6d, /* System Name */ + 0x69, + 0x74, + 0x33, + 0x30, + 0x30, + 0x2d, + 0x34, + 0x38, + 0x00, + 0x0c, + 0x4c, + 0x53, + 0x75, + 0x6d, + 0x6d, /* System Description */ + 0x69, + 0x74, + 0x33, + 0x30, + 0x30, + 0x2d, + 0x34, + 0x38, + 0x20, + 0x2d, + 0x20, + 0x56, + 0x65, + 0x72, + 0x73, + 0x69, + 0x6f, + 0x6e, + 0x20, + 0x37, + 0x2e, + 0x34, + 0x65, + 0x2e, + 0x31, + 0x20, + 0x28, + 0x42, + 0x75, + 0x69, + 0x6c, + 0x64, + 0x20, + 0x35, + 0x29, + 0x20, + 0x62, + 0x79, + 0x20, + 0x52, + 0x65, + 0x6c, + 0x65, + 0x61, + 0x73, + 0x65, + 0x5f, + 0x4d, + 0x61, + 0x73, + 0x74, + 0x65, + 0x72, + 0x20, + 0x30, + 0x35, + 0x2f, + 0x32, + 0x37, + 0x2f, + 0x30, + 0x35, + 0x20, + 0x30, + 0x34, + 0x3a, + 0x35, + 0x33, + 0x3a, + 0x31, + 0x31, + 0x00, + 0x0e, + 0x04, + 0x00, + 0x14, + 0x00, + 0x14, /* Capabilities */ + 0x10, + 0x0e, + 0x07, + 0x06, + 0x00, + 0x01, /* Management Address */ + 0x30, + 0xf9, + 0xad, + 0xa0, + 0x02, + 0x00, + 0x00, + 0x03, + 0xe9, + 0x00, + 0xfe, + 0x07, + 0x00, + 0x12, + 0x0f, + 0x02, /* IEEE 802.3 - Power Via MDI */ + 0x07, + 0x01, + 0x00, + 0xfe, + 0x09, + 0x00, + 0x12, + 0x0f, + 0x01, /* IEEE 802.3 - MAC/PHY Configuration/Status */ + 0x03, + 0x6c, + 0x00, + 0x00, + 0x10, + 0xfe, + 0x09, + 0x00, + 0x12, + 0x0f, + 0x03, /* IEEE 802.3 - Link Aggregation */ + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xfe, + 0x06, + 0x00, + 0x12, + 0x0f, + 0x04, /* IEEE 802.3 - Maximum Frame Size */ + 0x05, + 0xf2, + 0xfe, + 0x06, + 0x00, + 0x80, + 0xc2, + 0x01, /* IEEE 802.1 - Port VLAN ID */ + 0x01, + 0xe8, + 0xfe, + 0x07, + 0x00, + 0x80, + 0xc2, + 0x02, /* IEEE 802.1 - Port and Protocol VLAN ID */ + 0x01, + 0x00, + 0x00, + 0xfe, + 0x16, + 0x00, + 0x80, + 0xc2, + 0x03, /* IEEE 802.1 - VLAN Name */ + 0x01, + 0xe8, + 0x0f, + 0x76, + 0x32, + 0x2d, + 0x30, + 0x34, + 0x38, + 0x38, + 0x2d, + 0x30, + 0x33, + 0x2d, + 0x30, + 0x35, + 0x30, + 0x35, + 0xfe, + 0x05, + 0x00, + 0x80, + 0xc2, + 0x04, /* IEEE 802.1 - Protocol Identity */ + 0x00, + 0x00, + 0x00 /* End of LLDPDU */ +); + +static void +_test_recv_data1_check(GMainLoop *loop, NMLldpListener *listener) +{ + GVariant * neighbors, *attr, *child; + gs_unref_variant GVariant *neighbor = NULL; + guint v_uint = 0; + const char * v_str = NULL; + + neighbors = nm_lldp_listener_get_neighbors(listener); + nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}")); + g_assert_cmpint(g_variant_n_children(neighbors), ==, 1); + + neighbor = get_lldp_neighbor(neighbors, + SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS, + "00:01:30:F9:AD:A0", + SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME, + "1/1"); + g_assert(neighbor); + g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 16); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING); + nmtst_assert_variant_bytestring(attr, + _test_recv_data1_frame0.frame, + _test_recv_data1_frame0.frame_len); + nm_clear_g_variant(&attr); + + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_DESTINATION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, NM_LLDP_DEST_NEAREST_BRIDGE); + nm_clear_g_variant(&attr); + + /* unsupported: Time To Live */ + + /* Port Description */ + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_PORT_DESCRIPTION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "Summit300-48-Port 1001"); + nm_clear_g_variant(&attr); + + /* System Name */ + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_NAME, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "Summit300-48"); + nm_clear_g_variant(&attr); + + /* System Description */ + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string( + attr, + "Summit300-48 - Version 7.4e.1 (Build 5) by Release_Master 05/27/05 04:53:11"); + nm_clear_g_variant(&attr); + + /* Capabilities */ + attr = + g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 20); + nm_clear_g_variant(&attr); + + /* Management Address */ + attr = g_variant_lookup_value(neighbor, + NM_LLDP_ATTR_MANAGEMENT_ADDRESSES, + G_VARIANT_TYPE("aa{sv}")); + g_assert(attr); + g_assert_cmpuint(g_variant_n_children(attr), ==, 1); + child = g_variant_get_child_value(attr, 0); + g_assert(child); + g_assert(g_variant_lookup(child, "interface-number", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 1001); + g_assert(g_variant_lookup(child, "interface-number-subtype", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 2); + g_assert(g_variant_lookup(child, "address-subtype", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 6); + nm_clear_g_variant(&child); + nm_clear_g_variant(&attr); + + /* IEEE 802.3 - Power Via MDI */ + attr = g_variant_lookup_value(neighbor, + NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI, + G_VARIANT_TYPE_VARDICT); + g_assert(attr); + g_assert(g_variant_lookup(attr, "mdi-power-support", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 7); + g_assert(g_variant_lookup(attr, "pse-power-pair", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 1); + g_assert(g_variant_lookup(attr, "power-class", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 0); + nm_clear_g_variant(&attr); + + /* IEEE 802.3 - MAC/PHY Configuration/Status */ + attr = g_variant_lookup_value(neighbor, + NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF, + G_VARIANT_TYPE_VARDICT); + g_assert(attr); + g_assert(g_variant_lookup(attr, "autoneg", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 3); + g_assert(g_variant_lookup(attr, "pmd-autoneg-cap", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 0x6c00); + g_assert(g_variant_lookup(attr, "operational-mau-type", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 16); + nm_clear_g_variant(&attr); + + /* unsupported: IEEE 802.3 - Link Aggregation */ + + /* Maximum Frame Size */ + attr = g_variant_lookup_value(neighbor, + NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE, + G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 1522); + nm_clear_g_variant(&attr); + + /* IEEE 802.1 - Port VLAN ID */ + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PVID, G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 488); + nm_clear_g_variant(&attr); + + /* IEEE 802.1 - Port and Protocol VLAN ID */ + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PPVID, G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 0); + nm_clear_g_variant(&attr); + attr = g_variant_lookup_value(neighbor, + NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS, + G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 1); + nm_clear_g_variant(&attr); + + /* new PPVID attributes */ + attr = + g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PPVIDS, G_VARIANT_TYPE("aa{sv}")); + g_assert_cmpuint(g_variant_n_children(attr), ==, 1); + child = g_variant_get_child_value(attr, 0); + g_assert(child); + g_assert(g_variant_lookup(child, "ppvid", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 0); + g_assert(g_variant_lookup(child, "flags", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 1); + nm_clear_g_variant(&child); + nm_clear_g_variant(&attr); + + /* IEEE 802.1 - VLAN Name */ + attr = + g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME, G_VARIANT_TYPE_STRING); + nmtst_assert_variant_string(attr, "v2-0488-03-0505"); + nm_clear_g_variant(&attr); + attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VID, G_VARIANT_TYPE_UINT32); + nmtst_assert_variant_uint32(attr, 488); + nm_clear_g_variant(&attr); + + /* new VLAN attributes */ + attr = + g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VLANS, G_VARIANT_TYPE("aa{sv}")); + g_assert_cmpuint(g_variant_n_children(attr), ==, 1); + child = g_variant_get_child_value(attr, 0); + g_assert(child); + g_assert(g_variant_lookup(child, "vid", "u", &v_uint)); + g_assert_cmpint(v_uint, ==, 488); + g_assert(g_variant_lookup(child, "name", "&s", &v_str)); + g_assert_cmpstr(v_str, ==, "v2-0488-03-0505"); + nm_clear_g_variant(&child); + nm_clear_g_variant(&attr); + + /* unsupported: IEEE 802.1 - Protocol Identity */ +} + +TEST_RECV_DATA_DEFINE(_test_recv_data1, 1, _test_recv_data1_check, &_test_recv_data1_frame0); + +TEST_RECV_FRAME_DEFINE( + _test_recv_data2_frame0_ttl1, + "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x88, " + "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x04, 0x05, 0x31, 0x2f, " + "0x33, 0x06, 0x02, 0x00, 0x01, 0x08, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x0a, 0x03, 0x53, 0x59, " + "0x53, 0x0c, 0x04, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, " + "'chassis-id': <'00:01:02:03:04:05'>, 'port-id-type': <uint32 5>, 'port-id': <'1/3'>, " + "'destination': <'nearest-non-tpmr-bridge'>, 'port-description': <'Port'>, 'system-name': " + "<'SYS'>, 'system-description': <'foo'>}", + /* Ethernet header */ + 0x01, + 0x80, + 0xc2, + 0x00, + 0x00, + 0x03, /* Destination MAC */ + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, /* Source MAC */ + 0x88, + 0xcc, /* Ethertype */ + /* LLDP mandatory TLVs */ + 0x02, + 0x07, + 0x04, + 0x00, + 0x01, + 0x02, /* Chassis: MAC, 00:01:02:03:04:05 */ + 0x03, + 0x04, + 0x05, + 0x04, + 0x04, + 0x05, + 0x31, + 0x2f, + 0x33, /* Port: interface name, "1/3" */ + 0x06, + 0x02, + 0x00, + 0x01, /* TTL: 1 seconds */ + /* LLDP optional TLVs */ + 0x08, + 0x04, + 0x50, + 0x6f, + 0x72, + 0x74, /* Port Description: "Port" */ + 0x0a, + 0x03, + 0x53, + 0x59, + 0x53, /* System Name: "SYS" */ + 0x0c, + 0x04, + 0x66, + 0x6f, + 0x6f, + 0x00, /* System Description: "foo" (NULL-terminated) */ + 0x00, + 0x00 /* End Of LLDPDU */ +); + +static void +_test_recv_data2_ttl1_check(GMainLoop *loop, NMLldpListener *listener) +{ + gulong notify_id; + GVariant *neighbors; + + _test_recv_data0_check_do(loop, listener, &_test_recv_data2_frame0_ttl1); + + /* wait for signal. */ + notify_id = g_signal_connect(listener, + "notify::" NM_LLDP_LISTENER_NEIGHBORS, + nmtst_main_loop_quit_on_notify, + loop); + if (!nmtst_main_loop_run(loop, 5000)) + g_assert_not_reached(); + nm_clear_g_signal_handler(listener, ¬ify_id); + + neighbors = nm_lldp_listener_get_neighbors(listener); + nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}")); + g_assert_cmpint(g_variant_n_children(neighbors), ==, 0); +} + +TEST_RECV_DATA_DEFINE(_test_recv_data2_ttl1, + 1, + _test_recv_data2_ttl1_check, + &_test_recv_data2_frame0_ttl1); + +static void +_test_recv_fixture_setup(TestRecvFixture *fixture, gconstpointer user_data) +{ + const NMPlatformLink *link; + nm_auto_close int fd = -1; + + fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC); + if (fd == -1) { + g_test_skip("Unable to open /dev/net/tun"); + fixture->ifindex = 0; + return; + } + + if (nmtst_get_rand_bool()) { + const NMPlatformLnkTun lnk = { + .type = IFF_TAP, + .pi = FALSE, + .vnet_hdr = FALSE, + .multi_queue = FALSE, + .persist = FALSE, + }; + + nm_close(nm_steal_fd(&fd)); + + link = nmtstp_link_tun_add(NM_PLATFORM_GET, FALSE, TEST_IFNAME, &lnk, &fd); + g_assert(link); + nmtstp_link_set_updown(NM_PLATFORM_GET, -1, link->ifindex, TRUE); + link = nmtstp_assert_wait_for_link(NM_PLATFORM_GET, TEST_IFNAME, NM_LINK_TYPE_TUN, 0); + } else { + int s; + struct ifreq ifr; + int r; + int try_cnt = 0; + +again: + memset(&ifr, 0, sizeof(ifr)); + ifr.ifr_flags = IFF_TAP | IFF_NO_PI; + nm_utils_ifname_cpy(ifr.ifr_name, TEST_IFNAME); + + r = ioctl(fd, TUNSETIFF, &ifr); + if (r != 0) { + if (errno == EPERM && try_cnt++ < 10) { + g_usleep(2); + goto again; + } + g_assert_cmpint(errno, ==, 0); + g_assert_cmpint(r, ==, 0); + } + + /* Bring the interface up */ + s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + g_assert(s >= 0); + + ifr.ifr_flags |= IFF_UP; + r = ioctl(s, SIOCSIFFLAGS, &ifr); + if (r != 0) { + g_assert_cmpint(errno, ==, 0); + g_assert_cmpint(r, ==, 0); + } + + nm_close(s); + + link = nmtstp_assert_wait_for_link(NM_PLATFORM_GET, TEST_IFNAME, NM_LINK_TYPE_TUN, 100); + } + + fixture->ifindex = link->ifindex; + fixture->fd = nm_steal_fd(&fd); + memcpy(fixture->mac, link->l_address.data, ETH_ALEN); +} + +typedef struct { + int num_called; +} TestRecvCallbackInfo; + +static void +lldp_neighbors_changed(NMLldpListener *lldp_listener, GParamSpec *pspec, gpointer user_data) +{ + TestRecvCallbackInfo *info = user_data; + + info->num_called++; +} + +static void +test_recv(TestRecvFixture *fixture, gconstpointer user_data) +{ + const TestRecvData *data = user_data; + gs_unref_object NMLldpListener *listener = NULL; + GMainLoop * loop; + TestRecvCallbackInfo info = {}; + gsize i_frames; + gulong notify_id; + GError * error = NULL; + guint sd_id; + + if (fixture->ifindex == 0) { + g_test_skip("Tun device not available"); + return; + } + + listener = nm_lldp_listener_new(); + g_assert(listener != NULL); + g_assert(nm_lldp_listener_start(listener, fixture->ifindex, &error)); + g_assert_no_error(error); + + notify_id = g_signal_connect(listener, + "notify::" NM_LLDP_LISTENER_NEIGHBORS, + (GCallback) lldp_neighbors_changed, + &info); + loop = g_main_loop_new(NULL, FALSE); + sd_id = nm_sd_event_attach_default(); + + for (i_frames = 0; i_frames < data->frames_len; i_frames++) { + const TestRecvFrame *f = data->frames[i_frames]; + + g_assert(write(fixture->fd, f->frame, f->frame_len) == f->frame_len); + } + + if (nmtst_main_loop_run(loop, 500)) + g_assert_not_reached(); + + g_assert_cmpint(info.num_called, ==, data->expected_num_called); + + nm_clear_g_signal_handler(listener, ¬ify_id); + + data->check(loop, listener); + + nm_clear_g_source(&sd_id); + nm_clear_pointer(&loop, g_main_loop_unref); +} + +static void +_test_recv_fixture_teardown(TestRecvFixture *fixture, gconstpointer user_data) +{ + if (fixture->ifindex) + nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex); +} + +/*****************************************************************************/ + +static void +test_parse_frames(gconstpointer test_data) +{ + const TestRecvFrame *frame = test_data; + gs_unref_variant GVariant *v_neighbor = NULL; + gs_unref_variant GVariant *attr = NULL; + gs_free char * as_variant = NULL; + + v_neighbor = nmtst_lldp_parse_from_raw(frame->frame, frame->frame_len); + g_assert(v_neighbor); + + attr = g_variant_lookup_value(v_neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING); + nmtst_assert_variant_bytestring(attr, frame->frame, frame->frame_len); + nm_clear_g_variant(&attr); + + as_variant = g_variant_print(v_neighbor, TRUE); + g_assert(as_variant); + g_assert_cmpstr(frame->as_variant, ==, as_variant); +} + +/*****************************************************************************/ + +TEST_RECV_FRAME_DEFINE( + _test_parse_frames_3, + /* https://github.com/the-tcpdump-group/tcpdump/blob/c4f8796bf8bec740621a360eded236d8991ea00f/tests/lldp_mudurl.pcap */ + "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x23, 0x54, 0xc2, 0x57, 0x02, 0x88, " + "0xcc, 0x02, 0x07, 0x04, 0x00, 0x23, 0x54, 0xc2, 0x57, 0x02, 0x04, 0x07, 0x03, 0x00, 0x23, " + "0x54, 0xc2, 0x57, 0x02, 0x06, 0x02, 0x00, 0x78, 0x0a, 0x1c, 0x75, 0x70, 0x73, 0x74, 0x61, " + "0x69, 0x72, 0x73, 0x2e, 0x6f, 0x66, 0x63, 0x6f, 0x75, 0x72, 0x73, 0x65, 0x69, 0x6d, 0x72, " + "0x69, 0x67, 0x68, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x5c, 0x55, 0x62, 0x75, 0x6e, 0x74, " + "0x75, 0x20, 0x31, 0x34, 0x2e, 0x30, 0x34, 0x2e, 0x35, 0x20, 0x4c, 0x54, 0x53, 0x20, 0x4c, " + "0x69, 0x6e, 0x75, 0x78, 0x20, 0x33, 0x2e, 0x31, 0x33, 0x2e, 0x30, 0x2d, 0x31, 0x30, 0x36, " + "0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x20, 0x23, 0x31, 0x35, 0x33, 0x2d, 0x55, " + "0x62, 0x75, 0x6e, 0x74, 0x75, 0x20, 0x53, 0x4d, 0x50, 0x20, 0x54, 0x75, 0x65, 0x20, 0x44, " + "0x65, 0x63, 0x20, 0x36, 0x20, 0x31, 0x35, 0x3a, 0x34, 0x35, 0x3a, 0x31, 0x33, 0x20, 0x55, " + "0x54, 0x43, 0x20, 0x32, 0x30, 0x31, 0x36, 0x20, 0x69, 0x36, 0x38, 0x36, 0x0e, 0x04, 0x00, " + "0x9c, 0x00, 0x08, 0x10, 0x0c, 0x05, 0x01, 0x3e, 0x0c, 0xad, 0x72, 0x02, 0x00, 0x00, 0x00, " + "0x02, 0x00, 0x10, 0x18, 0x11, 0x02, 0x20, 0x01, 0x08, 0xa8, 0x10, 0x06, 0x00, 0x04, 0x02, " + "0x23, 0x54, 0xff, 0xfe, 0xc2, 0x57, 0x02, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x04, " + "0x65, 0x74, 0x68, 0x30, 0xfe, 0x09, 0x00, 0x12, 0x0f, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, " + "0xfe, 0x09, 0x00, 0x12, 0x0f, 0x01, 0x03, 0xec, 0xc3, 0x00, 0x10, 0xfe, 0x40, 0x00, 0x00, " + "0x5e, 0x01, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x69, 0x6d, 0x72, 0x69, 0x67, " + "0x68, 0x74, 0x2e, 0x6d, 0x75, 0x64, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, " + "0x63, 0x6f, 0x6d, 0x2f, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, " + "0x2f, 0x6d, 0x75, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x6f, 0x6d, 0x69, 0x74, 0x76, 0x32, " + "0x2e, 0x30, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, 'chassis-id': <'00:23:54:C2:57:02'>, " + "'port-id-type': <uint32 3>, 'port-id': <'00:23:54:C2:57:02'>, 'destination': " + "<'nearest-bridge'>, 'port-description': <'eth0'>, 'system-name': " + "<'upstairs.ofcourseimright.com'>, 'system-description': <'Ubuntu 14.04.5 LTS Linux " + "3.13.0-106-generic #153-Ubuntu SMP Tue Dec 6 15:45:13 UTC 2016 i686'>, 'system-capabilities': " + "<uint32 156>, 'management-addresses': <[{'address-subtype': <uint32 1>, 'address': <[byte " + "0x3e, 0x0c, 0xad, 0x72]>, 'interface-number-subtype': <uint32 2>, 'interface-number': <uint32 " + "2>}, {'address-subtype': <uint32 2>, 'address': <[byte 0x20, 0x01, 0x08, 0xa8, 0x10, 0x06, " + "0x00, 0x04, 0x02, 0x23, 0x54, 0xff, 0xfe, 0xc2, 0x57, 0x02]>, 'interface-number-subtype': " + "<uint32 2>, 'interface-number': <uint32 2>}]>, 'ieee-802-3-mac-phy-conf': <{'autoneg': " + "<uint32 3>, 'pmd-autoneg-cap': <uint32 60611>, 'operational-mau-type': <uint32 16>}>, " + "'mud-url': <'https://imright.mud.example.com/.well-known/mud/v1/vomitv2.0'>}", + 0x01, + 0x80, + 0xc2, + 0x00, + 0x00, + 0x0e, /* ethernet destination */ + 0x00, + 0x23, + 0x54, + 0xc2, + 0x57, + 0x02, /* ethernet source */ + 0x88, + 0xcc, /* ethernet type */ + + 0x02, + 0x07, + 0x04, + 0x00, + 0x23, + 0x54, + 0xc2, + 0x57, + 0x02, + 0x04, + 0x07, + 0x03, + 0x00, + 0x23, + 0x54, + 0xc2, + 0x57, + 0x02, + 0x06, + 0x02, + 0x00, + 0x78, + 0x0a, + 0x1c, + 0x75, + 0x70, + 0x73, + 0x74, + 0x61, + 0x69, + 0x72, + 0x73, + 0x2e, + 0x6f, + 0x66, + 0x63, + 0x6f, + 0x75, + 0x72, + 0x73, + 0x65, + 0x69, + 0x6d, + 0x72, + 0x69, + 0x67, + 0x68, + 0x74, + 0x2e, + 0x63, + 0x6f, + 0x6d, + 0x0c, + 0x5c, + 0x55, + 0x62, + 0x75, + 0x6e, + 0x74, + 0x75, + 0x20, + 0x31, + 0x34, + 0x2e, + 0x30, + 0x34, + 0x2e, + 0x35, + 0x20, + 0x4c, + 0x54, + 0x53, + 0x20, + 0x4c, + 0x69, + 0x6e, + 0x75, + 0x78, + 0x20, + 0x33, + 0x2e, + 0x31, + 0x33, + 0x2e, + 0x30, + 0x2d, + 0x31, + 0x30, + 0x36, + 0x2d, + 0x67, + 0x65, + 0x6e, + 0x65, + 0x72, + 0x69, + 0x63, + 0x20, + 0x23, + 0x31, + 0x35, + 0x33, + 0x2d, + 0x55, + 0x62, + 0x75, + 0x6e, + 0x74, + 0x75, + 0x20, + 0x53, + 0x4d, + 0x50, + 0x20, + 0x54, + 0x75, + 0x65, + 0x20, + 0x44, + 0x65, + 0x63, + 0x20, + 0x36, + 0x20, + 0x31, + 0x35, + 0x3a, + 0x34, + 0x35, + 0x3a, + 0x31, + 0x33, + 0x20, + 0x55, + 0x54, + 0x43, + 0x20, + 0x32, + 0x30, + 0x31, + 0x36, + 0x20, + 0x69, + 0x36, + 0x38, + 0x36, + 0x0e, + 0x04, + 0x00, + 0x9c, + 0x00, + 0x08, + 0x10, + 0x0c, + 0x05, + 0x01, + 0x3e, + 0x0c, + 0xad, + 0x72, + 0x02, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x10, + 0x18, + 0x11, + 0x02, + 0x20, + 0x01, + 0x08, + 0xa8, + 0x10, + 0x06, + 0x00, + 0x04, + 0x02, + 0x23, + 0x54, + 0xff, + 0xfe, + 0xc2, + 0x57, + 0x02, + 0x02, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x08, + 0x04, + 0x65, + 0x74, + 0x68, + 0x30, + 0xfe, + 0x09, + 0x00, + 0x12, + 0x0f, + 0x03, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xfe, + 0x09, + 0x00, + 0x12, + 0x0f, + 0x01, + 0x03, + 0xec, + 0xc3, + 0x00, + 0x10, + 0xfe, + 0x40, + 0x00, + 0x00, + 0x5e, + 0x01, + 0x68, + 0x74, + 0x74, + 0x70, + 0x73, + 0x3a, + 0x2f, + 0x2f, + 0x69, + 0x6d, + 0x72, + 0x69, + 0x67, + 0x68, + 0x74, + 0x2e, + 0x6d, + 0x75, + 0x64, + 0x2e, + 0x65, + 0x78, + 0x61, + 0x6d, + 0x70, + 0x6c, + 0x65, + 0x2e, + 0x63, + 0x6f, + 0x6d, + 0x2f, + 0x2e, + 0x77, + 0x65, + 0x6c, + 0x6c, + 0x2d, + 0x6b, + 0x6e, + 0x6f, + 0x77, + 0x6e, + 0x2f, + 0x6d, + 0x75, + 0x64, + 0x2f, + 0x76, + 0x31, + 0x2f, + 0x76, + 0x6f, + 0x6d, + 0x69, + 0x74, + 0x76, + 0x32, + 0x2e, + + 0x30, + 0x00, + 0x00, /* ethernet trailer */ +); + +/*****************************************************************************/ + +NMTstpSetupFunc const _nmtstp_setup_platform_func = nm_linux_platform_setup; + +void +_nmtstp_init_tests(int *argc, char ***argv) +{ + nmtst_init_assert_logging(argc, argv, "WARN", "ALL"); +} + +void +_nmtstp_setup_tests(void) +{ +#define _TEST_ADD_RECV(testpath, testdata) \ + g_test_add(testpath, \ + TestRecvFixture, \ + testdata, \ + _test_recv_fixture_setup, \ + test_recv, \ + _test_recv_fixture_teardown) + _TEST_ADD_RECV("/lldp/recv/0", &_test_recv_data0); + _TEST_ADD_RECV("/lldp/recv/0_twice", &_test_recv_data0_twice); + _TEST_ADD_RECV("/lldp/recv/1", &_test_recv_data1); + _TEST_ADD_RECV("/lldp/recv/2_ttl1", &_test_recv_data2_ttl1); + + g_test_add_data_func("/lldp/parse-frames/0", &_test_recv_data0_frame0, test_parse_frames); + g_test_add_data_func("/lldp/parse-frames/1", &_test_recv_data1_frame0, test_parse_frames); + g_test_add_data_func("/lldp/parse-frames/2", &_test_recv_data2_frame0_ttl1, test_parse_frames); + g_test_add_data_func("/lldp/parse-frames/3", &_test_parse_frames_3, test_parse_frames); +} diff --git a/src/core/devices/wifi/meson.build b/src/core/devices/wifi/meson.build new file mode 100644 index 00000000..743937db --- /dev/null +++ b/src/core/devices/wifi/meson.build @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +iwd_sources = files() +if enable_iwd + iwd_sources += files( + 'nm-device-iwd.c', + 'nm-iwd-manager.c', + ) +endif + +libnm_device_plugin_wifi_static = static_library( + 'nm-device-plugin-wifi-static', + sources: files( + 'nm-device-olpc-mesh.c', + 'nm-device-wifi-p2p.c', + 'nm-device-wifi.c', + 'nm-wifi-ap.c', + 'nm-wifi-common.c', + 'nm-wifi-p2p-peer.c', + 'nm-wifi-utils.c', + ) + iwd_sources, + dependencies: [ + core_plugin_dep, + ], + c_args: daemon_c_flags, +) + +libnm_device_plugin_wifi_static_dep = declare_dependency( + link_with: libnm_device_plugin_wifi_static, +) + +libnm_device_plugin_wifi = shared_module( + 'nm-device-plugin-wifi', + sources: files( + 'nm-wifi-factory.c', + ), + dependencies: [ + core_plugin_dep, + libnm_device_plugin_wifi_static_dep + ], + c_args: daemon_c_flags, + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, +) + +core_plugins += libnm_device_plugin_wifi + +test( + 'check-local-devices-wifi', + check_exports, + args: [libnm_device_plugin_wifi.full_path(), linker_script_devices], +) + +if enable_tests + test_unit = 'test-devices-wifi' + + exe = executable( + test_unit, + 'tests/' + test_unit + '.c', + dependencies: [ + libNetworkManagerTest_dep, + libnm_device_plugin_wifi_static_dep, + ], + c_args: test_c_flags, + ) + + test( + test_unit, + test_script, + args: test_args + [exe.full_path()], + timeout: default_test_timeout, + ) +endif diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c new file mode 100644 index 00000000..f0de90d3 --- /dev/null +++ b/src/core/devices/wifi/nm-device-iwd.c @@ -0,0 +1,3508 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Intel Corporation + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-iwd.h" + +#include <linux/if_ether.h> + +#include "devices/nm-device-private.h" +#include "devices/nm-device.h" +#include "nm-act-request.h" +#include "nm-config.h" +#include "nm-core-internal.h" +#include "nm-dbus-manager.h" +#include "nm-glib-aux/nm-ref-string.h" +#include "nm-iwd-manager.h" +#include "nm-libnm-core-intern/nm-common-macros.h" +#include "nm-setting-8021x.h" +#include "nm-setting-connection.h" +#include "nm-setting-wireless-security.h" +#include "nm-setting-wireless.h" +#include "nm-std-aux/nm-dbus-compat.h" +#include "nm-utils.h" +#include "nm-wifi-common.h" +#include "nm-wifi-utils.h" +#include "settings/nm-settings-connection.h" +#include "settings/nm-settings.h" +#include "supplicant/nm-supplicant-types.h" +#include "nm-auth-utils.h" +#include "nm-manager.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceIwd +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIwd, + PROP_MODE, + PROP_BITRATE, + PROP_ACCESS_POINTS, + PROP_ACTIVE_ACCESS_POINT, + PROP_CAPABILITIES, + PROP_SCANNING, + PROP_LAST_SCAN, ); + +typedef struct { + GDBusObject * dbus_obj; + GDBusProxy * dbus_device_proxy; + GDBusProxy * dbus_station_proxy; + GDBusProxy * dbus_ap_proxy; + GDBusProxy * dbus_adhoc_proxy; + CList aps_lst_head; + NMWifiAP * current_ap; + GCancellable * cancellable; + NMDeviceWifiCapabilities capabilities; + NMActRequestGetSecretsCallId *wifi_secrets_id; + guint periodic_scan_id; + guint periodic_update_id; + bool enabled : 1; + bool can_scan : 1; + bool nm_autoconnect : 1; + bool iwd_autoconnect : 1; + bool scanning : 1; + bool scan_requested : 1; + bool act_mode_switch : 1; + bool secrets_failed : 1; + bool networks_requested : 1; + bool networks_changed : 1; + gint64 last_scan; + uint32_t ap_id; + guint32 rate; + NMEtherAddr current_ap_bssid; + GDBusMethodInvocation * pending_agent_request; + NMActiveConnection * assumed_ac; + guint assumed_ac_timeout; +} NMDeviceIwdPrivate; + +struct _NMDeviceIwd { + NMDevice parent; + NMDeviceIwdPrivate _priv; +}; + +struct _NMDeviceIwdClass { + NMDeviceClass parent; +}; + +/*****************************************************************************/ + +G_DEFINE_TYPE(NMDeviceIwd, nm_device_iwd, NM_TYPE_DEVICE) + +#define NM_DEVICE_IWD_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceIwd, NM_IS_DEVICE_IWD, NMDevice) + +/*****************************************************************************/ + +static void schedule_periodic_scan(NMDeviceIwd *self, gboolean initial_scan); + +static gboolean check_scanning_prohibited(NMDeviceIwd *self, gboolean periodic); + +/*****************************************************************************/ + +static void +_ap_dump(NMDeviceIwd *self, NMLogLevel log_level, const NMWifiAP *ap, const char *prefix) +{ + char buf[1024]; + + buf[0] = '\0'; + _NMLOG(log_level, + LOGD_WIFI_SCAN, + "wifi-ap: %-7s %s", + prefix, + nm_wifi_ap_to_string(ap, buf, sizeof(buf), 0)); +} + +/* Callers ensure we're not removing current_ap */ +static void +ap_add_remove(NMDeviceIwd *self, + gboolean is_adding, /* or else is removing */ + NMWifiAP * ap, + gboolean recheck_available_connections) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (is_adding) { + g_object_ref(ap); + ap->wifi_device = NM_DEVICE(self); + c_list_link_tail(&priv->aps_lst_head, &ap->aps_lst); + nm_dbus_object_export(NM_DBUS_OBJECT(ap)); + _ap_dump(self, LOGL_DEBUG, ap, "added"); + nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, TRUE); + } else { + ap->wifi_device = NULL; + c_list_unlink(&ap->aps_lst); + _ap_dump(self, LOGL_DEBUG, ap, "removed"); + } + + _notify(self, PROP_ACCESS_POINTS); + + if (!is_adding) { + nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, FALSE); + nm_dbus_object_clear_and_unexport(&ap); + } + + if (priv->enabled && !priv->iwd_autoconnect) + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + + if (recheck_available_connections) + nm_device_recheck_available_connections(NM_DEVICE(self)); +} + +static void +set_current_ap(NMDeviceIwd *self, NMWifiAP *new_ap, gboolean recheck_available_connections) +{ + NMDeviceIwdPrivate *priv; + NMWifiAP * old_ap; + + g_return_if_fail(NM_IS_DEVICE_IWD(self)); + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + old_ap = priv->current_ap; + + if (old_ap == new_ap) + return; + + if (new_ap) + priv->current_ap = g_object_ref(new_ap); + else + priv->current_ap = NULL; + + if (old_ap) { + if (nm_wifi_ap_get_fake(old_ap)) + ap_add_remove(self, FALSE, old_ap, recheck_available_connections); + g_object_unref(old_ap); + } + + memset(&priv->current_ap_bssid, 0, ETH_ALEN); + _notify(self, PROP_ACTIVE_ACCESS_POINT); + _notify(self, PROP_MODE); +} + +static void +remove_all_aps(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMWifiAP * ap, *ap_safe; + + if (c_list_is_empty(&priv->aps_lst_head)) + return; + + c_list_for_each_entry_safe (ap, ap_safe, &priv->aps_lst_head, aps_lst) + ap_add_remove(self, FALSE, ap, FALSE); + + if (!priv->iwd_autoconnect) + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + + nm_device_recheck_available_connections(NM_DEVICE(self)); +} + +static NM80211ApSecurityFlags +ap_security_flags_from_network_type(const char *type) +{ + NM80211ApSecurityFlags flags; + + if (nm_streq(type, "psk")) + flags = NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (nm_streq(type, "8021x")) + flags = NM_802_11_AP_SEC_KEY_MGMT_802_1X; + else + return NM_802_11_AP_SEC_NONE; + + flags |= NM_802_11_AP_SEC_PAIR_CCMP; + flags |= NM_802_11_AP_SEC_GROUP_CCMP; + return flags; +} + +static NMWifiAP * +ap_from_network(NMDeviceIwd *self, + GDBusProxy * network, + NMRefString *bss_path, + gint64 last_seen_msec, + int16_t signal) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + gs_unref_variant GVariant *name_value = NULL; + gs_unref_variant GVariant *type_value = NULL; + const char * name; + const char * type; + uint32_t ap_id; + gs_unref_bytes GBytes *ssid = NULL; + NMWifiAP * ap; + NMSupplicantBssInfo bss_info; + + g_return_val_if_fail(network, NULL); + + name_value = g_dbus_proxy_get_cached_property(network, "Name"); + type_value = g_dbus_proxy_get_cached_property(network, "Type"); + if (!name_value || !g_variant_is_of_type(name_value, G_VARIANT_TYPE_STRING) || !type_value + || !g_variant_is_of_type(type_value, G_VARIANT_TYPE_STRING)) + return NULL; + + name = g_variant_get_string(name_value, NULL); + type = g_variant_get_string(type_value, NULL); + + if (nm_streq(type, "wep")) { + /* WEP not supported */ + return NULL; + } + + /* What we get from IWD are networks, or ESSs, that may contain + * multiple APs, or BSSs, each. We don't get information about any + * specific BSSs within an ESS but we can safely present each ESS + * as an individual BSS to NM, which will be seen as ESSs comprising + * a single BSS each. NM won't be able to handle roaming but IWD + * already does that. We fake the BSSIDs as they don't play any + * role either. + */ + ap_id = priv->ap_id++; + + ssid = g_bytes_new(name, NM_MIN(32u, strlen(name))); + + bss_info = (NMSupplicantBssInfo){ + .bss_path = bss_path, + .last_seen_msec = last_seen_msec, + .bssid_valid = TRUE, + .mode = NM_802_11_MODE_INFRA, + .rsn_flags = ap_security_flags_from_network_type(type), + .ssid = ssid, + .signal_percent = nm_wifi_utils_level_to_quality(signal / 100), + .frequency = 2417, + .max_rate = 65000, + .bssid = NM_ETHER_ADDR_INIT(0x00, 0x01, 0x02, ap_id >> 16, ap_id >> 8, ap_id), + }; + + ap = nm_wifi_ap_new_from_properties(&bss_info); + + nm_assert(bss_path == nm_wifi_ap_get_supplicant_path(ap)); + + return ap; +} + +static void +insert_ap_from_network(NMDeviceIwd *self, + GHashTable * aps, + const char * path, + gint64 last_seen_msec, + int16_t signal) +{ + gs_unref_object GDBusProxy *network_proxy = NULL; + nm_auto_ref_string NMRefString *bss_path = nm_ref_string_new(path); + NMWifiAP * ap; + + if (g_hash_table_lookup(aps, bss_path)) { + _LOGD(LOGD_WIFI, "Duplicate network at %s", path); + return; + } + + network_proxy = + nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), path, NM_IWD_NETWORK_INTERFACE); + + ap = ap_from_network(self, network_proxy, bss_path, last_seen_msec, signal); + if (!ap) + return; + + g_hash_table_insert(aps, bss_path, ap); +} + +static void +get_ordered_networks_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *variant = NULL; + GVariantIter * networks; + const char * path; + int16_t signal; + NMWifiAP * ap, *ap_safe, *new_ap; + gboolean changed; + GHashTableIter ap_iter; + gs_unref_hashtable GHashTable *new_aps = NULL; + gint64 last_seen_msec; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant && nm_utils_error_is_cancelled(error)) + return; + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + priv->networks_requested = FALSE; + + if (!variant) { + _LOGE(LOGD_WIFI, "Station.GetOrderedNetworks failed: %s", error->message); + return; + } + + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE("(a(on))"))) { + _LOGE(LOGD_WIFI, + "Station.GetOrderedNetworks returned type %s instead of (a(on))", + g_variant_get_type_string(variant)); + return; + } + + new_aps = g_hash_table_new_full(nm_direct_hash, NULL, NULL, g_object_unref); + g_variant_get(variant, "(a(on))", &networks); + + last_seen_msec = nm_utils_get_monotonic_timestamp_msec(); + while (g_variant_iter_next(networks, "(&on)", &path, &signal)) + insert_ap_from_network(self, new_aps, path, last_seen_msec, signal); + + g_variant_iter_free(networks); + + changed = priv->networks_changed; + priv->networks_changed = FALSE; + + c_list_for_each_entry_safe (ap, ap_safe, &priv->aps_lst_head, aps_lst) { + new_ap = g_hash_table_lookup(new_aps, nm_wifi_ap_get_supplicant_path(ap)); + if (new_ap) { + if (nm_wifi_ap_set_strength(ap, nm_wifi_ap_get_strength(new_ap))) { + _ap_dump(self, LOGL_TRACE, ap, "updated"); + changed = TRUE; + } + g_hash_table_remove(new_aps, nm_wifi_ap_get_supplicant_path(ap)); + continue; + } + + if (ap == priv->current_ap) { + /* Normally IWD will prevent the current AP from being + * removed from the list and set a low signal strength, + * but just making sure. + */ + continue; + } + + ap_add_remove(self, FALSE, ap, FALSE); + changed = TRUE; + } + + g_hash_table_iter_init(&ap_iter, new_aps); + while (g_hash_table_iter_next(&ap_iter, NULL, (gpointer) &ap)) { + ap_add_remove(self, TRUE, ap, FALSE); + g_hash_table_iter_remove(&ap_iter); + changed = TRUE; + } + + if (changed) { + if (!priv->iwd_autoconnect) + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + + nm_device_recheck_available_connections(NM_DEVICE(self)); + } +} + +static void +update_aps(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (!priv->cancellable) + priv->cancellable = g_cancellable_new(); + + g_dbus_proxy_call(priv->dbus_station_proxy, + "GetOrderedNetworks", + NULL, + G_DBUS_CALL_FLAGS_NONE, + 2000, + priv->cancellable, + get_ordered_networks_cb, + self); + priv->networks_requested = TRUE; +} + +static void +periodic_update(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + int ifindex; + guint32 new_rate; + int percent; + NMEtherAddr bssid; + gboolean ap_changed = FALSE; + NMPlatform * platform; + + ifindex = nm_device_get_ifindex(NM_DEVICE(self)); + if (ifindex <= 0) + g_return_if_reached(); + + platform = nm_device_get_platform(NM_DEVICE(self)); + + /* TODO: obtain quality through the net.connman.iwd.SignalLevelAgent API. + * For now we're waking up for the rate/BSSID updates anyway. + */ + if (!nm_platform_wifi_get_station(platform, ifindex, &bssid, &percent, &new_rate)) { + _LOGD(LOGD_WIFI, "BSSID / quality / rate platform query failed"); + return; + } + + if (nm_wifi_ap_set_strength(priv->current_ap, (gint8) percent)) { +#if NM_MORE_LOGGING + ap_changed = TRUE; +#endif + } + + if (new_rate != priv->rate) { + priv->rate = new_rate; + _notify(self, PROP_BITRATE); + } + + if (nm_ether_addr_is_valid(&bssid) && !nm_ether_addr_equal(&bssid, &priv->current_ap_bssid)) { + priv->current_ap_bssid = bssid; + ap_changed |= nm_wifi_ap_set_address_bin(priv->current_ap, &bssid); + ap_changed |= nm_wifi_ap_set_freq(priv->current_ap, + nm_platform_wifi_get_frequency(platform, ifindex)); + } + + if (ap_changed) + _ap_dump(self, LOGL_DEBUG, priv->current_ap, "updated"); +} + +static gboolean +periodic_update_cb(gpointer user_data) +{ + periodic_update(user_data); + return TRUE; +} + +static void +send_disconnect(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + g_dbus_proxy_call(priv->dbus_station_proxy, + "Disconnect", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +static void +wifi_secrets_cancel(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (priv->wifi_secrets_id) + nm_act_request_cancel_secrets(NULL, priv->wifi_secrets_id); + nm_assert(!priv->wifi_secrets_id); + + if (priv->pending_agent_request) { + g_dbus_method_invocation_return_error_literal(priv->pending_agent_request, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "NM secrets request cancelled"); + g_clear_object(&priv->pending_agent_request); + } +} + +static void +cleanup_assumed_connect(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (!priv->assumed_ac) + return; + + g_signal_handlers_disconnect_by_data(priv->assumed_ac, self); + g_clear_object(&priv->assumed_ac); +} + +static void +cleanup_association_attempt(NMDeviceIwd *self, gboolean disconnect) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + cleanup_assumed_connect(self); + wifi_secrets_cancel(self); + + set_current_ap(self, NULL, TRUE); + nm_clear_g_source(&priv->periodic_update_id); + nm_clear_g_source(&priv->assumed_ac_timeout); + + if (disconnect && priv->dbus_station_proxy) + send_disconnect(self); +} + +static void +reset_mode(NMDeviceIwd * self, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + g_dbus_proxy_call( + priv->dbus_device_proxy, + DBUS_INTERFACE_PROPERTIES ".Set", + g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Mode", g_variant_new_string("station")), + G_DBUS_CALL_FLAGS_NONE, + 2000, + cancellable, + callback, + user_data); +} + +static gboolean +get_variant_boolean(GVariant *v, const char *property) +{ + if (!v || !g_variant_is_of_type(v, G_VARIANT_TYPE_BOOLEAN)) { + nm_log_warn(LOGD_DEVICE | LOGD_WIFI, + "Property %s not cached or not boolean type", + property); + + return FALSE; + } + + return g_variant_get_boolean(v); +} + +static const char * +get_variant_state(GVariant *v) +{ + if (!v || !g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) { + nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "State property not cached or not a string"); + + return "unknown"; + } + + return g_variant_get_string(v, NULL); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (!priv->dbus_obj) + return; + + if (priv->dbus_station_proxy) { + gs_unref_variant GVariant *value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + + if (NM_IN_STRSET(get_variant_state(value), "disconnecting", "disconnected")) + return; + } + + cleanup_association_attempt(self, TRUE); + priv->act_mode_switch = FALSE; + + if (!priv->dbus_station_proxy) + reset_mode(self, NULL, NULL, NULL); +} + +static void +disconnect_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + gs_unref_object NMDeviceIwd *self = NULL; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + + nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data); + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + callback(NM_DEVICE(self), error, callback_user_data); +} + +static void +disconnect_cb_on_idle(gpointer user_data, GCancellable *cancellable) +{ + gs_unref_object NMDeviceIwd *self = NULL; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + gs_free_error GError *cancelled_error = NULL; + + nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data); + + g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error); + callback(NM_DEVICE(self), cancelled_error, callback_user_data); +} + +static void +deactivate_async(NMDevice * device, + GCancellable * cancellable, + NMDeviceDeactivateCallback callback, + gpointer callback_user_data) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + gpointer user_data; + + nm_assert(G_IS_CANCELLABLE(cancellable)); + nm_assert(callback); + + user_data = nm_utils_user_data_pack(g_object_ref(self), callback, callback_user_data); + + if (!priv->dbus_obj) { + nm_utils_invoke_on_idle(cancellable, disconnect_cb_on_idle, user_data); + return; + } + + cleanup_association_attempt(self, FALSE); + priv->act_mode_switch = FALSE; + + if (priv->dbus_station_proxy) { + g_dbus_proxy_call(priv->dbus_station_proxy, + "Disconnect", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + disconnect_cb, + user_data); + } else + reset_mode(self, cancellable, disconnect_cb, user_data); +} + +static gboolean +is_connection_known_network(NMConnection *connection) +{ + NMIwdNetworkSecurity security; + gs_free char * ssid = NULL; + + if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, &security)) + return FALSE; + + return nm_iwd_manager_is_known_network(nm_iwd_manager_get(), ssid, security); +} + +static gboolean +is_ap_known_network(NMWifiAP *ap) +{ + gs_unref_object GDBusProxy *network_proxy = NULL; + gs_unref_variant GVariant *known_network = NULL; + + network_proxy = + nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), + nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)), + NM_IWD_NETWORK_INTERFACE); + if (!network_proxy) + return FALSE; + + known_network = g_dbus_proxy_get_cached_property(network_proxy, "KnownNetwork"); + return nm_g_variant_is_of_type(known_network, G_VARIANT_TYPE_OBJECT_PATH); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMSettingWireless * s_wireless; + const char * mac; + const char *const * mac_blacklist; + int i; + const char * perm_hw_addr; + const char * mode; + NMIwdNetworkSecurity security; + GBytes * ssid; + const guint8 * ssid_bytes; + gsize ssid_len; + + if (!NM_DEVICE_CLASS(nm_device_iwd_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_wireless = nm_connection_get_setting_wireless(connection); + + /* complete_connection would be called (if at all) before this function + * so an SSID should always be set. IWD doesn't support non-UTF8 SSIDs + * (ignores BSSes with such SSIDs and has no way to represent them on + * DBus) so we can cut it short for connections with a non-UTF8 SSID. + */ + ssid = nm_setting_wireless_get_ssid(s_wireless); + if (!ssid) + return FALSE; + + ssid_bytes = g_bytes_get_data(ssid, &ssid_len); + if (!g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "non-UTF-8 connection SSID not supported by IWD backend"); + return FALSE; + } + + perm_hw_addr = nm_device_get_permanent_hw_address(device); + mac = nm_setting_wireless_get_mac_address(s_wireless); + if (perm_hw_addr) { + if (mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "device MAC address does not match the profile"); + return FALSE; + } + + /* Check for MAC address blacklist */ + mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless); + for (i = 0; mac_blacklist[i]; i++) { + nm_assert(nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)); + + if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "MAC address blacklisted"); + return FALSE; + } + } + } else if (mac) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has no valid MAC address as required by profile"); + return FALSE; + } + + if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security) + || security == NM_IWD_NETWORK_SECURITY_WEP) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "connection authentication type not supported by IWD backend"); + return FALSE; + } + + mode = nm_setting_wireless_get_mode(s_wireless); + + /* Hidden SSIDs only supported in client mode */ + if (nm_setting_wireless_get_hidden(s_wireless) + && !NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) { + nm_utils_error_set_literal( + error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "non-infrastructure hidden networks not supported by the IWD backend"); + return FALSE; + } + + if (NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) { + /* 8021x networks can only be used if they've been provisioned on the IWD side and + * thus are Known Networks. + */ + if (security == NM_IWD_NETWORK_SECURITY_8021X) { + if (!is_connection_known_network(connection)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "802.1x connections must have IWD provisioning files"); + return FALSE; + } + } else if (!NM_IN_SET(security, + NM_IWD_NETWORK_SECURITY_OPEN, + NM_IWD_NETWORK_SECURITY_PSK)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "IWD backend only supports Open, PSK and 802.1x network " + "authentication in Infrastructure mode"); + return FALSE; + } + } else if (nm_streq(mode, NM_SETTING_WIRELESS_MODE_AP)) { + NMSettingWirelessSecurity *s_wireless_sec = + nm_connection_get_setting_wireless_security(connection); + + if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_AP)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "device does not support Access Point mode"); + return FALSE; + } + + if (!NM_IN_SET(security, NM_IWD_NETWORK_SECURITY_PSK) || !s_wireless_sec + || !nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec), "wpa-psk")) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "IWD backend only supports PSK authentication in AP mode"); + return FALSE; + } + } else if (nm_streq(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) { + NMSettingWirelessSecurity *s_wireless_sec = + nm_connection_get_setting_wireless_security(connection); + + if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_ADHOC)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "device does not support Ad-Hoc mode"); + return FALSE; + } + + if (!NM_IN_SET(security, NM_IWD_NETWORK_SECURITY_OPEN, NM_IWD_NETWORK_SECURITY_PSK) + || (s_wireless_sec + && !nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec), + "wpa-psk"))) { + nm_utils_error_set_literal( + error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "IWD backend only supports Open and PSK authentication in Ad-Hoc mode"); + return FALSE; + } + } else { + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "'%s' type profiles not supported by IWD backend", + mode); + return FALSE; + } + + return TRUE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMSettingWireless * s_wifi; + const char * mode; + NMWifiAP * ap = NULL; + NMIwdNetworkSecurity security; + + s_wifi = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wifi, FALSE); + + /* a connection that is available for a certain @specific_object, MUST + * also be available in general (without @specific_object). */ + + if (specific_object) { + ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object); + if (!ap) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "requested access point not found"); + return FALSE; + } + if (!nm_wifi_ap_check_compatible(ap, connection)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "requested access point is not compatible with profile"); + return FALSE; + } + } + + /* AP and Ad-Hoc connections can be activated independent of the scan list */ + mode = nm_setting_wireless_get_mode(s_wifi); + if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC)) + return TRUE; + + /* Hidden SSIDs obviously don't always appear in the scan list either. + * + * For an explicit user-activation-request, a connection is considered + * available because for hidden Wi-Fi, clients didn't consistently + * set the 'hidden' property to indicate hidden SSID networks. If + * activating but the network isn't available let the device recheck + * availability. + */ + if (nm_setting_wireless_get_hidden(s_wifi) + || NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP)) + return TRUE; + + if (!ap) + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + + if (!ap) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "no compatible access point found"); + return FALSE; + } + + /* 8021x networks can only be used if they've been provisioned on the IWD side and + * thus are Known Networks. + */ + if (nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security) + && security == NM_IWD_NETWORK_SECURITY_8021X) { + if (!is_ap_known_network(ap)) { + nm_utils_error_set_literal( + error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "802.1x network is not an IWD Known Network (missing provisioning file?)"); + return FALSE; + } + } + + return TRUE; +} + +/* To be used where the SSID has been validated before */ +static char * +iwd_ssid_to_str(const GBytes *ssid) +{ + const guint8 *ssid_bytes; + gsize ssid_len; + + ssid_bytes = g_bytes_get_data((GBytes *) ssid, &ssid_len); + nm_assert(ssid && g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL)); + return g_strndup((const char *) ssid_bytes, ssid_len); +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMSettingWireless * s_wifi; + gs_free char * ssid_utf8 = NULL; + NMWifiAP * ap; + GBytes * ssid = NULL; + gboolean hidden = FALSE; + const char * mode; + + s_wifi = nm_connection_get_setting_wireless(connection); + + mode = s_wifi ? nm_setting_wireless_get_mode(s_wifi) : NULL; + + if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP) || !specific_object) { + const guint8 *ssid_bytes; + gsize ssid_len; + + /* If not given a specific object, we need at minimum an SSID */ + if (!s_wifi) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'wireless' setting is required if no AP path was given."); + return FALSE; + } + + ssid = nm_setting_wireless_get_ssid(s_wifi); + ssid_bytes = g_bytes_get_data(ssid, &ssid_len); + + if (!ssid || ssid_len == 0 || !g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'wireless' setting with a valid UTF-8 SSID is required if no AP " + "path was given."); + return FALSE; + } + } + + if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) { + if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error)) + return FALSE; + ap = NULL; + } else if (!specific_object) { + /* Find a compatible AP in the scan list */ + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + if (!ap) { + /* If we still don't have an AP, then the WiFI settings needs to be + * fully specified by the client. Might not be able to find an AP + * if the network isn't broadcasting the SSID for example. + */ + if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error)) + return FALSE; + + /* We could either require the profile to be marked as hidden by the + * client or at least check that a hidden AP with a matching security + * type is in range using Station.GetHiddenAccessPoints(). For now + * assume it is hidden even though that will reveal the SSID on the + * air. + */ + hidden = TRUE; + } + } else { + ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object); + if (!ap) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND, + "The access point %s was not in the scan list.", + specific_object); + return FALSE; + } + + ssid = nm_wifi_ap_get_ssid(ap); + + /* Add a wifi setting if one doesn't exist yet */ + if (!s_wifi) { + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wifi)); + } + } + + if (ap) { + if (!nm_wifi_ap_complete_connection(ap, connection, FALSE, error)) + return FALSE; + } + + ssid_utf8 = iwd_ssid_to_str(ssid); + nm_utils_complete_generic( + nm_device_get_platform(device), + connection, + NM_SETTING_WIRELESS_SETTING_NAME, + existing_connections, + ssid_utf8, + ssid_utf8, + NULL, + nm_setting_wireless_get_mac_address(s_wifi) ? NULL : nm_device_get_iface(device), + TRUE); + + if (hidden) + g_object_set(s_wifi, NM_SETTING_WIRELESS_HIDDEN, TRUE, NULL); + + return TRUE; +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDeviceState state = nm_device_get_state(device); + + /* Available if either the device is UP and in station mode + * or in AP/Ad-Hoc modes while activating or activated. Device + * may be temporarily DOWN while activating or deactivating and + * we don't want it to be marked unavailable because of this. + * + * For reference: + * We call nm_device_queue_recheck_available whenever + * priv->enabled changes or priv->dbus_station_proxy changes. + */ + return priv->dbus_obj && priv->enabled + && (priv->dbus_station_proxy + || (state >= NM_DEVICE_STATE_CONFIG && state <= NM_DEVICE_STATE_DEACTIVATING)); +} + +static gboolean +get_autoconnect_allowed(NMDevice *device) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(NM_DEVICE_IWD(device)); + + return priv->nm_autoconnect; +} + +static gboolean +can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMConnection * connection; + NMSettingWireless * s_wifi; + NMWifiAP * ap; + const char * mode; + guint64 timestamp = 0; + + nm_assert(!specific_object || !*specific_object); + + if (!NM_DEVICE_CLASS(nm_device_iwd_parent_class)->can_auto_connect(device, sett_conn, NULL)) + return FALSE; + + connection = nm_settings_connection_get_connection(sett_conn); + + s_wifi = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wifi, FALSE); + + /* Don't auto-activate AP or Ad-Hoc connections. + * Note the wpa_supplicant backend has the opposite policy. + */ + mode = nm_setting_wireless_get_mode(s_wifi); + if (mode && g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_INFRA) != 0) + return FALSE; + + /* Don't autoconnect to networks that have been tried at least once + * but haven't been successful, since these are often accidental choices + * from the menu and the user may not know the password. + */ + if (nm_settings_connection_get_timestamp(sett_conn, ×tamp)) { + if (timestamp == 0) + return FALSE; + } + + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + if (ap) { + /* All good; connection is usable */ + NM_SET_OUT(specific_object, g_strdup(nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)))); + return TRUE; + } + + return FALSE; +} + +const CList * +_nm_device_iwd_get_aps(NMDeviceIwd *self) +{ + return &NM_DEVICE_IWD_GET_PRIVATE(self)->aps_lst_head; +} + +static void +scan_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv; + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant && nm_utils_error_is_cancelled(error)) + return; + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + priv->scan_requested = FALSE; + priv->last_scan = nm_utils_get_monotonic_timestamp_msec(); + _notify(self, PROP_LAST_SCAN); + + /* On success, priv->scanning becomes true right before or right + * after this callback, so the next automatic scan will be + * scheduled when priv->scanning goes back to false. On error, + * schedule a retry now. + */ + if (error && !priv->scanning) + schedule_periodic_scan(self, FALSE); +} + +static void +dbus_request_scan_cb(NMDevice * device, + GDBusMethodInvocation *context, + NMAuthSubject * subject, + GError * error, + gpointer user_data) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv; + gs_unref_variant GVariant *scan_options = user_data; + + if (error) { + g_dbus_method_invocation_return_gerror(context, error); + return; + } + + if (check_scanning_prohibited(self, FALSE)) { + g_dbus_method_invocation_return_error_literal(context, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "Scanning not allowed at this time"); + return; + } + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (!priv->can_scan) { + g_dbus_method_invocation_return_error_literal(context, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "Scanning not allowed while unavailable"); + return; + } + + if (scan_options) { + gs_unref_variant GVariant *val = g_variant_lookup_value(scan_options, "ssids", NULL); + + if (val) { + g_dbus_method_invocation_return_error_literal(context, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "'ssid' scan option not supported"); + return; + } + } + + if (!priv->scanning && !priv->scan_requested) { + g_dbus_proxy_call(priv->dbus_station_proxy, + "Scan", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->cancellable, + scan_cb, + self); + priv->scan_requested = TRUE; + } + + g_dbus_method_invocation_return_value(context, NULL); +} + +void +_nm_device_iwd_request_scan(NMDeviceIwd *self, GVariant *options, GDBusMethodInvocation *invocation) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + + if (!priv->can_scan) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "Scanning not allowed while unavailable"); + return; + } + + nm_device_auth_request(device, + invocation, + NULL, + NM_AUTH_PERMISSION_WIFI_SCAN, + TRUE, + NULL, + dbus_request_scan_cb, + nm_g_variant_ref(options)); +} + +static gboolean +check_scanning_prohibited(NMDeviceIwd *self, gboolean periodic) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + g_return_val_if_fail(priv->dbus_obj != NULL, TRUE); + + switch (nm_device_get_state(NM_DEVICE(self))) { + case NM_DEVICE_STATE_UNKNOWN: + case NM_DEVICE_STATE_UNMANAGED: + case NM_DEVICE_STATE_UNAVAILABLE: + case NM_DEVICE_STATE_PREPARE: + case NM_DEVICE_STATE_CONFIG: + case NM_DEVICE_STATE_IP_CONFIG: + case NM_DEVICE_STATE_IP_CHECK: + case NM_DEVICE_STATE_SECONDARIES: + case NM_DEVICE_STATE_DEACTIVATING: + /* Prohibit scans when unusable or activating */ + return TRUE; + case NM_DEVICE_STATE_DISCONNECTED: + case NM_DEVICE_STATE_FAILED: + case NM_DEVICE_STATE_ACTIVATED: + case NM_DEVICE_STATE_NEED_AUTH: + break; + } + + /* Prohibit scans if IWD is busy */ + return !priv->can_scan; +} + +static const char * +get_agent_request_network_path(GDBusMethodInvocation *invocation) +{ + const char *method_name = g_dbus_method_invocation_get_method_name(invocation); + GVariant * params = g_dbus_method_invocation_get_parameters(invocation); + const char *network_path = NULL; + + if (nm_streq(method_name, "RequestPassphrase")) + g_variant_get(params, "(o)", &network_path); + else if (nm_streq(method_name, "RequestPrivateKeyPassphrase")) + g_variant_get(params, "(o)", &network_path); + else if (nm_streq(method_name, "RequestUserNameAndPassword")) + g_variant_get(params, "(o)", &network_path); + else if (nm_streq(method_name, "RequestUserPassword")) { + const char *user; + g_variant_get(params, "(os)", &network_path, &user); + } + + return network_path; +} + +/* + * try_reply_agent_request + * + * Check if the connection settings already have the secrets corresponding + * to the IWD agent method that was invoked. If they do, send the method reply + * with the appropriate secrets. Otherwise, return the missing secret's setting + * name and key so the caller can send a NM secrets request with this data. + * Return TRUE in either case, return FALSE if an error is detected. + */ +static gboolean +try_reply_agent_request(NMDeviceIwd * self, + NMConnection * connection, + GDBusMethodInvocation *invocation, + const char ** setting_name, + const char ** setting_key, + gboolean * replied) +{ + const char * method_name = g_dbus_method_invocation_get_method_name(invocation); + NMSettingWirelessSecurity *s_wireless_sec; + NMSetting8021x * s_8021x; + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + s_8021x = nm_connection_get_setting_802_1x(connection); + + *replied = FALSE; + + if (nm_streq(method_name, "RequestPassphrase")) { + const char *psk; + + if (!s_wireless_sec) + return FALSE; + + psk = nm_setting_wireless_security_get_psk(s_wireless_sec); + if (psk) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the PSK to the IWD Agent"); + + g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", psk)); + *replied = TRUE; + return TRUE; + } + + *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME; + *setting_key = NM_SETTING_WIRELESS_SECURITY_PSK; + return TRUE; + } else if (nm_streq(method_name, "RequestPrivateKeyPassphrase")) { + const char *password; + + if (!s_8021x) + return FALSE; + + password = nm_setting_802_1x_get_private_key_password(s_8021x); + if (password) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the private key password to the IWD Agent"); + + g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", password)); + *replied = TRUE; + return TRUE; + } + + *setting_name = NM_SETTING_802_1X_SETTING_NAME; + *setting_key = NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD; + return TRUE; + } else if (nm_streq(method_name, "RequestUserNameAndPassword")) { + const char *identity, *password; + + if (!s_8021x) + return FALSE; + + identity = nm_setting_802_1x_get_identity(s_8021x); + password = nm_setting_802_1x_get_password(s_8021x); + if (identity && password) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the username and password to the IWD Agent"); + + g_dbus_method_invocation_return_value(invocation, + g_variant_new("(ss)", identity, password)); + *replied = TRUE; + return TRUE; + } + + *setting_name = NM_SETTING_802_1X_SETTING_NAME; + if (!identity) + *setting_key = NM_SETTING_802_1X_IDENTITY; + else + *setting_key = NM_SETTING_802_1X_PASSWORD; + return TRUE; + } else if (nm_streq(method_name, "RequestUserPassword")) { + const char *password; + + if (!s_8021x) + return FALSE; + + password = nm_setting_802_1x_get_password(s_8021x); + if (password) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the user password to the IWD Agent"); + + g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", password)); + *replied = TRUE; + return TRUE; + } + + *setting_name = NM_SETTING_802_1X_SETTING_NAME; + *setting_key = NM_SETTING_802_1X_PASSWORD; + return TRUE; + } else + return FALSE; +} + +static gboolean +assumed_ac_timeout_cb(gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + nm_assert(priv->assumed_ac); + + priv->assumed_ac_timeout = 0; + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT); + /* NMDevice's state change -> NMActRequests/NMActiveConnection's state + * change -> assumed_connection_state_changed_before_managed() -> + * cleanup_association_attempt() so no need to call it explicitly. + */ + return G_SOURCE_REMOVE; +} + +static void wifi_secrets_get_one(NMDeviceIwd * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags, + const char * setting_key, + GDBusMethodInvocation * invocation); + +static void +wifi_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * s_connection, + GError * error, + gpointer user_data) +{ + NMDeviceIwd * self; + NMDeviceIwdPrivate * priv; + NMDevice * device; + GDBusMethodInvocation * invocation; + const char * setting_name; + const char * setting_key; + gboolean replied; + NMSecretAgentGetSecretsFlags get_secret_flags = + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION; + + nm_utils_user_data_unpack(user_data, &self, &invocation); + + g_return_if_fail(NM_IS_DEVICE_IWD(self)); + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + device = NM_DEVICE(self); + + g_return_if_fail(priv->wifi_secrets_id == call_id); + + priv->wifi_secrets_id = NULL; + + if (nm_utils_error_is_cancelled(error)) { + priv->secrets_failed = TRUE; + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "NM secrets request cancelled"); + return; + } + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_act_request_get_settings_connection(req) == s_connection); + + if (nm_device_get_state(device) != NM_DEVICE_STATE_NEED_AUTH) + goto secrets_error; + + if (error) { + _LOGW(LOGD_WIFI, "%s", error->message); + goto secrets_error; + } + + if (!try_reply_agent_request(self, + nm_act_request_get_applied_connection(req), + invocation, + &setting_name, + &setting_key, + &replied)) + goto secrets_error; + + if (replied) { + /* If we replied to the secrets request from IWD in the "disconnected" + * state and IWD doesn't move to a new state within 1 second, assume + * something went wrong (shouldn't happen). If a state change arrives + * after that nothing is lost, state_changed() will try to assume the + * connection again. + */ + if (priv->assumed_ac) { + gs_unref_variant GVariant *value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + + if (nm_streq(get_variant_state(value), "disconnected")) + priv->assumed_ac_timeout = g_timeout_add_seconds(1, assumed_ac_timeout_cb, self); + } + + /* Change state back to what it was before NEED_AUTH */ + nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + return; + } + + if (nm_settings_connection_get_timestamp(nm_act_request_get_settings_connection(req), NULL)) + get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + + /* Request further secrets if we still need something */ + wifi_secrets_get_one(self, setting_name, get_secret_flags, setting_key, invocation); + return; + +secrets_error: + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "NM secrets request failed"); + + if (priv->assumed_ac) { + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + /* NMDevice's state change -> NMActRequests/NMActiveConnection's state + * change -> assumed_connection_state_changed_before_managed() -> + * cleanup_association_attempt() so no need to call it explicitly. + */ + } else { + priv->secrets_failed = TRUE; + /* Now wait for the Connect callback to update device state */ + } +} + +static void +wifi_secrets_get_one(NMDeviceIwd * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags, + const char * setting_key, + GDBusMethodInvocation * invocation) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMActRequest * req; + + wifi_secrets_cancel(self); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv->wifi_secrets_id = nm_act_request_get_secrets(req, + TRUE, + setting_name, + flags, + NM_MAKE_STRV(setting_key), + wifi_secrets_cb, + nm_utils_user_data_pack(self, invocation)); +} + +static void +network_connect_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDevice * device = NM_DEVICE(self); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + NMConnection * connection; + gs_free char * ssid = NULL; + NMDeviceStateReason reason = NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED; + GVariant * value; + gboolean disconnect; + + disconnect = !priv->iwd_autoconnect + || nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL); + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant) { + gs_free char *dbus_error = NULL; + + /* Connection failed; radio problems or if the network wasn't + * open, the passwords or certificates may be wrong. + */ + + _LOGE(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) Network.Connect failed: %s", + error->message); + + if (nm_utils_error_is_cancelled(error)) + return; + + if (!NM_IN_SET(nm_device_get_state(device), + NM_DEVICE_STATE_CONFIG, + NM_DEVICE_STATE_NEED_AUTH)) + return; + + connection = nm_device_get_applied_connection(device); + if (!connection) + goto failed; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR)) + dbus_error = g_dbus_error_get_remote_error(error); + + if (nm_streq0(dbus_error, "net.connman.iwd.Failed")) { + nm_connection_clear_secrets(connection); + + /* If secrets were wrong, we'd be getting a net.connman.iwd.Failed */ + reason = NM_DEVICE_STATE_REASON_NO_SECRETS; + } else if (nm_streq0(dbus_error, "net.connman.iwd.Aborted") && priv->secrets_failed) { + /* If agent call was cancelled we'd be getting a net.connman.iwd.Aborted */ + reason = NM_DEVICE_STATE_REASON_NO_SECRETS; + } + + goto failed; + } + + nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG); + + disconnect = TRUE; + + connection = nm_device_get_applied_connection(device); + if (!connection) + goto failed; + + if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, NULL)) + goto failed; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) Stage 2 of 5 (Device Configure) successful. Connected to '%s'.", + ssid); + nm_device_activate_schedule_stage3_ip_config_start(device); + + return; + +failed: + /* If necessary call Disconnect to make sure IWD's autoconnect is disabled */ + cleanup_association_attempt(self, disconnect); + + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason); + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + if (!priv->iwd_autoconnect && nm_streq(get_variant_state(value), "disconnected")) { + schedule_periodic_scan(self, TRUE); + + if (!priv->nm_autoconnect) { + priv->nm_autoconnect = true; + nm_device_emit_recheck_auto_activate(device); + } + } + g_variant_unref(value); +} + +static void +act_failed_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDevice * device = NM_DEVICE(self); + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant && nm_utils_error_is_cancelled(error)) + return; + + /* Change state to FAILED unless already done by state_changed + * which may have been triggered by the station interface + * appearing on DBus. + */ + if (nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG) + nm_device_queue_state(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); +} + +static void +act_start_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + gs_free char * ssid = NULL; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant) { + _LOGE(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) {AccessPoint,AdHoc}.Start() failed: %s", + error->message); + + if (nm_utils_error_is_cancelled(error)) + return; + + if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG)) + return; + + goto error; + } + + nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG); + + if (!nm_wifi_connection_get_iwd_ssid_and_security(nm_device_get_applied_connection(device), + &ssid, + NULL)) + goto error; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) Stage 2 of 5 (Device Configure) successful. Started '%s'.", + ssid); + nm_device_activate_schedule_stage3_ip_config_start(device); + + return; + +error: + reset_mode(self, priv->cancellable, act_failed_cb, self); +} + +/* Check if we're activating an AP/AdHoc connection and if the target + * DBus interface has appeared already. If so proceed to call Start or + * StartOpen on that interface. + */ +static void +act_check_interface(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMSettingWireless * s_wireless; + GDBusProxy * proxy = NULL; + gs_free char * ssid = NULL; + const char * mode; + NMIwdNetworkSecurity security; + + if (!priv->act_mode_switch) + return; + + s_wireless = + (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS); + + mode = nm_setting_wireless_get_mode(s_wireless); + if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) + proxy = priv->dbus_ap_proxy; + else if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) + proxy = priv->dbus_adhoc_proxy; + + if (!proxy) + return; + + priv->act_mode_switch = FALSE; + + if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG)) + return; + + if (!nm_wifi_connection_get_iwd_ssid_and_security(nm_device_get_applied_connection(device), + &ssid, + &security)) + goto failed; + + if (security == NM_IWD_NETWORK_SECURITY_OPEN) { + g_dbus_proxy_call(proxy, + "StartOpen", + g_variant_new("(s)", ssid), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + priv->cancellable, + act_start_cb, + self); + } else if (security == NM_IWD_NETWORK_SECURITY_PSK) { + NMSettingWirelessSecurity *s_wireless_sec; + const char * psk; + + s_wireless_sec = (NMSettingWirelessSecurity *) nm_device_get_applied_setting( + device, + NM_TYPE_SETTING_WIRELESS_SECURITY); + psk = nm_setting_wireless_security_get_psk(s_wireless_sec); + + if (!psk) { + _LOGE(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) No PSK for '%s'.", ssid); + goto failed; + } + + g_dbus_proxy_call(proxy, + "Start", + g_variant_new("(ss)", ssid, psk), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + priv->cancellable, + act_start_cb, + self); + } else + goto failed; + + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) Called Start('%s').", ssid); + return; + +failed: + reset_mode(self, priv->cancellable, act_failed_cb, self); +} + +static void +act_set_mode_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant) { + _LOGE(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) Setting Device.Mode failed: %s", + error->message); + + if (nm_utils_error_is_cancelled(error)) + return; + + if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG) + || !priv->act_mode_switch) + return; + + priv->act_mode_switch = FALSE; + nm_device_queue_state(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) IWD Device.Mode set successfully"); + + act_check_interface(self); +} + +static void +act_set_mode(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + const char * iwd_mode; + const char * mode; + NMSettingWireless * s_wireless; + + s_wireless = + (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS); + mode = nm_setting_wireless_get_mode(s_wireless); + + /* We need to first set interface mode (Device.Mode) to ap or ad-hoc. + * We can't directly queue a call to the Start/StartOpen method on + * the DBus interface that's going to be created after the property + * set call returns. + */ + iwd_mode = nm_streq(mode, NM_SETTING_WIRELESS_MODE_AP) ? "ap" : "ad-hoc"; + + if (!priv->cancellable) + priv->cancellable = g_cancellable_new(); + + g_dbus_proxy_call( + priv->dbus_device_proxy, + DBUS_INTERFACE_PROPERTIES ".Set", + g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Mode", g_variant_new("s", iwd_mode)), + G_DBUS_CALL_FLAGS_NONE, + 2000, + priv->cancellable, + act_set_mode_cb, + self); + priv->act_mode_switch = TRUE; +} + +static void +act_psk_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * s_connection, + GError * error, + gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv; + NMDevice * device; + + if (nm_utils_error_is_cancelled(error)) + return; + + priv = NM_DEVICE_IWD_GET_PRIVATE(self); + device = NM_DEVICE(self); + + g_return_if_fail(priv->wifi_secrets_id == call_id); + priv->wifi_secrets_id = NULL; + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_act_request_get_settings_connection(req) == s_connection); + + if (nm_device_get_state(device) != NM_DEVICE_STATE_NEED_AUTH) + goto secrets_error; + + if (error) { + _LOGW(LOGD_WIFI, "%s", error->message); + goto secrets_error; + } + + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) missing PSK request completed"); + + /* Change state back to what it was before NEED_AUTH */ + nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + act_set_mode(self); + return; + +secrets_error: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + cleanup_association_attempt(self, FALSE); +} + +static void +set_powered(NMDeviceIwd *self, gboolean powered) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + g_dbus_proxy_call( + priv->dbus_device_proxy, + DBUS_INTERFACE_PROPERTIES ".Set", + g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Powered", g_variant_new("b", powered)), + G_DBUS_CALL_FLAGS_NONE, + 2000, + NULL, + NULL, + NULL); +} + +/*****************************************************************************/ + +static NMWifiAP * +find_ap_by_supplicant_path(NMDeviceIwd *self, const NMRefString *path) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMWifiAP * tmp; + + c_list_for_each_entry (tmp, &priv->aps_lst_head, aps_lst) + if (nm_wifi_ap_get_supplicant_path(tmp) == path) + return tmp; + + return NULL; +} + +static void +assumed_connection_state_changed(NMActiveConnection *active, GParamSpec *pspec, NMDeviceIwd *self) +{ + NMSettingsConnection * sett_conn = nm_active_connection_get_settings_connection(active); + NMActiveConnectionState state = nm_active_connection_get_state(active); + + /* Delete the temporary connection created for an external IWD connection + * (triggered by somebody outside of NM, be it IWD autoconnect or a + * parallel client), unless it's been referenced by a Known Network + * object since, which would remove the EXTERNAL flag. + * + * Note we can't do this too early, e.g. at the same time that we're + * setting the device state to FAILED or DISCONNECTING because the + * connection shouldn't disappear while it's still being used. We do + * this on the connection's transition to DEACTIVATED same as as + * NMManager does for external activations. + */ + if (state != NM_ACTIVE_CONNECTION_STATE_DEACTIVATED) + return; + + g_signal_handlers_disconnect_by_func(active, assumed_connection_state_changed, NULL); + + if (sett_conn + && NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn), + NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)) + nm_settings_connection_delete(sett_conn, FALSE); +} + +static void +assumed_connection_state_changed_before_managed(NMActiveConnection *active, + GParamSpec * pspec, + NMDeviceIwd * self) +{ + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMActiveConnectionState state = nm_active_connection_get_state(active); + gboolean disconnect; + + if (state != NM_ACTIVE_CONNECTION_STATE_DEACTIVATED) + return; + + /* When an assumed connection fails we always get called, even if the + * activation hasn't reached PREPARE or CONFIG, e.g. because of a policy + * or authorization problem in NMManager. .deactivate would only be + * called starting at some stage so we can't rely on that. + * + * If the error happened before PREPARE (where we set a non-NULL + * priv->current_ap) that will mean NM is somehow blocking autoconnect + * so we want to call IWD's Station.Disconnect() to block its + * autoconnect. If this happens during or after PREPARE, we just + * clean up and wait for a new attempt by IWD. + * + * cleanup_association_attempt will clear priv->assumed_ac, disconnect + * this callback from the signal and also send a Disconnect to IWD if + * needed. + * + * Note this function won't be called after IWD transitions to + * "connected" (and NMDevice to IP_CONFIG) as we disconnect from the + * signal at that point, cleanup_association_attempt() will be + * triggered by an IWD state change instead. + */ + disconnect = !priv->current_ap; + cleanup_association_attempt(self, disconnect); +} + +static void +assume_connection(NMDeviceIwd *self, NMWifiAP *ap) +{ + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMSettingsConnection *sett_conn; + gs_unref_object NMAuthSubject *subject = NULL; + NMActiveConnection * ac; + gs_free_error GError *error = NULL; + + /* We can use the .update_connection / nm_device_emit_recheck_assume + * API but we can also pass an assumed/external activation type + * directly to nm_manager_activate_connection() and skip the + * complicated process of creating a matching connection, taking + * advantage of the Known Networks pointing directly to a mirror + * connection. The only downside seems to be + * nm_manager_activate_connection() goes through the extra + * authorization. + * + * However for now we implement a similar behaviour using a normal + * "managed" activation. For one, assumed/external + * connection state is not reflected in nm_manager_get_state() until + * fully activated. Secondly setting the device state to FAILED + * is treated as ACTIVATED so we'd have to find another way to signal + * that stage2 is failing asynchronously. Thirdly the connection + * becomes "managed" only when ACTIVATED but for IWD it's really + * managed when IP_CONFIG starts. + */ + sett_conn = nm_iwd_manager_get_ap_mirror_connection(nm_iwd_manager_get(), ap); + if (!sett_conn) + goto error; + + subject = nm_auth_subject_new_internal(); + ac = nm_manager_activate_connection( + NM_MANAGER_GET, + sett_conn, + NULL, + nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)), + NM_DEVICE(self), + subject, + NM_ACTIVATION_TYPE_MANAGED, + NM_ACTIVATION_REASON_ASSUME, + NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY, + &error); + + if (!ac) { + _LOGW(LOGD_WIFI, "Activation: (wifi) assume error: %s", error->message); + goto error; + } + + /* If no Known Network existed for this AP, we generated a temporary + * NMSettingsConnection with the EXTERNAL flag. It is not referenced by + * any Known Network objects at this time so we want to delete it if the + * IWD connection ends up failing or a later part of the activation fails + * before IWD created a Known Network. + * Setting the activation type to EXTERNAL would do this by causing + * NM_ACTIVATION_STATE_FLAG_EXTERNAL to be set on the NMActiveConnection + * but we don't want the connection to be marked EXTERNAL because we + * will be assuming the ownership of it in IP_CONFIG or thereabouts. + * + * This callback stays connected forever while the second one gets + * disconnected when we reset the activation type to managed. + */ + g_signal_connect(ac, + "notify::" NM_ACTIVE_CONNECTION_STATE, + G_CALLBACK(assumed_connection_state_changed), + NULL); + g_signal_connect(ac, + "notify::" NM_ACTIVE_CONNECTION_STATE, + G_CALLBACK(assumed_connection_state_changed_before_managed), + self); + priv->assumed_ac = g_object_ref(ac); + + return; + +error: + send_disconnect(self); + + if (sett_conn + && NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn), + NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)) + nm_settings_connection_delete(sett_conn, FALSE); +} + +static void +assumed_connection_progress_to_ip_config(NMDeviceIwd *self, gboolean was_postponed) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMDeviceState dev_state = nm_device_get_state(device); + + wifi_secrets_cancel(self); + nm_clear_g_source(&priv->assumed_ac_timeout); + + /* NM takes over the activation from this point on so clear the assumed + * activation state and if we were using NM_ACTIVATION_TYPE_ASSUMED or + * _EXTERNAL we'd need to reset the activation type to _MANAGED at this + * point instead of waiting for the ACTIVATED state (as done in + * nm_active_connection_set_state). + */ + cleanup_assumed_connect(self); + + if (dev_state == NM_DEVICE_STATE_NEED_AUTH) + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_CONFIG, + NM_DEVICE_STATE_REASON_NONE); + + /* If stage2 had returned NM_ACT_STAGE_RETURN_POSTPONE, we tell NMDevice + * that stage2 is done. + */ + if (was_postponed) + nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self)); +} + +static void +initial_check_assume(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + const char * network_path_str; + nm_auto_ref_string NMRefString *network_path = NULL; + NMWifiAP * ap = NULL; + gs_unref_variant GVariant *state_value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + gs_unref_variant GVariant *cn_value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "ConnectedNetwork"); + + if (!NM_IN_STRSET(get_variant_state(state_value), "connecting", "connected", "roaming")) + return; + + if (!priv->iwd_autoconnect) { + send_disconnect(self); + return; + } + + if (!cn_value || !g_variant_is_of_type(cn_value, G_VARIANT_TYPE_OBJECT_PATH)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "ConnectedNetwork property not cached or not an object path"); + return; + } + + network_path_str = g_variant_get_string(cn_value, NULL); + network_path = nm_ref_string_new(network_path_str); + ap = find_ap_by_supplicant_path(self, network_path); + + if (!ap) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "ConnectedNetwork points to an unknown Network %s", + network_path_str); + return; + } + + _LOGD(LOGD_DEVICE | LOGD_WIFI, "assuming connection in initial_check_assume"); + assume_connection(self, ap); +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMWifiAP * ap = NULL; + gs_unref_object NMWifiAP *ap_fake = NULL; + NMActRequest * req; + NMConnection * connection; + NMSettingWireless * s_wireless; + const char * mode; + const char * ap_path; + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + connection = nm_act_request_get_applied_connection(req); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + s_wireless = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE); + + /* AP, Ad-Hoc modes never use a specific object or existing scanned AP */ + mode = nm_setting_wireless_get_mode(s_wireless); + if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC)) + goto add_new; + + ap_path = nm_active_connection_get_specific_object(NM_ACTIVE_CONNECTION(req)); + ap = ap_path ? nm_wifi_ap_lookup_for_device(NM_DEVICE(self), ap_path) : NULL; + if (ap) { + set_current_ap(self, ap, TRUE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + if (ap) { + nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req), + nm_dbus_object_get_path(NM_DBUS_OBJECT(ap))); + set_current_ap(self, ap, TRUE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + + /* In infrastructure mode the specific object should be set by now except + * for a first-time connection to a hidden network. If a hidden network is + * a Known Network it should still have been in the AP list. + */ + if (!nm_setting_wireless_get_hidden(s_wireless) || is_connection_known_network(connection)) + return NM_ACT_STAGE_RETURN_FAILURE; + +add_new: + /* If the user is trying to connect to an AP that NM doesn't yet know about + * (hidden network or something) or starting a Hotspot, create a fake AP + * from the security settings in the connection. This "fake" AP gets used + * until the real one is found in the scan list (Ad-Hoc or Hidden), or until + * the device is deactivated (Ad-Hoc or Hotspot). + */ + ap_fake = nm_wifi_ap_new_fake_from_connection(connection); + if (!ap_fake) + g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE); + + if (nm_wifi_ap_is_hotspot(ap_fake)) + nm_wifi_ap_set_address(ap_fake, nm_device_get_hw_address(device)); + + g_object_freeze_notify(G_OBJECT(self)); + ap_add_remove(self, TRUE, ap_fake, FALSE); + g_object_thaw_notify(G_OBJECT(self)); + set_current_ap(self, ap_fake, FALSE); + nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req), + nm_dbus_object_get_path(NM_DBUS_OBJECT(ap_fake))); + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMActRequest * req; + NMConnection * connection; + NMSettingWireless * s_wireless; + const char * mode; + + req = nm_device_get_act_request(device); + connection = nm_act_request_get_applied_connection(req); + s_wireless = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE); + + mode = nm_setting_wireless_get_mode(s_wireless); + + if (NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) { + gs_unref_object GDBusProxy *network_proxy = NULL; + NMWifiAP * ap = priv->current_ap; + NMSettingWirelessSecurity * s_wireless_sec; + + if (!ap) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + goto out_fail; + } + + /* With priv->iwd_autoconnect, if we're assuming a connection because + * of a state change to "connecting", signal stage 2 is still running. + * If "connected" or "roaming", we can go right to the IP_CONFIG state + * and there's nothing left to do in CONFIG. + * If we're assuming the connection because of an agent request we + * switch to NEED_AUTH and actually send the request now that we + * have an activation request. + * + * This all assumes ConnectedNetwork hasn't changed. + */ + if (priv->assumed_ac) { + gboolean result; + + if (!priv->pending_agent_request) { + gs_unref_variant GVariant *value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + + if (nm_streq(get_variant_state(value), "connecting")) { + return NM_ACT_STAGE_RETURN_POSTPONE; + } else { + /* This basically forgets that the connection was "assumed" + * as we can treat it like any connection triggered by a + * Network.Connect() call from now on. + */ + assumed_connection_progress_to_ip_config(self, FALSE); + return NM_ACT_STAGE_RETURN_SUCCESS; + } + } + + result = nm_device_iwd_agent_query(self, priv->pending_agent_request); + g_clear_object(&priv->pending_agent_request); + nm_assert(result); + + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + /* 802.1x networks that are not IWD Known Networks will definitely + * fail, for other combinations we will let the Connect call fail + * or ask us for any missing secrets through the Agent. + */ + if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(ap)) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) access point '%s' has 802.1x security but is not configured " + "in IWD.", + nm_connection_get_id(connection)); + + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + goto out_fail; + } + + priv->secrets_failed = FALSE; + + if (nm_wifi_ap_get_fake(ap)) { + gs_free char *ssid = NULL; + + if (!nm_setting_wireless_get_hidden(s_wireless)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) target network not known to IWD but is not " + "marked hidden"); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + goto out_fail; + } + + if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, NULL)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + goto out_fail; + } + + /* Use Station.ConnectHiddenNetwork method instead of Network proxy. */ + g_dbus_proxy_call(priv->dbus_station_proxy, + "ConnectHiddenNetwork", + g_variant_new("(s)", ssid), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + priv->cancellable, + network_connect_cb, + self); + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + network_proxy = nm_iwd_manager_get_dbus_interface( + nm_iwd_manager_get(), + nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)), + NM_IWD_NETWORK_INTERFACE); + if (!network_proxy) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) could not get Network interface proxy for %s", + nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap))); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + goto out_fail; + } + + if (!priv->cancellable) + priv->cancellable = g_cancellable_new(); + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + if (s_wireless_sec + && nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec), "owe")) { + _LOGI(LOGD_WIFI, + "An OWE connection is requested but IWD may connect to either an OWE " + "or unsecured network and there won't be any indication of whether " + "encryption is in use -- proceed at your own risk!"); + } + + /* Call Network.Connect. No timeout because IWD already handles + * timeouts. + */ + g_dbus_proxy_call(network_proxy, + "Connect", + NULL, + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + priv->cancellable, + network_connect_cb, + self); + + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC)) { + NMSettingWirelessSecurity *s_wireless_sec; + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + if (s_wireless_sec && !nm_setting_wireless_security_get_psk(s_wireless_sec)) { + /* PSK is missing from the settings, have to request it */ + + wifi_secrets_cancel(self); + + priv->wifi_secrets_id = + nm_act_request_get_secrets(req, + TRUE, + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION, + NM_MAKE_STRV(NM_SETTING_WIRELESS_SECURITY_PSK), + act_psk_cb, + self); + nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE); + } else + act_set_mode(self); + + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) iwd cannot handle mode %s", mode); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + +out_fail: + cleanup_association_attempt(self, FALSE); + return NM_ACT_STAGE_RETURN_FAILURE; +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + return nm_device_get_configured_mtu_from_connection(device, + NM_TYPE_SETTING_WIRELESS, + out_source); +} + +static gboolean +periodic_scan_timeout_cb(gpointer user_data) +{ + NMDeviceIwd * self = user_data; + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + priv->periodic_scan_id = 0; + + if (priv->scanning || priv->scan_requested) + return FALSE; + + g_dbus_proxy_call(priv->dbus_station_proxy, + "Scan", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->cancellable, + scan_cb, + self); + priv->scan_requested = TRUE; + + return FALSE; +} + +static void +schedule_periodic_scan(NMDeviceIwd *self, gboolean initial_scan) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + guint interval; + + /* Automatically start a scan after a disconnect, mode change or device UP, + * otherwise scan periodically every 10 seconds if needed for NM's + * autoconnect. There's no need to scan When using IWD's autoconnect or + * when connected, we update the AP list on UI requests. + * + * (initial_scan && disconnected && !priv->iwd_autoconnect) override + * priv->scanning below because of an IWD quirk where a device will often + * be in the autoconnect state and scanning at the time of our initial_scan, + * but our logic will then send it a Disconnect() causing IWD to exit + * autoconnect and interrupt the ongoing scan, meaning that we still want + * a new scan ASAP. + */ + if (!priv->can_scan || priv->scan_requested || priv->current_ap || priv->iwd_autoconnect) + interval = -1; + else if (initial_scan && priv->scanning) + interval = 0; + else if (priv->scanning) + interval = -1; + else if (!priv->periodic_scan_id) + interval = 10; + else + return; + + nm_clear_g_source(&priv->periodic_scan_id); + + if (interval != (guint) -1) + priv->periodic_scan_id = g_timeout_add_seconds(interval, periodic_scan_timeout_cb, self); +} + +static void +set_can_scan(NMDeviceIwd *self, gboolean can_scan) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (priv->can_scan == can_scan) + return; + + priv->can_scan = can_scan; + + if (!priv->iwd_autoconnect) + schedule_periodic_scan(self, TRUE); +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMSettingWireless * s_wireless; + const char * mode; + + switch (new_state) { + case NM_DEVICE_STATE_UNMANAGED: + break; + case NM_DEVICE_STATE_UNAVAILABLE: + /* + * If the device is enabled and the IWD manager is ready, + * transition to DISCONNECTED because the device is now + * ready to use. + */ + if (priv->enabled && priv->dbus_station_proxy) { + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + } + break; + case NM_DEVICE_STATE_DISCONNECTED: + if (old_state == NM_DEVICE_STATE_UNAVAILABLE) + initial_check_assume(self); + break; + case NM_DEVICE_STATE_IP_CONFIG: + s_wireless = + (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS); + mode = nm_setting_wireless_get_mode(s_wireless); + if (!priv->periodic_update_id + && NM_IN_STRSET(mode, + NULL, + NM_SETTING_WIRELESS_MODE_INFRA, + NM_SETTING_WIRELESS_MODE_ADHOC)) { + priv->periodic_update_id = g_timeout_add_seconds(6, periodic_update_cb, self); + periodic_update(self); + } + break; + default: + break; + } +} + +static gboolean +get_enabled(NMDevice *device) +{ + return NM_DEVICE_IWD_GET_PRIVATE(device)->enabled; +} + +static void +set_enabled(NMDevice *device, gboolean enabled) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(device); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDeviceState state; + + enabled = !!enabled; + + if (priv->enabled == enabled) + return; + + priv->enabled = enabled; + + _LOGD(LOGD_WIFI, "device now %s", enabled ? "enabled" : "disabled"); + + state = nm_device_get_state(device); + if (state < NM_DEVICE_STATE_UNAVAILABLE) { + _LOGD(LOGD_WIFI, "(%s): device blocked by UNMANAGED state", enabled ? "enable" : "disable"); + return; + } + + if (priv->dbus_obj) + set_powered(self, enabled); + + if (enabled) { + if (state != NM_DEVICE_STATE_UNAVAILABLE) + _LOGW(LOGD_CORE, "not in expected unavailable state!"); + + if (priv->dbus_station_proxy) { + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + } + } else { + nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE); + } +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + /* Only handle wireless setting here, delegate other settings to parent class */ + if (nm_streq(setting_name, NM_SETTING_WIRELESS_SETTING_NAME)) { + return nm_device_hash_check_invalid_keys( + diffs, + NM_SETTING_WIRELESS_SETTING_NAME, + error, + NM_SETTING_WIRELESS_SEEN_BSSIDS, /* ignored */ + NM_SETTING_WIRELESS_MTU); /* reapplied with IP config */ + } + + device_class = NM_DEVICE_CLASS(nm_device_iwd_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(object); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + const char ** list; + + switch (prop_id) { + case PROP_MODE: + if (!priv->current_ap) + g_value_set_uint(value, NM_802_11_MODE_UNKNOWN); + else if (nm_wifi_ap_is_hotspot(priv->current_ap)) + g_value_set_uint(value, NM_802_11_MODE_AP); + else + g_value_set_uint(value, nm_wifi_ap_get_mode(priv->current_ap)); + + break; + case PROP_BITRATE: + g_value_set_uint(value, priv->rate); + break; + case PROP_CAPABILITIES: + g_value_set_uint(value, priv->capabilities); + break; + case PROP_ACCESS_POINTS: + list = nm_wifi_aps_get_paths(&priv->aps_lst_head, TRUE); + g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list)); + break; + case PROP_ACTIVE_ACCESS_POINT: + nm_dbus_utils_g_value_set_object_path(value, priv->current_ap); + break; + case PROP_SCANNING: + g_value_set_boolean(value, priv->scanning); + break; + case PROP_LAST_SCAN: + g_value_set_int64( + value, + priv->last_scan > 0 + ? nm_utils_monotonic_timestamp_as_boottime(priv->last_scan, NM_UTILS_NSEC_PER_MSEC) + : (gint64) -1); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +state_changed(NMDeviceIwd *self, const char *new_state) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMDeviceState dev_state = nm_device_get_state(device); + gboolean nm_connection = priv->current_ap || priv->assumed_ac; + gboolean iwd_connection = FALSE; + NMWifiAP * ap = NULL; + gboolean can_connect = priv->nm_autoconnect; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, "new IWD device state is %s", new_state); + + if (NM_IN_STRSET(new_state, "connecting", "connected", "roaming")) { + gs_unref_variant GVariant *value = NULL; + const char * network_path_str; + nm_auto_ref_string NMRefString *network_path = NULL; + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "ConnectedNetwork"); + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "ConnectedNetwork property not cached or not an object path"); + return; + } + + iwd_connection = TRUE; + network_path_str = g_variant_get_string(value, NULL); + network_path = nm_ref_string_new(network_path_str); + ap = find_ap_by_supplicant_path(self, network_path); + + if (!ap) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "ConnectedNetwork points to an unknown Network %s", + network_path_str); + return; + } + } + + /* Don't allow scanning while connecting, disconnecting or roaming */ + set_can_scan(self, NM_IN_STRSET(new_state, "connected", "disconnected")); + + priv->nm_autoconnect = FALSE; + + if (nm_connection && iwd_connection && priv->current_ap && ap != priv->current_ap) { + gboolean switch_ap = priv->iwd_autoconnect && priv->assumed_ac; + + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "IWD is connecting to the wrong AP, %s activation", + switch_ap ? "replacing" : "aborting"); + cleanup_association_attempt(self, !switch_ap); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + + if (switch_ap) + assume_connection(self, ap); + return; + } + + if (priv->iwd_autoconnect && iwd_connection) { + if (dev_state < NM_DEVICE_STATE_DISCONNECTED) + return; + + /* If IWD is in any state other than disconnected and the NMDevice is + * in DISCONNECTED then someone else, possibly IWD's autoconnect, has + * commanded an action and we need to update our NMDevice's state to + * match, including finding the NMSettingsConnection and NMWifiAP + * matching the network pointed to by Station.ConnectedNetwork. + * + * If IWD is in the connected state and we're in CONFIG, we only have + * to signal that the existing connection request has advanced to a new + * state. If the connection request came from NM, we must have used + * Network.Connect() so that method call's callback will update the + * connection request, otherwise we do it here. + * + * If IWD is disconnecting or just disconnected, the common code below + * (independent from priv->iwd_autoconnect) will handle this case. + * If IWD is disconnecting but we never saw a connection request in the + * first place (maybe because we're only startig up) we won't be + * setting up an NMActiveConnection just to put the NMDevice in the + * DEACTIVATING state and we ignore this case. + * + * If IWD was in the disconnected state and transitioned to + * "connecting" but we were already in NEED_AUTH because we handled an + * agent query -- IWD normally stays in "disconnected" until it has all + * the secrets -- we record this fact and remain in NEED_AUTH. + */ + if (!nm_connection) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "This is a new connection, 'assuming' it"); + assume_connection(self, ap); + return; + } + + if (priv->assumed_ac && dev_state >= NM_DEVICE_STATE_PREPARE + && dev_state < NM_DEVICE_STATE_IP_CONFIG + && NM_IN_STRSET(new_state, "connected", "roaming")) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Updating assumed activation state"); + assumed_connection_progress_to_ip_config(self, TRUE); + return; + } + + if (priv->assumed_ac) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "Clearing assumed activation timeout"); + nm_clear_g_source(&priv->assumed_ac_timeout); + return; + } + } else if (!priv->iwd_autoconnect && iwd_connection) { + /* If we were connecting, do nothing, the confirmation of + * a connection success is handled in the Device.Connect + * method return callback. Otherwise, IWD must have connected + * without Network Manager's will so for simplicity force a + * disconnect. + */ + if (nm_connection) + return; + + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Unsolicited connection, asking IWD to disconnect"); + send_disconnect(self); + } else if (NM_IN_STRSET(new_state, "disconnecting", "disconnected")) { + /* If necessary, call Disconnect on the IWD device object to make sure + * it disables its autoconnect. + */ + if ((!priv->iwd_autoconnect + || nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL)) + && !priv->wifi_secrets_id && !priv->pending_agent_request) + send_disconnect(self); + + /* + * If IWD is still handling the Connect call, let our Connect + * callback for the dbus method handle the failure. The main + * reason we don't want to handle the failure here is because the + * method callback will have more information on the specific + * failure reason. + * + * If IWD is handling an autoconnect agent call, let the agent's + * Cancel() handler take care of this. + */ + if (NM_IN_SET(dev_state, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_NEED_AUTH) + && !priv->assumed_ac) + return; + if (NM_IN_SET(dev_state, NM_DEVICE_STATE_NEED_AUTH) && priv->assumed_ac) + return; + + if (nm_connection) { + cleanup_association_attempt(self, FALSE); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + } + } else if (!nm_streq(new_state, "unknown")) { + _LOGE(LOGD_WIFI, "State %s unknown", new_state); + return; + } + + /* Don't allow new connection until iwd exits disconnecting and no + * Connect callback is pending. + */ + if (!priv->iwd_autoconnect && NM_IN_STRSET(new_state, "disconnected")) { + priv->nm_autoconnect = TRUE; + if (!can_connect) + nm_device_emit_recheck_auto_activate(device); + } +} + +static void +scanning_changed(NMDeviceIwd *self, gboolean new_scanning) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + if (new_scanning == priv->scanning) + return; + + priv->scanning = new_scanning; + + _notify(self, PROP_SCANNING); + + if (!priv->scanning) { + update_aps(self); + + if (!priv->scan_requested && !priv->iwd_autoconnect) + schedule_periodic_scan(self, FALSE); + } +} + +static void +station_properties_changed(GDBusProxy *proxy, + GVariant * changed_properties, + GStrv invalidate_properties, + gpointer user_data) +{ + NMDeviceIwd *self = user_data; + const char * new_str; + gboolean new_bool; + + if (g_variant_lookup(changed_properties, "State", "&s", &new_str)) + state_changed(self, new_str); + + if (g_variant_lookup(changed_properties, "Scanning", "b", &new_bool)) + scanning_changed(self, new_bool); +} + +static void +ap_adhoc_properties_changed(GDBusProxy *proxy, + GVariant * changed_properties, + GStrv invalidate_properties, + gpointer user_data) +{ + NMDeviceIwd *self = user_data; + gboolean new_bool; + + if (g_variant_lookup(changed_properties, "Started", "b", &new_bool)) + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "IWD AP/AdHoc state is now %s", + new_bool ? "Started" : "Stopped"); +} + +static void +powered_changed(NMDeviceIwd *self, gboolean new_powered) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + GDBusInterface * interface; + + nm_device_queue_recheck_available(NM_DEVICE(self), + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + + interface = + new_powered ? g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_AP_INTERFACE) : NULL; + + if (priv->dbus_ap_proxy) { + g_signal_handlers_disconnect_by_func(priv->dbus_ap_proxy, + ap_adhoc_properties_changed, + self); + g_clear_object(&priv->dbus_ap_proxy); + } + + if (interface) { + priv->dbus_ap_proxy = G_DBUS_PROXY(interface); + g_signal_connect(priv->dbus_ap_proxy, + "g-properties-changed", + G_CALLBACK(ap_adhoc_properties_changed), + self); + + if (priv->act_mode_switch) + act_check_interface(self); + else + reset_mode(self, NULL, NULL, NULL); + } + + interface = + new_powered ? g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_ADHOC_INTERFACE) : NULL; + + if (priv->dbus_adhoc_proxy) { + g_signal_handlers_disconnect_by_func(priv->dbus_adhoc_proxy, + ap_adhoc_properties_changed, + self); + g_clear_object(&priv->dbus_adhoc_proxy); + } + + if (interface) { + priv->dbus_adhoc_proxy = G_DBUS_PROXY(interface); + g_signal_connect(priv->dbus_adhoc_proxy, + "g-properties-changed", + G_CALLBACK(ap_adhoc_properties_changed), + self); + + if (priv->act_mode_switch) + act_check_interface(self); + else + reset_mode(self, NULL, NULL, NULL); + } + + /* We expect one of the three interfaces to always be present when + * device is Powered so if AP and AdHoc are not present we should + * be in station mode. + */ + if (new_powered && !priv->dbus_ap_proxy && !priv->dbus_adhoc_proxy) { + interface = g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_STATION_INTERFACE); + if (!interface) { + _LOGE(LOGD_WIFI, + "Interface %s not found on obj %s", + NM_IWD_STATION_INTERFACE, + g_dbus_object_get_object_path(priv->dbus_obj)); + interface = NULL; + } + } else + interface = NULL; + + if (priv->dbus_station_proxy) { + g_signal_handlers_disconnect_by_func(priv->dbus_station_proxy, + station_properties_changed, + self); + g_clear_object(&priv->dbus_station_proxy); + } + + if (interface) { + GVariant *value; + + priv->dbus_station_proxy = G_DBUS_PROXY(interface); + g_signal_connect(priv->dbus_station_proxy, + "g-properties-changed", + G_CALLBACK(station_properties_changed), + self); + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "Scanning"); + priv->scanning = get_variant_boolean(value, "Scanning"); + g_variant_unref(value); + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + state_changed(self, get_variant_state(value)); + g_variant_unref(value); + + update_aps(self); + + /* When a device is brought UP in station mode, including after a mode + * switch, IWD re-enables autoconnect. This is unlike NM's autoconnect + * where a mode change doesn't interfere with the + * BLOCKED_MANUAL_DISCONNECT flag. + */ + if (priv->iwd_autoconnect) { + nm_device_autoconnect_blocked_unset(NM_DEVICE(self), + NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL); + } + } else { + set_can_scan(self, FALSE); + priv->scanning = FALSE; + priv->scan_requested = FALSE; + priv->nm_autoconnect = FALSE; + cleanup_association_attempt(self, FALSE); + remove_all_aps(self); + } +} + +static void +device_properties_changed(GDBusProxy *proxy, + GVariant * changed_properties, + GStrv invalidate_properties, + gpointer user_data) +{ + NMDeviceIwd *self = user_data; + gboolean new_bool; + + if (g_variant_lookup(changed_properties, "Powered", "b", &new_bool)) + powered_changed(self, new_bool); +} + +static void +config_changed(NMConfig * config, + NMConfigData * config_data, + NMConfigChangeFlags changes, + NMConfigData * old_data, + NMDeviceIwd * self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + gboolean old_iwd_ac = priv->iwd_autoconnect; + + priv->iwd_autoconnect = + nm_config_data_get_device_config_boolean(config_data, + NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_IWD_AUTOCONNECT, + NM_DEVICE(self), + TRUE, + TRUE); + + if (old_iwd_ac != priv->iwd_autoconnect && priv->dbus_station_proxy && !priv->current_ap) { + gs_unref_variant GVariant *value = NULL; + + if (!priv->iwd_autoconnect + && !nm_device_autoconnect_blocked_get(NM_DEVICE(self), + NM_DEVICE_AUTOCONNECT_BLOCKED_ALL)) + send_disconnect(self); + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + state_changed(self, get_variant_state(value)); + } +} + +void +nm_device_iwd_set_dbus_object(NMDeviceIwd *self, GDBusObject *object) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + GDBusInterface * interface; + gs_unref_variant GVariant *value = NULL; + gs_unref_object GDBusProxy *adapter_proxy = NULL; + GVariantIter * iter; + const char * mode; + gboolean powered; + NMDeviceWifiCapabilities capabilities; + + if (!nm_g_object_ref_set(&priv->dbus_obj, object)) + return; + + if (priv->dbus_device_proxy) { + g_signal_handlers_disconnect_by_func(priv->dbus_device_proxy, + device_properties_changed, + self); + g_clear_object(&priv->dbus_device_proxy); + + powered_changed(self, FALSE); + + priv->act_mode_switch = FALSE; + + g_signal_handlers_disconnect_by_func(nm_config_get(), config_changed, self); + } + + if (!object) + return; + + interface = g_dbus_object_get_interface(object, NM_IWD_DEVICE_INTERFACE); + if (!interface) { + _LOGE(LOGD_WIFI, + "Interface %s not found on obj %s", + NM_IWD_DEVICE_INTERFACE, + g_dbus_object_get_object_path(object)); + g_clear_object(&priv->dbus_obj); + return; + } + + priv->dbus_device_proxy = G_DBUS_PROXY(interface); + + g_signal_connect(priv->dbus_device_proxy, + "g-properties-changed", + G_CALLBACK(device_properties_changed), + self); + + /* Parse list of interface modes supported by adapter (wiphy) */ + + value = g_dbus_proxy_get_cached_property(priv->dbus_device_proxy, "Adapter"); + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) { + nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "Adapter property not cached or not an object path"); + goto error; + } + + adapter_proxy = nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), + g_variant_get_string(value, NULL), + NM_IWD_WIPHY_INTERFACE); + if (!adapter_proxy) { + nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "Can't get DBus proxy for IWD Adapter for IWD Device"); + goto error; + } + + g_variant_unref(value); + value = g_dbus_proxy_get_cached_property(adapter_proxy, "SupportedModes"); + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY)) { + nm_log_warn(LOGD_DEVICE | LOGD_WIFI, + "SupportedModes property not cached or not a string array"); + goto error; + } + + capabilities = NM_WIFI_DEVICE_CAP_CIPHER_CCMP | NM_WIFI_DEVICE_CAP_RSN; + + g_variant_get(value, "as", &iter); + while (g_variant_iter_next(iter, "&s", &mode)) { + if (nm_streq(mode, "ap")) + capabilities |= NM_WIFI_DEVICE_CAP_AP; + else if (nm_streq(mode, "ad-hoc")) + capabilities |= NM_WIFI_DEVICE_CAP_ADHOC; + } + g_variant_iter_free(iter); + + if (priv->capabilities != capabilities) { + priv->capabilities = capabilities; + _notify(self, PROP_CAPABILITIES); + } + + /* Update iwd_autoconnect before any state_changed call */ + g_signal_connect(nm_config_get(), + NM_CONFIG_SIGNAL_CONFIG_CHANGED, + G_CALLBACK(config_changed), + self); + config_changed(NULL, NM_CONFIG_GET_DATA, 0, NULL, self); + + g_variant_unref(value); + value = g_dbus_proxy_get_cached_property(priv->dbus_device_proxy, "Powered"); + powered = get_variant_boolean(value, "Powered"); + + if (powered != priv->enabled) + set_powered(self, priv->enabled); + else if (powered) + powered_changed(self, TRUE); + + return; + +error: + g_signal_handlers_disconnect_by_func(priv->dbus_device_proxy, device_properties_changed, self); + g_clear_object(&priv->dbus_device_proxy); +} + +gboolean +nm_device_iwd_agent_query(NMDeviceIwd *self, GDBusMethodInvocation *invocation) +{ + NMDevice * device = NM_DEVICE(self); + NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMDeviceState state = nm_device_get_state(device); + const char * setting_name; + const char * setting_key; + gboolean replied; + NMWifiAP * ap; + NMSecretAgentGetSecretsFlags get_secret_flags = + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION; + nm_auto_ref_string NMRefString *network_path = NULL; + + if (!invocation) { + gs_unref_variant GVariant *value = + g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + gboolean disconnect; + + if (!priv->wifi_secrets_id && !priv->pending_agent_request) + return FALSE; + + _LOGI(LOGD_WIFI, "IWD agent request is being cancelled"); + wifi_secrets_cancel(self); + + if (state == NM_DEVICE_STATE_NEED_AUTH) + nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE); + + /* The secrets request is being cancelled. If we don't have an assumed + * connection than we've probably called Network.Connect and that method + * call's callback is going to handle the failure. And if the state was + * not "disconnected" then let the state change handler process the + * failure. + */ + if (!priv->assumed_ac) + return TRUE; + + if (!nm_streq(get_variant_state(value), "disconnected")) + return TRUE; + + disconnect = nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL); + cleanup_association_attempt(self, disconnect); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return TRUE; + } + + if (state > NM_DEVICE_STATE_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) { + _LOGW(LOGD_WIFI, "Can't handle the IWD agent request in current device state"); + return FALSE; + } + + if (priv->wifi_secrets_id || priv->pending_agent_request) { + _LOGW(LOGD_WIFI, "There's already a pending agent request for this device"); + return FALSE; + } + + network_path = nm_ref_string_new(get_agent_request_network_path(invocation)); + ap = find_ap_by_supplicant_path(self, network_path); + if (!ap) { + _LOGW(LOGD_WIFI, "IWD Network object not found for the agent request"); + return FALSE; + } + + if (priv->assumed_ac) { + const char *ac_ap_path = nm_active_connection_get_specific_object(priv->assumed_ac); + + if (!nm_streq(ac_ap_path, nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)))) { + _LOGW(LOGD_WIFI, + "Dropping an existing assumed connection to create a new one based on the IWD " + "agent request network parameter"); + + if (priv->current_ap) + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + + cleanup_association_attempt(self, FALSE); + priv->pending_agent_request = g_object_ref(invocation); + assume_connection(self, ap); + return TRUE; + } + + if (state != NM_DEVICE_STATE_CONFIG) { + _LOGI(LOGD_WIFI, "IWD agent request deferred until in CONFIG"); + priv->pending_agent_request = g_object_ref(invocation); + return TRUE; + } + + /* Otherwise handle as usual */ + } else if (!priv->current_ap) { + _LOGI(LOGD_WIFI, "IWD is asking for secrets without explicit connect request"); + + if (priv->iwd_autoconnect) { + priv->pending_agent_request = g_object_ref(invocation); + assume_connection(self, ap); + return TRUE; + } + + send_disconnect(self); + return FALSE; + } else if (priv->current_ap) { + if (priv->current_ap != ap) { + _LOGW(LOGD_WIFI, "IWD agent request for a wrong network object"); + cleanup_association_attempt(self, TRUE); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return FALSE; + } + + /* Otherwise handle as usual */ + } + + if (!try_reply_agent_request(self, + nm_device_get_applied_connection(device), + invocation, + &setting_name, + &setting_key, + &replied)) { + priv->secrets_failed = TRUE; + return FALSE; + } + + if (replied) + return TRUE; + + /* Normally require new secrets every time IWD asks for them. + * IWD only queries us if it has not saved the secrets (e.g. by policy) + * or a previous attempt has failed with current secrets so it wants + * a fresh set. However if this is a new connection it may include + * all of the needed settings already so allow using these, too. + * Connection timestamp is set after activation or after first + * activation failure (to 0). + */ + if (nm_settings_connection_get_timestamp(nm_device_get_settings_connection(device), NULL)) + get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + + nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NO_SECRETS); + wifi_secrets_get_one(self, setting_name, get_secret_flags, setting_key, invocation); + + return TRUE; +} + +void +nm_device_iwd_network_add_remove(NMDeviceIwd *self, GDBusProxy *network, bool add) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + NMWifiAP * ap = NULL; + bool recheck; + nm_auto_ref_string NMRefString *bss_path = NULL; + + bss_path = nm_ref_string_new(g_dbus_proxy_get_object_path(network)); + ap = find_ap_by_supplicant_path(self, bss_path); + + /* We could schedule an update_aps(self) idle call here but up to IWD 1.9 + * when a hidden network connection is attempted, that network is initially + * only added as a Network object but not shown in GetOrderedNetworks() + * return values, and for some corner case scenarios it's beneficial to + * have that Network reflected in our ap list so that we don't attempt + * calling ConnectHiddenNetwork() on it, as that will fail in 1.9. But we + * can skip recheck-available if we're currently scanning or in the middle + * of a GetOrderedNetworks() call as that will trigger the recheck too. + */ + recheck = priv->enabled && !priv->scanning && !priv->networks_requested; + + if (!add) { + if (ap) { + ap_add_remove(self, FALSE, ap, recheck); + priv->networks_changed |= !recheck; + } + + return; + } + + if (!ap) { + ap = ap_from_network(self, + network, + bss_path, + nm_utils_get_monotonic_timestamp_msec(), + -10000); + if (!ap) + return; + + ap_add_remove(self, TRUE, ap, recheck); + g_object_unref(ap); + priv->networks_changed |= !recheck; + return; + } +} + +static void +autoconnect_changed(NMDevice *device, GParamSpec *pspec, NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + gs_unref_variant GVariant *value = NULL; + + /* Note IWD normally remains in "disconnected" during a secret request + * and we don't want to interrupt it by calling Station.Disconnect(). + */ + if (!priv->dbus_station_proxy || !priv->iwd_autoconnect + || !nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL) + || priv->wifi_secrets_id || priv->pending_agent_request) + return; + + value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State"); + if (!nm_streq(get_variant_state(value), "disconnected")) + return; + + send_disconnect(self); +} + +/*****************************************************************************/ + +static const char * +get_type_description(NMDevice *device) +{ + nm_assert(NM_IS_DEVICE_IWD(device)); + + return "wifi"; +} + +/*****************************************************************************/ + +static void +nm_device_iwd_init(NMDeviceIwd *self) +{ + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + c_list_init(&priv->aps_lst_head); + + g_signal_connect(self, "notify::" NM_DEVICE_AUTOCONNECT, G_CALLBACK(autoconnect_changed), self); + + /* Make sure the manager is running */ + (void) nm_iwd_manager_get(); +} + +NMDevice * +nm_device_iwd_new(const char *iface) +{ + return g_object_new(NM_TYPE_DEVICE_IWD, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "802.11 Wi-Fi", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_WIFI, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_WIFI, + NM_DEVICE_RFKILL_TYPE, + RFKILL_TYPE_WLAN, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceIwd * self = NM_DEVICE_IWD(object); + NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self); + + nm_clear_g_cancellable(&priv->cancellable); + + g_signal_handlers_disconnect_by_func(self, autoconnect_changed, self); + nm_device_iwd_set_dbus_object(self, NULL); + + G_OBJECT_CLASS(nm_device_iwd_parent_class)->dispose(object); + + nm_assert(c_list_is_empty(&priv->aps_lst_head)); +} + +static void +nm_device_iwd_class_init(NMDeviceIwdClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->get_property = get_property; + object_class->dispose = dispose; + + dbus_object_class->interface_infos = + NM_DBUS_INTERFACE_INFOS(&nm_interface_info_device_wireless); + + device_class->connection_type_supported = NM_SETTING_WIRELESS_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WIRELESS_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI); + + device_class->can_auto_connect = can_auto_connect; + device_class->is_available = is_available; + device_class->get_autoconnect_allowed = get_autoconnect_allowed; + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + device_class->get_enabled = get_enabled; + device_class->set_enabled = set_enabled; + device_class->get_type_description = get_type_description; + + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->get_configured_mtu = get_configured_mtu; + device_class->deactivate = deactivate; + device_class->deactivate_async = deactivate_async; + device_class->can_reapply_change = can_reapply_change; + + /* Stage 1 needed only for the set_current_ap() call. Stage 2 is + * needed if we're assuming a connection still in the "connecting" + * state or on an agent request. + */ + device_class->act_stage1_prepare_also_for_external_or_assume = TRUE; + device_class->act_stage2_config_also_for_external_or_assume = TRUE; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_MODE] = g_param_spec_uint(NM_DEVICE_IWD_MODE, + "", + "", + NM_802_11_MODE_UNKNOWN, + NM_802_11_MODE_AP, + NM_802_11_MODE_INFRA, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BITRATE] = g_param_spec_uint(NM_DEVICE_IWD_BITRATE, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ACCESS_POINTS] = + g_param_spec_boxed(NM_DEVICE_IWD_ACCESS_POINTS, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ACTIVE_ACCESS_POINT] = + g_param_spec_string(NM_DEVICE_IWD_ACTIVE_ACCESS_POINT, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_IWD_CAPABILITIES, + "", + "", + 0, + G_MAXUINT32, + NM_WIFI_DEVICE_CAP_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SCANNING] = g_param_spec_boolean(NM_DEVICE_IWD_SCANNING, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LAST_SCAN] = g_param_spec_int64(NM_DEVICE_IWD_LAST_SCAN, + "", + "", + -1, + G_MAXINT64, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wifi/nm-device-iwd.h b/src/core/devices/wifi/nm-device-iwd.h new file mode 100644 index 00000000..ce94c9ea --- /dev/null +++ b/src/core/devices/wifi/nm-device-iwd.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Intel Corporation + */ + +#ifndef __NETWORKMANAGER_DEVICE_IWD_H__ +#define __NETWORKMANAGER_DEVICE_IWD_H__ + +#include "devices/nm-device.h" +#include "nm-wifi-ap.h" +#include "nm-device-wifi.h" + +#define NM_TYPE_DEVICE_IWD (nm_device_iwd_get_type()) +#define NM_DEVICE_IWD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd)) +#define NM_DEVICE_IWD_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass)) +#define NM_IS_DEVICE_IWD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD)) +#define NM_IS_DEVICE_IWD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_IWD)) +#define NM_DEVICE_IWD_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass)) + +#define NM_DEVICE_IWD_MODE NM_DEVICE_WIFI_MODE +#define NM_DEVICE_IWD_BITRATE NM_DEVICE_WIFI_BITRATE +#define NM_DEVICE_IWD_ACCESS_POINTS NM_DEVICE_WIFI_ACCESS_POINTS +#define NM_DEVICE_IWD_ACTIVE_ACCESS_POINT NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT +#define NM_DEVICE_IWD_CAPABILITIES NM_DEVICE_WIFI_CAPABILITIES +#define NM_DEVICE_IWD_SCANNING NM_DEVICE_WIFI_SCANNING +#define NM_DEVICE_IWD_LAST_SCAN NM_DEVICE_WIFI_LAST_SCAN + +typedef struct _NMDeviceIwd NMDeviceIwd; +typedef struct _NMDeviceIwdClass NMDeviceIwdClass; + +GType nm_device_iwd_get_type(void); + +NMDevice *nm_device_iwd_new(const char *iface); + +void nm_device_iwd_set_dbus_object(NMDeviceIwd *device, GDBusObject *object); + +gboolean nm_device_iwd_agent_query(NMDeviceIwd *device, GDBusMethodInvocation *invocation); + +const CList *_nm_device_iwd_get_aps(NMDeviceIwd *self); + +void _nm_device_iwd_request_scan(NMDeviceIwd * self, + GVariant * options, + GDBusMethodInvocation *invocation); + +void nm_device_iwd_network_add_remove(NMDeviceIwd *device, GDBusProxy *network, bool add); + +#endif /* __NETWORKMANAGER_DEVICE_IWD_H__ */ diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.c b/src/core/devices/wifi/nm-device-olpc-mesh.c new file mode 100644 index 00000000..af83c4a3 --- /dev/null +++ b/src/core/devices/wifi/nm-device-olpc-mesh.c @@ -0,0 +1,551 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Dan Williams <dcbw@redhat.com> + * Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * Daniel Drake <dsd@laptop.org> + * Copyright (C) 2005 - 2014 Red Hat, Inc. + * Copyright (C) 2008 Collabora Ltd. + * Copyright (C) 2009 One Laptop per Child + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-olpc-mesh.h" + +#include <netinet/in.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> +#include <sys/ioctl.h> + +#include "devices/nm-device.h" +#include "nm-device-wifi.h" +#include "devices/nm-device-private.h" +#include "nm-utils.h" +#include "NetworkManagerUtils.h" +#include "nm-act-request.h" +#include "nm-setting-connection.h" +#include "nm-setting-olpc-mesh.h" +#include "nm-manager.h" +#include "platform/nm-platform.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceOlpcMesh +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceOlpcMesh, PROP_COMPANION, PROP_ACTIVE_CHANNEL, ); + +typedef struct { + NMDevice * companion; + NMManager *manager; + bool stage1_waiting : 1; +} NMDeviceOlpcMeshPrivate; + +struct _NMDeviceOlpcMesh { + NMDevice parent; + NMDeviceOlpcMeshPrivate _priv; +}; + +struct _NMDeviceOlpcMeshClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceOlpcMesh, nm_device_olpc_mesh, NM_TYPE_DEVICE) + +#define NM_DEVICE_OLPC_MESH_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceOlpcMesh, NM_IS_DEVICE_OLPC_MESH, NMDevice) + +/*****************************************************************************/ + +static gboolean +get_autoconnect_allowed(NMDevice *device) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(device); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + /* We can't even connect if we don't have a companion yet. */ + if (!priv->companion) + return FALSE; + + /* We must not attempt to autoconnect when the companion is connected or + * connecting, * because we'd tear down its connection. */ + if (nm_device_get_state(priv->companion) > NM_DEVICE_STATE_DISCONNECTED) + return FALSE; + + return TRUE; +} + +#define DEFAULT_SSID "olpc-mesh" + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMSettingOlpcMesh *s_mesh; + + s_mesh = nm_connection_get_setting_olpc_mesh(connection); + if (!s_mesh) { + s_mesh = (NMSettingOlpcMesh *) nm_setting_olpc_mesh_new(); + nm_connection_add_setting(connection, NM_SETTING(s_mesh)); + } + + if (!nm_setting_olpc_mesh_get_ssid(s_mesh)) { + gs_unref_bytes GBytes *ssid = NULL; + + ssid = g_bytes_new_static(DEFAULT_SSID, NM_STRLEN(DEFAULT_SSID)); + g_object_set(G_OBJECT(s_mesh), NM_SETTING_OLPC_MESH_SSID, ssid, NULL); + } + + if (!nm_setting_olpc_mesh_get_dhcp_anycast_address(s_mesh)) { + const char *anycast = "c0:27:c0:27:c0:27"; + + g_object_set(G_OBJECT(s_mesh), NM_SETTING_OLPC_MESH_DHCP_ANYCAST_ADDRESS, anycast, NULL); + } + + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_OLPC_MESH_SETTING_NAME, + existing_connections, + NULL, + _("Mesh"), + NULL, + NULL, + FALSE); /* No IPv6 by default */ + + return TRUE; +} + +/*****************************************************************************/ + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(device); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + /* disconnect companion device, if it is connected */ + if (nm_device_get_act_request(NM_DEVICE(priv->companion))) { + _LOGI(LOGD_OLPC, "disconnecting companion device %s", nm_device_get_iface(priv->companion)); + /* FIXME: VPN stuff here is a bug; but we can't really change API now... */ + nm_device_state_changed(NM_DEVICE(priv->companion), + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + _LOGI(LOGD_OLPC, "companion %s disconnected", nm_device_get_iface(priv->companion)); + } + + /* wait with continuing configuration until the companion device is done scanning */ + if (nm_device_wifi_get_scanning(NM_DEVICE_WIFI(priv->companion))) { + priv->stage1_waiting = TRUE; + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + priv->stage1_waiting = FALSE; + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static gboolean +_mesh_set_channel(NMDeviceOlpcMesh *self, guint32 channel) +{ + NMPlatform *platform; + int ifindex = nm_device_get_ifindex(NM_DEVICE(self)); + guint32 old_channel; + + platform = nm_device_get_platform(NM_DEVICE(self)); + old_channel = nm_platform_mesh_get_channel(platform, ifindex); + + if (channel == 0) + channel = old_channel; + + /* We want to call this even if the channel number is the same, + * because that actually starts the mesh with the configured mesh ID. */ + if (!nm_platform_mesh_set_channel(platform, ifindex, channel)) + return FALSE; + + if (old_channel != channel) + _notify(self, PROP_ACTIVE_CHANNEL); + + return TRUE; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(device); + NMSettingOlpcMesh *s_mesh; + GBytes * ssid; + const char * anycast_addr; + gboolean success; + + s_mesh = nm_device_get_applied_setting(device, NM_TYPE_SETTING_OLPC_MESH); + g_return_val_if_fail(s_mesh, NM_ACT_STAGE_RETURN_FAILURE); + + ssid = nm_setting_olpc_mesh_get_ssid(s_mesh); + + nm_device_take_down(NM_DEVICE(self), TRUE); + success = nm_platform_mesh_set_ssid(nm_device_get_platform(device), + nm_device_get_ifindex(device), + g_bytes_get_data(ssid, NULL), + g_bytes_get_size(ssid)); + nm_device_bring_up(NM_DEVICE(self), TRUE, NULL); + if (!success) { + _LOGW(LOGD_WIFI, "Unable to set the mesh ID"); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + anycast_addr = nm_setting_olpc_mesh_get_dhcp_anycast_address(s_mesh); + nm_device_set_dhcp_anycast_address(device, anycast_addr); + + if (!_mesh_set_channel(self, nm_setting_olpc_mesh_get_channel(s_mesh))) { + _LOGW(LOGD_WIFI, "Unable to set the mesh channel"); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(device); + + if (!NM_DEVICE_OLPC_MESH_GET_PRIVATE(self)->companion) { + _LOGD(LOGD_WIFI, "not available because companion not found"); + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static void +companion_cleanup(NMDeviceOlpcMesh *self) +{ + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + if (priv->companion) { + nm_device_wifi_scanning_prohibited_track(NM_DEVICE_WIFI(priv->companion), self, FALSE); + g_signal_handlers_disconnect_by_data(priv->companion, self); + g_clear_object(&priv->companion); + } + _notify(self, PROP_COMPANION); +} + +static void +companion_notify_cb(NMDeviceWifi *companion, GParamSpec *pspec, gpointer user_data) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(user_data); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + nm_assert(NM_IS_DEVICE_WIFI(companion)); + nm_assert(priv->companion == (gpointer) companion); + + if (!priv->stage1_waiting) + return; + + if (!nm_device_wifi_get_scanning(NM_DEVICE_WIFI(companion))) { + priv->stage1_waiting = FALSE; + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); + } +} + +/* disconnect from mesh if someone starts using the companion */ +static void +companion_state_changed_cb(NMDeviceWifi * companion, + NMDeviceState state, + NMDeviceState old_state, + NMDeviceStateReason reason, + gpointer user_data) +{ + NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(user_data); + NMDeviceState self_state = nm_device_get_state(NM_DEVICE(self)); + + if (old_state > NM_DEVICE_STATE_DISCONNECTED && state <= NM_DEVICE_STATE_DISCONNECTED) { + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + } + + if (self_state < NM_DEVICE_STATE_PREPARE || self_state > NM_DEVICE_STATE_ACTIVATED + || state < NM_DEVICE_STATE_PREPARE || state > NM_DEVICE_STATE_ACTIVATED) + return; + + _LOGD(LOGD_OLPC, "disconnecting mesh due to companion connectivity"); + /* FIXME: VPN stuff here is a bug; but we can't really change API now... */ + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_USER_REQUESTED); +} + +static gboolean +companion_autoconnect_allowed_cb(NMDeviceWifi *companion, gpointer user_data) +{ + NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(user_data); + NMDeviceState state = nm_device_get_state(NM_DEVICE(self)); + + /* Don't allow the companion to autoconnect while a mesh connection is + * active */ + return (state < NM_DEVICE_STATE_PREPARE) || (state > NM_DEVICE_STATE_ACTIVATED); +} + +static gboolean +check_companion(NMDeviceOlpcMesh *self, NMDevice *other) +{ + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + const char * my_addr, *their_addr; + + if (!NM_IS_DEVICE_WIFI(other)) + return FALSE; + + my_addr = nm_device_get_hw_address(NM_DEVICE(self)); + their_addr = nm_device_get_hw_address(other); + if (!nm_utils_hwaddr_matches(my_addr, -1, their_addr, -1)) + return FALSE; + + nm_assert(priv->companion == NULL); + priv->companion = g_object_ref(other); + + _LOGI(LOGD_OLPC, "found companion Wi-Fi device %s", nm_device_get_iface(other)); + + g_signal_connect(G_OBJECT(other), + NM_DEVICE_STATE_CHANGED, + G_CALLBACK(companion_state_changed_cb), + self); + + g_signal_connect(G_OBJECT(other), + "notify::" NM_DEVICE_WIFI_SCANNING, + G_CALLBACK(companion_notify_cb), + self); + + g_signal_connect(G_OBJECT(other), + NM_DEVICE_AUTOCONNECT_ALLOWED, + G_CALLBACK(companion_autoconnect_allowed_cb), + self); + + _notify(self, PROP_COMPANION); + + return TRUE; +} + +static void +device_added_cb(NMManager *manager, NMDevice *other, gpointer user_data) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(user_data); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + if (!priv->companion && check_companion(self, other)) { + nm_device_queue_recheck_available(NM_DEVICE(self), + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_NONE); + nm_device_remove_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_COMPANION, + FALSE); + } +} + +static void +device_removed_cb(NMManager *manager, NMDevice *other, gpointer user_data) +{ + NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(user_data); + + if (other == NM_DEVICE_OLPC_MESH_GET_PRIVATE(self)->companion) + companion_cleanup(self); +} + +static void +find_companion(NMDeviceOlpcMesh *self) +{ + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + const CList * tmp_lst; + NMDevice * candidate; + + if (priv->companion) + return; + + nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WAITING_FOR_COMPANION, TRUE); + + /* Try to find the companion if it's already known to the NMManager */ + nm_manager_for_each_device (priv->manager, candidate, tmp_lst) { + if (check_companion(self, candidate)) { + nm_device_queue_recheck_available(NM_DEVICE(self), + NM_DEVICE_STATE_REASON_NONE, + NM_DEVICE_STATE_REASON_NONE); + nm_device_remove_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_COMPANION, + TRUE); + break; + } + } +} + +static void +state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(device); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + if (new_state == NM_DEVICE_STATE_UNAVAILABLE) + find_companion(self); + + if (priv->companion) { + gboolean temporarily_prohibited = FALSE; + + if (new_state >= NM_DEVICE_STATE_PREPARE && new_state <= NM_DEVICE_STATE_IP_CONFIG) { + /* Don't allow the companion to scan while configuring the mesh interface */ + temporarily_prohibited = TRUE; + } + nm_device_wifi_scanning_prohibited_track(NM_DEVICE_WIFI(priv->companion), + self, + temporarily_prohibited); + } +} + +static guint32 +get_dhcp_timeout_for_device(NMDevice *device, int addr_family) +{ + /* shorter timeout for mesh connectivity */ + return 20; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(object); + NMDevice * device = NM_DEVICE(self); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_COMPANION: + nm_dbus_utils_g_value_set_object_path(value, priv->companion); + break; + case PROP_ACTIVE_CHANNEL: + g_value_set_uint(value, + nm_platform_mesh_get_channel(nm_device_get_platform(device), + nm_device_get_ifindex(device))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_olpc_mesh_init(NMDeviceOlpcMesh *self) +{} + +static void +constructed(GObject *object) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(object); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + G_OBJECT_CLASS(nm_device_olpc_mesh_parent_class)->constructed(object); + + priv->manager = g_object_ref(NM_MANAGER_GET); + + g_signal_connect(priv->manager, NM_MANAGER_DEVICE_ADDED, G_CALLBACK(device_added_cb), self); + g_signal_connect(priv->manager, NM_MANAGER_DEVICE_REMOVED, G_CALLBACK(device_removed_cb), self); +} + +NMDevice * +nm_device_olpc_mesh_new(const char *iface) +{ + return g_object_new(NM_TYPE_DEVICE_OLPC_MESH, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "802.11 OLPC Mesh", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_OLPC_MESH, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_OLPC_MESH, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(object); + NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self); + + companion_cleanup(self); + + if (priv->manager) { + g_signal_handlers_disconnect_by_func(priv->manager, G_CALLBACK(device_added_cb), self); + g_signal_handlers_disconnect_by_func(priv->manager, G_CALLBACK(device_removed_cb), self); + g_clear_object(&priv->manager); + } + + G_OBJECT_CLASS(nm_device_olpc_mesh_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_olpc_mesh = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_OLPC_MESH, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Companion", + "o", + NM_DEVICE_OLPC_MESH_COMPANION), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L( + "ActiveChannel", + "u", + NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_olpc_mesh_class_init(NMDeviceOlpcMeshClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->dispose = dispose; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_olpc_mesh); + + device_class->connection_type_supported = NM_SETTING_OLPC_MESH_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_OLPC_MESH_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_OLPC_MESH); + + device_class->get_autoconnect_allowed = get_autoconnect_allowed; + device_class->complete_connection = complete_connection; + device_class->is_available = is_available; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->state_changed = state_changed; + device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device; + + obj_properties[PROP_COMPANION] = g_param_spec_string(NM_DEVICE_OLPC_MESH_COMPANION, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ACTIVE_CHANNEL] = + g_param_spec_uint(NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.h b/src/core/devices/wifi/nm-device-olpc-mesh.h new file mode 100644 index 00000000..79b7fd5d --- /dev/null +++ b/src/core/devices/wifi/nm-device-olpc-mesh.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Dan Williams <dcbw@redhat.com> + * Sjoerd Simons <sjoerd.simons@collabora.co.uk> + * Daniel Drake <dsd@laptop.org> + * Copyright (C) 2005 Red Hat, Inc. + * Copyright (C) 2008 Collabora Ltd. + * Copyright (C) 2009 One Laptop per Child + */ + +#ifndef __NETWORKMANAGER_DEVICE_OLPC_MESH_H__ +#define __NETWORKMANAGER_DEVICE_OLPC_MESH_H__ + +#include "devices/nm-device.h" + +#define NM_TYPE_DEVICE_OLPC_MESH (nm_device_olpc_mesh_get_type()) +#define NM_DEVICE_OLPC_MESH(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh)) +#define NM_DEVICE_OLPC_MESH_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass)) +#define NM_IS_DEVICE_OLPC_MESH(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OLPC_MESH)) +#define NM_IS_DEVICE_OLPC_MESH_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OLPC_MESH)) +#define NM_DEVICE_OLPC_MESH_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass)) + +#define NM_DEVICE_OLPC_MESH_COMPANION "companion" +#define NM_DEVICE_OLPC_MESH_BITRATE "bitrate" +#define NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL "active-channel" + +typedef struct _NMDeviceOlpcMesh NMDeviceOlpcMesh; +typedef struct _NMDeviceOlpcMeshClass NMDeviceOlpcMeshClass; + +GType nm_device_olpc_mesh_get_type(void); + +NMDevice *nm_device_olpc_mesh_new(const char *iface); + +#endif /* __NETWORKMANAGER_DEVICE_OLPC_MESH_H__ */ diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c new file mode 100644 index 00000000..fb987600 --- /dev/null +++ b/src/core/devices/wifi/nm-device-wifi-p2p.c @@ -0,0 +1,1280 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-wifi-p2p.h" + +#include <sys/socket.h> + +#include "supplicant/nm-supplicant-manager.h" +#include "supplicant/nm-supplicant-interface.h" + +#include "NetworkManagerUtils.h" +#include "devices/nm-device-private.h" +#include "nm-act-request.h" +#include "nm-core-internal.h" +#include "nm-glib-aux/nm-ref-string.h" +#include "nm-ip4-config.h" +#include "nm-manager.h" +#include "nm-manager.h" +#include "nm-setting-wifi-p2p.h" +#include "nm-utils.h" +#include "nm-wifi-p2p-peer.h" +#include "platform/nm-platform.h" +#include "platform/nmp-object.h" +#include "settings/nm-settings.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceWifiP2P +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWifiP2P, PROP_PEERS, ); + +typedef struct { + NMSupplicantManager *sup_mgr; + + /* NOTE: In theory management and group ifaces could be identical. However, + * in practice, this cannot happen currently as NMDeviceWifiP2P is only + * created for existing non-P2P interfaces. + * (i.e. a single standalone P2P interface is not supported at this point) + */ + NMSupplicantInterface *mgmt_iface; + NMSupplicantInterface *group_iface; + + CList peers_lst_head; + + guint find_peer_timeout_id; + guint sup_timeout_id; + guint peer_dump_id; + guint peer_missing_id; + + bool is_waiting_for_supplicant : 1; +} NMDeviceWifiP2PPrivate; + +struct _NMDeviceWifiP2P { + NMDevice parent; + NMDeviceWifiP2PPrivate _priv; +}; + +struct _NMDeviceWifiP2PClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceWifiP2P, nm_device_wifi_p2p, NM_TYPE_DEVICE) + +#define NM_DEVICE_WIFI_P2P_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceWifiP2P, NM_IS_DEVICE_WIFI_P2P, NMDevice) + +/*****************************************************************************/ + +static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p; +static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_added; +static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_removed; + +static void supplicant_group_interface_release(NMDeviceWifiP2P *self); +static void supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting); + +/*****************************************************************************/ + +static void +_peer_dump(NMDeviceWifiP2P * self, + NMLogLevel log_level, + const NMWifiP2PPeer *peer, + const char * prefix, + gint32 now_s) +{ + char buf[1024]; + + _NMLOG(log_level, + LOGD_WIFI_SCAN, + "wifi-peer: %-7s %s", + prefix, + nm_wifi_p2p_peer_to_string(peer, buf, sizeof(buf), now_s)); +} + +static gboolean +peer_list_dump(gpointer user_data) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(user_data); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + priv->peer_dump_id = 0; + + if (_LOGD_ENABLED(LOGD_WIFI_SCAN)) { + NMWifiP2PPeer *peer; + gint32 now_s = nm_utils_get_monotonic_timestamp_sec(); + + _LOGD(LOGD_WIFI_SCAN, "P2P Peers: [now:%u]", now_s); + c_list_for_each_entry (peer, &priv->peers_lst_head, peers_lst) + _peer_dump(self, LOGL_DEBUG, peer, "dump", now_s); + } + return G_SOURCE_REMOVE; +} + +static void +schedule_peer_list_dump(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (!priv->peer_dump_id && _LOGD_ENABLED(LOGD_WIFI_SCAN)) + priv->peer_dump_id = g_timeout_add_seconds(1, peer_list_dump, self); +} + +/*****************************************************************************/ + +static void +_set_is_waiting_for_supplicant(NMDeviceWifiP2P *self, gboolean is_waiting) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (priv->is_waiting_for_supplicant == (!!is_waiting)) + return; + + priv->is_waiting_for_supplicant = is_waiting; + + if (is_waiting) + nm_device_add_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, + TRUE); + else + nm_device_remove_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, + TRUE); +} + +/*****************************************************************************/ + +static gboolean +check_connection_peer_joined(NMDeviceWifiP2P *device) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(device); + NMConnection * conn = nm_device_get_applied_connection(NM_DEVICE(device)); + NMWifiP2PPeer * peer; + const char * group; + const char *const * groups; + + if (!conn || !priv->group_iface) + return FALSE; + + /* Comparing the object path found on the group_iface with the peers + * found on the mgmt_iface is legal. */ + group = nm_supplicant_interface_get_p2p_group_path(priv->group_iface); + if (!group) + return FALSE; + + /* NOTE: We currently only support connections to a specific peer */ + peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, conn); + if (!peer) + return FALSE; + + groups = nm_wifi_p2p_peer_get_groups(peer); + if (!groups || !g_strv_contains(groups, group)) + return FALSE; + + return TRUE; +} + +static gboolean +disconnect_on_connection_peer_missing_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + _LOGW(LOGD_WIFI, "Peer requested in connection is missing for too long, failing connection."); + + priv->peer_missing_id = 0; + + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND); + return FALSE; +} + +static void +update_disconnect_on_connection_peer_missing(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMDeviceState state; + + state = nm_device_get_state(NM_DEVICE(self)); + if (state < NM_DEVICE_STATE_IP_CONFIG || state > NM_DEVICE_STATE_ACTIVATED) { + nm_clear_g_source(&priv->peer_missing_id); + return; + } + + if (check_connection_peer_joined(self)) { + if (nm_clear_g_source(&priv->peer_missing_id)) + _LOGD(LOGD_WIFI, "Peer requested in connection is joined, removing timeout"); + return; + } + + if (priv->peer_missing_id == 0) { + _LOGD(LOGD_WIFI, "Peer requested in connection is missing, adding timeout"); + priv->peer_missing_id = + g_timeout_add_seconds(5, disconnect_on_connection_peer_missing_cb, self); + } +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate * priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMSupplicantInterfaceState supplicant_state; + + if (!priv->mgmt_iface) + return FALSE; + + supplicant_state = nm_supplicant_interface_get_state(priv->mgmt_iface); + return nm_supplicant_interface_state_is_operational(supplicant_state); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + if (!NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + /* TODO: Allow limitting the interface using the HW-address? */ + + /* We don't need to check anything else here. The P2P device will only + * exists if we are able to establish a P2P connection, and there should + * be no further restrictions necessary. + */ + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + gs_free char * setting_name = NULL; + NMSettingWifiP2P *s_wifi_p2p; + NMWifiP2PPeer * peer; + const char * setting_peer; + + s_wifi_p2p = + NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)); + + if (!specific_object) { + /* If not given a specific object, we need at minimum a peer address */ + if (!s_wifi_p2p) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A '%s' setting is required if no Peer path was given", + NM_SETTING_WIFI_P2P_SETTING_NAME); + return FALSE; + } + + setting_peer = nm_setting_wifi_p2p_get_peer(s_wifi_p2p); + if (!setting_peer) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A '%s' setting with a valid Peer is required if no Peer path was given", + NM_SETTING_WIFI_P2P_SETTING_NAME); + return FALSE; + } + + } else { + peer = nm_wifi_p2p_peer_lookup_for_device(NM_DEVICE(self), specific_object); + if (!peer) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND, + "The P2P peer %s is unknown", + specific_object); + return FALSE; + } + + setting_peer = nm_wifi_p2p_peer_get_address(peer); + g_return_val_if_fail(setting_peer, FALSE); + } + + /* Add a Wi-Fi P2P setting if one doesn't exist yet */ + if (!s_wifi_p2p) { + s_wifi_p2p = NM_SETTING_WIFI_P2P(nm_setting_wifi_p2p_new()); + nm_connection_add_setting(connection, NM_SETTING(s_wifi_p2p)); + } + + g_object_set(G_OBJECT(s_wifi_p2p), NM_SETTING_WIFI_P2P_PEER, setting_peer, NULL); + + setting_name = g_strdup_printf("Wi-Fi P2P Peer %s", setting_peer); + nm_utils_complete_generic(nm_device_get_platform(device), + connection, + NM_SETTING_WIFI_P2P_SETTING_NAME, + existing_connections, + setting_name, + setting_name, + NULL, + NULL, + TRUE); + + return TRUE; +} + +/* + * supplicant_find_timeout_cb + * + * Called when the supplicant has been unable to find the peer we want to connect to. + */ +static gboolean +supplicant_find_timeout_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(user_data); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + priv->find_peer_timeout_id = 0; + + nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface); + + if (nm_device_is_activating(device)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi-p2p) could not find peer, failing activation"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_PEER_NOT_FOUND); + } + + return G_SOURCE_REMOVE; +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMConnection * connection; + NMSettingWifiP2P * s_wifi_p2p; + NMWifiP2PPeer * peer; + + if (!priv->mgmt_iface) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + s_wifi_p2p = + NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)); + g_return_val_if_fail(s_wifi_p2p, NM_ACT_STAGE_RETURN_FAILURE); + + peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection); + if (!peer) { + /* Set up a timeout on the find attempt and run a find for the same period of time */ + if (priv->find_peer_timeout_id == 0) { + priv->find_peer_timeout_id = + g_timeout_add_seconds(10, supplicant_find_timeout_cb, self); + + nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, 10); + } + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +/* + * supplicant_connection_timeout_cb + * + * Called when the supplicant has been unable to connect to a peer + * within a specified period of time. + */ +static gboolean +supplicant_connection_timeout_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(user_data); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + priv->sup_timeout_id = 0; + + nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface); + + if (nm_device_is_activating(device)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi-p2p) connecting took too long, failing activation"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT); + } + + return G_SOURCE_REMOVE; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMConnection * connection; + NMSettingWifiP2P * s_wifi_p2p; + NMWifiP2PPeer * peer; + GBytes * wfd_ies; + + if (nm_clear_g_source(&priv->find_peer_timeout_id)) + nm_assert_not_reached(); + + if (!priv->mgmt_iface) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + connection = nm_device_get_applied_connection(device); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + nm_assert( + NM_IS_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P))); + + /* The prepare stage ensures that the peer has been found */ + peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection); + if (!peer) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + /* Set the WFD IEs before trying to establish the connection. */ + s_wifi_p2p = + NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)); + wfd_ies = nm_setting_wifi_p2p_get_wfd_ies(s_wifi_p2p); + nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, wfd_ies); + + /* TODO: Grab secrets if we don't have them yet! */ + + /* TODO: Fix "pbc" being hardcoded here! */ + nm_supplicant_interface_p2p_connect(priv->mgmt_iface, + nm_wifi_p2p_peer_get_supplicant_path(peer), + "pbc", + NULL); + + /* Set up a timeout on the connect attempt */ + if (priv->sup_timeout_id == 0) { + priv->sup_timeout_id = g_timeout_add_seconds(45, supplicant_connection_timeout_cb, self); + } + + /* We'll get stage3 started when the P2P group has been started */ + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static void +emit_signal_p2p_peer_add_remove(NMDeviceWifiP2P *device, + NMWifiP2PPeer * peer, + gboolean is_added /* or else is_removed */) +{ + nm_dbus_object_emit_signal(NM_DBUS_OBJECT(device), + &interface_info_device_wifi_p2p, + is_added ? &nm_signal_info_wifi_p2p_peer_added + : &nm_signal_info_wifi_p2p_peer_removed, + "(o)", + nm_dbus_object_get_path(NM_DBUS_OBJECT(peer))); +} + +static void +peer_add_remove(NMDeviceWifiP2P *self, + gboolean is_adding, /* or else removing */ + NMWifiP2PPeer * peer, + gboolean recheck_available_connections) +{ + NMDevice * device = NM_DEVICE(self); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (is_adding) { + g_object_ref(peer); + peer->wifi_device = device; + c_list_link_tail(&priv->peers_lst_head, &peer->peers_lst); + nm_dbus_object_export(NM_DBUS_OBJECT(peer)); + _peer_dump(self, LOGL_DEBUG, peer, "added", 0); + + emit_signal_p2p_peer_add_remove(self, peer, TRUE); + } else { + peer->wifi_device = NULL; + c_list_unlink(&peer->peers_lst); + _peer_dump(self, LOGL_DEBUG, peer, "removed", 0); + } + + _notify(self, PROP_PEERS); + + if (!is_adding) { + emit_signal_p2p_peer_add_remove(self, peer, FALSE); + nm_dbus_object_clear_and_unexport(&peer); + } + + if (is_adding) { + /* If we are in prepare state, then we are currently runnign a find + * to search for the requested peer. */ + if (priv->find_peer_timeout_id != 0) { + NMConnection *connection; + + nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_PREPARE); + + connection = nm_device_get_applied_connection(device); + nm_assert(NM_IS_CONNECTION(connection)); + + peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection); + if (peer) { + /* A peer for the connection was found, cancel the timeout and go to configure state. */ + nm_clear_g_source(&priv->find_peer_timeout_id); + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); + } + } + + /* TODO: We may want to re-check auto-activation here, otherwise it will never work. */ + } + + update_disconnect_on_connection_peer_missing(self); +} + +static void +remove_all_peers(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMWifiP2PPeer * peer; + + if (c_list_is_empty(&priv->peers_lst_head)) + return; + + while ((peer = c_list_first_entry(&priv->peers_lst_head, NMWifiP2PPeer, peers_lst))) + peer_add_remove(self, FALSE, peer, FALSE); + + nm_device_recheck_available_connections(NM_DEVICE(self)); +} + +/*****************************************************************************/ + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + gboolean indicate_addressing_running; + NMConnection *connection; + const char * method; + + connection = nm_device_get_applied_connection(device); + + method = nm_utils_get_ip_config_method(connection, addr_family); + + if (addr_family == AF_INET) + indicate_addressing_running = NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO); + else { + indicate_addressing_running = NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_DHCP); + } + + if (indicate_addressing_running) + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + TRUE); + + return NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + int ifindex = nm_device_get_ip_ifindex(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + nm_clear_g_source(&priv->find_peer_timeout_id); + nm_clear_g_source(&priv->sup_timeout_id); + nm_clear_g_source(&priv->peer_missing_id); + + if (priv->mgmt_iface) + nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface); + + if (priv->group_iface) + nm_supplicant_interface_p2p_disconnect(priv->group_iface); + + /* Clear any critical protocol notification in the Wi-Fi stack */ + if (ifindex > 0) + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + ifindex, + FALSE); +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + *out_source = NM_DEVICE_MTU_SOURCE_NONE; + return 0; +} + +static const char * +get_auto_ip_config_method(NMDevice *device, int addr_family) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + /* Override the AUTO method to mean shared if we are group owner. */ + if (priv->group_iface && nm_supplicant_interface_get_p2p_group_owner(priv->group_iface)) { + if (addr_family == AF_INET) + return NM_SETTING_IP4_CONFIG_METHOD_SHARED; + + if (addr_family == AF_INET6) + return NM_SETTING_IP6_CONFIG_METHOD_SHARED; + } + + return NULL; +} + +static gboolean +unmanaged_on_quit(NMDevice *self) +{ + return TRUE; +} + +static void +supplicant_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(user_data); + NMDevice * device = NM_DEVICE(self); + NMSupplicantInterfaceState new_state = new_state_i; + NMSupplicantInterfaceState old_state = old_state_i; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "supplicant management interface state: %s -> %s", + nm_supplicant_interface_state_to_string(old_state), + nm_supplicant_interface_state_to_string(new_state)); + + if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) { + supplicant_interfaces_release(self, TRUE); + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + _LOGD(LOGD_WIFI, "supplicant ready"); + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + _set_is_waiting_for_supplicant(self, FALSE); + } +} + +static void +supplicant_iface_peer_changed_cb(NMSupplicantInterface *iface, + NMSupplicantPeerInfo * peer_info, + gboolean is_present, + NMDeviceWifiP2P * self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + NMWifiP2PPeer * found_peer; + + found_peer = + nm_wifi_p2p_peers_find_by_supplicant_path(&priv->peers_lst_head, peer_info->peer_path->str); + + if (!is_present) { + if (!found_peer) + return; + + peer_add_remove(self, FALSE, found_peer, TRUE); + goto out; + } + + if (found_peer) { + if (!nm_wifi_p2p_peer_update_from_properties(found_peer, peer_info)) + return; + + update_disconnect_on_connection_peer_missing(self); + _peer_dump(self, LOGL_DEBUG, found_peer, "updated", 0); + } else { + gs_unref_object NMWifiP2PPeer *peer = NULL; + + peer = nm_wifi_p2p_peer_new_from_properties(peer_info); + peer_add_remove(self, TRUE, peer, TRUE); + } + +out: + schedule_peer_list_dump(self); +} + +static void +check_group_iface_ready(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (!priv->group_iface) + return; + + if (!nm_supplicant_interface_state_is_operational( + nm_supplicant_interface_get_state(priv->group_iface))) + return; + + if (!nm_supplicant_interface_get_p2p_group_joined(priv->group_iface)) + return; + + nm_clear_g_source(&priv->sup_timeout_id); + update_disconnect_on_connection_peer_missing(self); + + nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self)); +} + +static void +supplicant_group_iface_is_ready(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + _LOGD(LOGD_WIFI, "P2P Group supplicant ready"); + + if (!nm_device_set_ip_iface(NM_DEVICE(self), + nm_supplicant_interface_get_ifname(priv->group_iface))) { + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + return; + } + + _set_is_waiting_for_supplicant(self, FALSE); + check_group_iface_ready(self); +} + +static void +supplicant_group_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(user_data); + NMSupplicantInterfaceState new_state = new_state_i; + NMSupplicantInterfaceState old_state = old_state_i; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "P2P Group supplicant interface state: %s -> %s", + nm_supplicant_interface_state_to_string(old_state), + nm_supplicant_interface_state_to_string(new_state)); + + if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) { + supplicant_group_interface_release(self); + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + return; + } + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + supplicant_group_iface_is_ready(self); + return; + } +} + +static void +supplicant_group_iface_group_finished_cb(NMSupplicantInterface *iface, + const char * iface_path, + void * user_data) +{ + NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data); + + supplicant_group_interface_release(self); + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); +} + +static void +supplicant_iface_group_joined_updated_cb(NMSupplicantInterface *iface, + GParamSpec * pspec, + void * user_data) +{ + NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data); + + check_group_iface_ready(self); +} + +static void +supplicant_iface_group_started_cb(NMSupplicantInterface *iface, + NMSupplicantInterface *group_iface, + NMDeviceWifiP2P * self) +{ + NMDeviceWifiP2PPrivate * priv; + NMSupplicantInterfaceState state; + + g_return_if_fail(self); + + if (!nm_device_is_activating(NM_DEVICE(self))) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "P2P: WPA supplicant notified a group start but we are not trying to connect! " + "Ignoring the event."); + return; + } + + priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + supplicant_group_interface_release(self); + + priv->group_iface = g_object_ref(group_iface); + + /* We need to wait for the interface to be ready and the group + * information to be resolved. */ + g_signal_connect(priv->group_iface, + "notify::" NM_SUPPLICANT_INTERFACE_P2P_GROUP_JOINED, + G_CALLBACK(supplicant_iface_group_joined_updated_cb), + self); + + g_signal_connect(priv->group_iface, + NM_SUPPLICANT_INTERFACE_STATE, + G_CALLBACK(supplicant_group_iface_state_cb), + self); + + g_signal_connect(priv->group_iface, + NM_SUPPLICANT_INTERFACE_GROUP_FINISHED, + G_CALLBACK(supplicant_group_iface_group_finished_cb), + self); + + state = nm_supplicant_interface_get_state(priv->group_iface); + if (state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + _set_is_waiting_for_supplicant(self, TRUE); + return; + } + + supplicant_group_iface_is_ready(self); +} + +static void +supplicant_group_interface_release(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (!priv->group_iface) + return; + + g_signal_handlers_disconnect_by_data(priv->group_iface, self); + + nm_supplicant_interface_p2p_disconnect(priv->group_iface); + + g_clear_object(&priv->group_iface); +} + +static void +supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + nm_clear_g_source(&priv->peer_dump_id); + + remove_all_peers(self); + + if (priv->mgmt_iface) { + _LOGD(LOGD_DEVICE | LOGD_WIFI, "P2P: Releasing WPA supplicant interface."); + nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL); + g_signal_handlers_disconnect_by_data(priv->mgmt_iface, self); + g_clear_object(&priv->mgmt_iface); + nm_clear_g_source(&priv->find_peer_timeout_id); + nm_clear_g_source(&priv->sup_timeout_id); + } + + supplicant_group_interface_release(self); + + if (set_is_waiting) + _set_is_waiting_for_supplicant(self, TRUE); +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(device); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + update_disconnect_on_connection_peer_missing(self); + + if (new_state <= NM_DEVICE_STATE_UNAVAILABLE) { + /* Clean up the supplicant interface because in these states the + * device cannot be used. + * Do not clean up for the UNMANAGED to UNAVAILABLE transition which + * will happen during initialization. + */ + if (priv->mgmt_iface && old_state > new_state) + supplicant_interfaces_release(self, TRUE); + + /* TODO: More cleanup needed? */ + } + + switch (new_state) { + case NM_DEVICE_STATE_UNMANAGED: + break; + case NM_DEVICE_STATE_UNAVAILABLE: + if (!priv->mgmt_iface + || !nm_supplicant_interface_state_is_operational( + nm_supplicant_interface_get_state(priv->mgmt_iface))) + _set_is_waiting_for_supplicant(self, TRUE); + break; + case NM_DEVICE_STATE_NEED_AUTH: + /* Disconnect? */ + break; + case NM_DEVICE_STATE_IP_CHECK: + /* Clear any critical protocol notification in the wifi stack */ + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + FALSE); + break; + case NM_DEVICE_STATE_ACTIVATED: + //activation_success_handler (device); + break; + case NM_DEVICE_STATE_FAILED: + /* Clear any critical protocol notification in the wifi stack. + * At this point the IP device may have been removed already. */ + nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL); + if (nm_device_get_ip_ifindex(device) > 0) + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + FALSE); + break; + case NM_DEVICE_STATE_DISCONNECTED: + nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL); + break; + default: + break; + } +} + +static void +impl_device_wifi_p2p_start_find(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(obj); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + gs_unref_variant GVariant *options = NULL; + const char * opts_key; + GVariant * opts_val; + GVariantIter iter; + gint32 timeout = 30; + + g_variant_get(parameters, "(@a{sv})", &options); + + g_variant_iter_init(&iter, options); + while (g_variant_iter_next(&iter, "{&sv}", &opts_key, &opts_val)) { + _nm_unused gs_unref_variant GVariant *opts_val_free = opts_val; + + if (nm_streq(opts_key, "timeout")) { + if (!g_variant_is_of_type(opts_val, G_VARIANT_TYPE_INT32)) { + g_dbus_method_invocation_return_error_literal( + invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "\"timeout\" must be an integer \"i\""); + return; + } + + timeout = g_variant_get_int32(opts_val); + if (timeout <= 0 || timeout > 600) { + g_dbus_method_invocation_return_error_literal( + invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "The timeout for a find operation needs to be in the range of 1-600s."); + return; + } + + continue; + } + + g_dbus_method_invocation_return_error(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "Unsupported options key \"%s\"", + opts_key); + return; + } + + if (!priv->mgmt_iface) { + g_dbus_method_invocation_return_error_literal( + invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "WPA Supplicant management interface is currently unavailable."); + return; + } + + nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, timeout); + + g_dbus_method_invocation_return_value(invocation, NULL); +} + +static void +impl_device_wifi_p2p_stop_find(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(obj); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (!priv->mgmt_iface) { + g_dbus_method_invocation_return_error_literal( + invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ACTIVE, + "WPA Supplicant management interface is currently unavailable."); + return; + } + + nm_supplicant_interface_p2p_stop_find(priv->mgmt_iface); + + g_dbus_method_invocation_return_value(invocation, NULL); +} + +/*****************************************************************************/ + +NMSupplicantInterface * +nm_device_wifi_p2p_get_mgmt_iface(NMDeviceWifiP2P *self) +{ + g_return_val_if_fail(NM_IS_DEVICE_WIFI_P2P(self), NULL); + + return NM_DEVICE_WIFI_P2P_GET_PRIVATE(self)->mgmt_iface; +} + +void +nm_device_wifi_p2p_set_mgmt_iface(NMDeviceWifiP2P *self, NMSupplicantInterface *iface) +{ + NMDeviceWifiP2PPrivate *priv; + + g_return_if_fail(NM_IS_DEVICE_WIFI_P2P(self)); + g_return_if_fail(!iface || NM_IS_SUPPLICANT_INTERFACE(iface)); + + priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + if (priv->mgmt_iface == iface) + goto done; + + supplicant_interfaces_release(self, FALSE); + + if (!iface) + goto done; + + _LOGD(LOGD_DEVICE | LOGD_WIFI, + "P2P: WPA supplicant management interface changed to %s.", + nm_ref_string_get_str(nm_supplicant_interface_get_object_path(iface))); + + priv->mgmt_iface = g_object_ref(iface); + + g_signal_connect(priv->mgmt_iface, + NM_SUPPLICANT_INTERFACE_STATE, + G_CALLBACK(supplicant_iface_state_cb), + self); + g_signal_connect(priv->mgmt_iface, + NM_SUPPLICANT_INTERFACE_PEER_CHANGED, + G_CALLBACK(supplicant_iface_peer_changed_cb), + self); + g_signal_connect(priv->mgmt_iface, + NM_SUPPLICANT_INTERFACE_GROUP_STARTED, + G_CALLBACK(supplicant_iface_group_started_cb), + self); +done: + nm_device_queue_recheck_available(NM_DEVICE(self), + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + _set_is_waiting_for_supplicant(self, + !priv->mgmt_iface + || !nm_supplicant_interface_state_is_operational( + nm_supplicant_interface_get_state(priv->mgmt_iface))); +} + +void +nm_device_wifi_p2p_remove(NMDeviceWifiP2P *self) +{ + g_signal_emit_by_name(self, NM_DEVICE_REMOVED); +} + +/*****************************************************************************/ + +static const char * +get_type_description(NMDevice *device) +{ + return "wifi-p2p"; +} + +/*****************************************************************************/ + +static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_added = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT( + "PeerAdded", + .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), ); + +static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_removed = + NM_DEFINE_GDBUS_SIGNAL_INFO_INIT( + "PeerRemoved", + .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), ); + +static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_WIFI_P2P, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "StartFind", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("options", "a{sv}"), ), ), + .handle = impl_device_wifi_p2p_start_find, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("StopFind", ), + .handle = impl_device_wifi_p2p_stop_find, ), ), + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_wifi_p2p_peer_added, + &nm_signal_info_wifi_p2p_peer_removed, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Peers", + "ao", + NM_DEVICE_WIFI_P2P_PEERS), ), ), + .legacy_property_changed = FALSE, +}; + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(object); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + const char ** list; + + switch (prop_id) { + case PROP_PEERS: + list = nm_wifi_p2p_peers_get_paths(&priv->peers_lst_head); + g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_wifi_p2p_init(NMDeviceWifiP2P *self) +{ + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self); + + c_list_init(&priv->peers_lst_head); + + priv->sup_mgr = g_object_ref(nm_supplicant_manager_get()); +} + +static void +constructed(GObject *object) +{ + NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(object); + + G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->constructed(object); + + _set_is_waiting_for_supplicant(self, TRUE); +} + +NMDeviceWifiP2P * +nm_device_wifi_p2p_new(const char *iface) +{ + return g_object_new(NM_TYPE_DEVICE_WIFI_P2P, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "802.11 Wi-Fi P2P", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_WIFI_P2P, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_WIFI, + NM_DEVICE_RFKILL_TYPE, + RFKILL_TYPE_WLAN, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceWifiP2P * self = NM_DEVICE_WIFI_P2P(object); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(object); + + g_clear_object(&priv->sup_mgr); + + supplicant_interfaces_release(self, FALSE); + + G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMDeviceWifiP2P * peer = NM_DEVICE_WIFI_P2P(object); + NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(peer); + + nm_assert(c_list_is_empty(&priv->peers_lst_head)); + + G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->finalize(object); +} + +static void +nm_device_wifi_p2p_class_init(NMDeviceWifiP2PClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wifi_p2p); + + device_class->connection_type_supported = NM_SETTING_WIFI_P2P_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WIFI_P2P_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI_P2P); + device_class->get_type_description = get_type_description; + + /* Do we need compatibility checking or is the default good enough? */ + device_class->is_available = is_available; + device_class->check_connection_compatible = check_connection_compatible; + device_class->complete_connection = complete_connection; + + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->get_configured_mtu = get_configured_mtu; + device_class->get_auto_ip_config_method = get_auto_ip_config_method; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + + device_class->deactivate = deactivate; + device_class->unmanaged_on_quit = unmanaged_on_quit; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_PEERS] = g_param_spec_boxed(NM_DEVICE_WIFI_P2P_PEERS, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.h b/src/core/devices/wifi/nm-device-wifi-p2p.h new file mode 100644 index 00000000..d1aadd8e --- /dev/null +++ b/src/core/devices/wifi/nm-device-wifi-p2p.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#ifndef __NM_DEVICE_WIFI_P2P_H__ +#define __NM_DEVICE_WIFI_P2P_H__ + +#include "devices/nm-device.h" +#include "supplicant/nm-supplicant-interface.h" + +#define NM_TYPE_DEVICE_WIFI_P2P (nm_device_wifi_p2p_get_type()) +#define NM_DEVICE_WIFI_P2P(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P)) +#define NM_DEVICE_WIFI_P2P_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass)) +#define NM_IS_DEVICE_WIFI_P2P(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI_P2P)) +#define NM_IS_DEVICE_WIFI_P2P_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIFI_P2P)) +#define NM_DEVICE_WIFI_P2P_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass)) + +#define NM_DEVICE_WIFI_P2P_PEERS "peers" +#define NM_DEVICE_WIFI_P2P_GROUPS "groups" + +typedef struct _NMDeviceWifiP2P NMDeviceWifiP2P; +typedef struct _NMDeviceWifiP2PClass NMDeviceWifiP2PClass; + +GType nm_device_wifi_p2p_get_type(void); + +NMDeviceWifiP2P *nm_device_wifi_p2p_new(const char *iface); + +NMSupplicantInterface *nm_device_wifi_p2p_get_mgmt_iface(NMDeviceWifiP2P *self); +void nm_device_wifi_p2p_set_mgmt_iface(NMDeviceWifiP2P *self, NMSupplicantInterface *iface); + +void nm_device_wifi_p2p_remove(NMDeviceWifiP2P *self); + +#endif /* __NM_DEVICE_WIFI_P2P_H__ */ diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c new file mode 100644 index 00000000..042d4887 --- /dev/null +++ b/src/core/devices/wifi/nm-device-wifi.c @@ -0,0 +1,3879 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2017 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-wifi.h" + +#include <netinet/in.h> +#include <unistd.h> +#include <linux/if_ether.h> + +#include "nm-glib-aux/nm-ref-string.h" +#include "nm-glib-aux/nm-c-list.h" +#include "nm-device-wifi-p2p.h" +#include "nm-wifi-ap.h" +#include "nm-libnm-core-intern/nm-common-macros.h" +#include "devices/nm-device.h" +#include "devices/nm-device-private.h" +#include "nm-dbus-manager.h" +#include "nm-utils.h" +#include "NetworkManagerUtils.h" +#include "nm-act-request.h" +#include "supplicant/nm-supplicant-manager.h" +#include "supplicant/nm-supplicant-interface.h" +#include "supplicant/nm-supplicant-config.h" +#include "nm-setting-connection.h" +#include "nm-setting-wireless.h" +#include "nm-setting-wireless-security.h" +#include "nm-setting-8021x.h" +#include "nm-setting-ip4-config.h" +#include "nm-ip4-config.h" +#include "nm-setting-ip6-config.h" +#include "platform/nm-platform.h" +#include "nm-auth-utils.h" +#include "settings/nm-settings-connection.h" +#include "settings/nm-settings.h" +#include "nm-wifi-utils.h" +#include "nm-wifi-common.h" +#include "nm-core-internal.h" +#include "nm-config.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceWifi +#include "devices/nm-device-logging.h" + +#define SCAN_INTERVAL_SEC_MIN 3 +#define SCAN_INTERVAL_SEC_STEP 20 +#define SCAN_INTERVAL_SEC_MAX 120 + +#define SCAN_EXTRA_DELAY_MSEC 500 + +#define SCAN_RAND_MAC_ADDRESS_EXPIRE_SEC (5 * 60) + +#define SCAN_REQUEST_SSIDS_MAX_NUM 32u +#define SCAN_REQUEST_SSIDS_MAX_AGE_MSEC (3 * 60 * NM_UTILS_MSEC_PER_SEC) + +#define _LOGT_scan(...) _LOGT(LOGD_WIFI_SCAN, "wifi-scan: " __VA_ARGS__) + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWifi, + PROP_MODE, + PROP_BITRATE, + PROP_ACCESS_POINTS, + PROP_ACTIVE_ACCESS_POINT, + PROP_CAPABILITIES, + PROP_SCANNING, + PROP_LAST_SCAN, ); + +enum { + P2P_DEVICE_CREATED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct { + CList aps_lst_head; + GHashTable *aps_idx_by_supplicant_path; + + CList scanning_prohibited_lst_head; + + GCancellable *scan_request_cancellable; + + GSource *scan_request_delay_source; + + NMWifiAP *current_ap; + + GHashTable *scan_request_ssids_hash; + CList scan_request_ssids_lst_head; + + NMActRequestGetSecretsCallId *wifi_secrets_id; + + NMSupplicantManager * sup_mgr; + NMSupplMgrCreateIfaceHandle *sup_create_handle; + NMSupplicantInterface * sup_iface; + + gint64 scan_last_complete_msec; + gint64 scan_periodic_next_msec; + + gint64 scan_last_request_started_at_msec; + + guint scan_kickoff_timeout_id; + + guint ap_dump_id; + + guint periodic_update_id; + + guint link_timeout_id; + guint reacquire_iface_id; + guint wps_timeout_id; + guint sup_timeout_id; /* supplicant association timeout */ + + NMDeviceWifiCapabilities capabilities; + NMSettingWirelessWakeOnWLan wowlan_restore; + + NMDeviceWifiP2P *p2p_device; + NM80211Mode mode; + + guint32 failed_iface_count; + gint32 hw_addr_scan_expire; + + guint32 rate; + + guint8 scan_periodic_interval_sec; + + bool enabled : 1; /* rfkilled or not */ + bool scan_is_scanning : 1; + bool scan_periodic_allowed : 1; + bool scan_explicit_allowed : 1; + bool scan_explicit_requested : 1; + bool ssid_found : 1; + bool hidden_probe_scan_warn : 1; + +} NMDeviceWifiPrivate; + +struct _NMDeviceWifi { + NMDevice parent; + NMDeviceWifiPrivate _priv; +}; + +struct _NMDeviceWifiClass { + NMDeviceClass parent; +}; + +/*****************************************************************************/ + +G_DEFINE_TYPE(NMDeviceWifi, nm_device_wifi, NM_TYPE_DEVICE) + +#define NM_DEVICE_WIFI_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceWifi, NM_IS_DEVICE_WIFI, NMDevice) + +/*****************************************************************************/ + +static void supplicant_iface_state_down(NMDeviceWifi *self); + +static void cleanup_association_attempt(NMDeviceWifi *self, gboolean disconnect); + +static void supplicant_iface_state(NMDeviceWifi * self, + NMSupplicantInterfaceState new_state, + NMSupplicantInterfaceState old_state, + int disconnect_reason, + gboolean is_real_signal); + +static void supplicant_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data); + +static void supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface, + NMSupplicantBssInfo * bss_info, + gboolean is_present, + NMDeviceWifi * self); + +static void supplicant_iface_wps_credentials_cb(NMSupplicantInterface *iface, + GVariant * credentials, + NMDeviceWifi * self); + +static void supplicant_iface_notify_current_bss(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceWifi * self); + +static void supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceWifi * self); + +static void periodic_update(NMDeviceWifi *self); + +static void ap_add_remove(NMDeviceWifi *self, + gboolean is_adding, + NMWifiAP * ap, + gboolean recheck_available_connections); + +static void _hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset); + +static void recheck_p2p_availability(NMDeviceWifi *self); + +static void _scan_kickoff(NMDeviceWifi *self); + +static gboolean _scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff); + +/*****************************************************************************/ + +typedef struct { + GBytes *ssid; + CList lst; + gint64 timestamp_msec; +} ScanRequestSsidData; + +static void +_scan_request_ssids_remove(ScanRequestSsidData *srs_data) +{ + c_list_unlink_stale(&srs_data->lst); + g_bytes_unref(srs_data->ssid); + nm_g_slice_free(srs_data); +} + +static void +_scan_request_ssids_remove_with_hash(NMDeviceWifiPrivate *priv, ScanRequestSsidData *srs_data) +{ + nm_assert(srs_data); + nm_assert(nm_g_hash_table_lookup(priv->scan_request_ssids_hash, srs_data) == srs_data); + if (!g_hash_table_remove(priv->scan_request_ssids_hash, srs_data)) + nm_assert_not_reached(); + _scan_request_ssids_remove(srs_data); +} + +static void +_scan_request_ssids_remove_all(NMDeviceWifiPrivate *priv, + gint64 cutoff_with_now_msec, + guint cutoff_at_len) +{ + ScanRequestSsidData *srs_data; + + nm_assert((!priv->scan_request_ssids_hash) + == c_list_is_empty(&priv->scan_request_ssids_lst_head)); + if (!priv->scan_request_ssids_hash) + return; + + if (cutoff_at_len == 0) { + nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy); + while ( + (srs_data = + c_list_first_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst))) + _scan_request_ssids_remove(srs_data); + return; + } + + if (cutoff_with_now_msec != 0) { + gint64 cutoff_time_msec; + + /* remove all entries that are older than a max-age. */ + nm_assert(cutoff_with_now_msec > 0); + cutoff_time_msec = cutoff_with_now_msec - SCAN_REQUEST_SSIDS_MAX_AGE_MSEC; + while ( + (srs_data = + c_list_last_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst))) { + if (srs_data->timestamp_msec > cutoff_time_msec) + break; + _scan_request_ssids_remove_with_hash(priv, srs_data); + } + } + + if (cutoff_at_len != G_MAXUINT) { + guint i; + + /* trim the list to cutoff_at_len elements. */ + i = nm_g_hash_table_size(priv->scan_request_ssids_hash); + for (; i > cutoff_at_len; i--) { + ScanRequestSsidData *d; + + d = c_list_last_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst); + _scan_request_ssids_remove_with_hash(priv, d); + } + } + + nm_assert(nm_g_hash_table_size(priv->scan_request_ssids_hash) <= SCAN_REQUEST_SSIDS_MAX_NUM); + nm_assert(nm_g_hash_table_size(priv->scan_request_ssids_hash) + == c_list_length(&priv->scan_request_ssids_lst_head)); + if (c_list_is_empty(&priv->scan_request_ssids_lst_head)) + nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy); +} + +static GPtrArray * +_scan_request_ssids_fetch(NMDeviceWifiPrivate *priv, gint64 now_msec) +{ + ScanRequestSsidData *srs_data; + GPtrArray * ssids; + guint len; + + _scan_request_ssids_remove_all(priv, now_msec, G_MAXUINT); + + len = nm_g_hash_table_size(priv->scan_request_ssids_hash); + if (len == 0) + return NULL; + + ssids = g_ptr_array_new_full(len, (GDestroyNotify) g_bytes_unref); + nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy); + while ((srs_data = + c_list_first_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst))) { + g_ptr_array_add(ssids, g_steal_pointer(&srs_data->ssid)); + _scan_request_ssids_remove(srs_data); + } + return ssids; +} + +static void +_scan_request_ssids_track(NMDeviceWifiPrivate *priv, const GPtrArray *ssids) +{ + CList old_lst_head; + gint64 now_msec; + guint i; + + if (!ssids || ssids->len == 0) + return; + + now_msec = nm_utils_get_monotonic_timestamp_msec(); + + if (!priv->scan_request_ssids_hash) + priv->scan_request_ssids_hash = g_hash_table_new(nm_pgbytes_hash, nm_pgbytes_equal); + + /* Do a little dance. New elements shall keep their order as in @ssids, but all + * new elements should be sorted in the list preexisting elements of the list. + * First move the old elements away, and splice them back afterwards. */ + c_list_init(&old_lst_head); + c_list_splice(&old_lst_head, &priv->scan_request_ssids_lst_head); + + for (i = 0; i < ssids->len; i++) { + GBytes * ssid = ssids->pdata[i]; + ScanRequestSsidData *d; + + G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(ScanRequestSsidData, ssid) == 0); + d = g_hash_table_lookup(priv->scan_request_ssids_hash, &ssid); + if (!d) { + d = g_slice_new(ScanRequestSsidData); + *d = (ScanRequestSsidData){ + .lst = C_LIST_INIT(d->lst), + .timestamp_msec = now_msec, + .ssid = g_bytes_ref(ssid), + }; + g_hash_table_add(priv->scan_request_ssids_hash, d); + } else + d->timestamp_msec = now_msec; + c_list_link_tail(&priv->scan_request_ssids_lst_head, &d->lst); + } + + c_list_splice(&priv->scan_request_ssids_lst_head, &old_lst_head); + + /* Trim the excess. After our splice with old_lst_head, the list contains the new + * elements (from @ssids) at the front (in there original order), followed by older elements. */ + _scan_request_ssids_remove_all(priv, now_msec, SCAN_REQUEST_SSIDS_MAX_NUM); +} + +/*****************************************************************************/ + +void +nm_device_wifi_scanning_prohibited_track(NMDeviceWifi *self, + gpointer tag, + gboolean temporarily_prohibited) +{ + NMDeviceWifiPrivate *priv; + NMCListElem * elem; + + g_return_if_fail(NM_IS_DEVICE_WIFI(self)); + nm_assert(tag); + + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + /* We track these with a simple CList. This would be not efficient, if + * there would be many users that need to be tracked at the same time (there + * aren't). In fact, most of the time there is no NMDeviceOlpcMesh and + * nobody tracks itself here. Optimize for that and simplicity. */ + + elem = nm_c_list_elem_find_first(&priv->scanning_prohibited_lst_head, iter, iter == tag); + + if (!temporarily_prohibited) { + if (!elem) + return; + nm_c_list_elem_free(elem); + } else { + if (elem) + return; + c_list_link_tail(&priv->scanning_prohibited_lst_head, &nm_c_list_elem_new_stale(tag)->lst); + } + + _scan_notify_allowed(self, NM_TERNARY_DEFAULT); +} + +/*****************************************************************************/ + +static void +_ap_dump(NMDeviceWifi * self, + NMLogLevel log_level, + const NMWifiAP *ap, + const char * prefix, + gint64 now_msec) +{ + char buf[1024]; + + buf[0] = '\0'; + _NMLOG(log_level, + LOGD_WIFI_SCAN, + "wifi-ap: %-7s %s", + prefix, + nm_wifi_ap_to_string(ap, buf, sizeof(buf), now_msec)); +} + +gboolean +nm_device_wifi_get_scanning(NMDeviceWifi *self) +{ + g_return_val_if_fail(NM_IS_DEVICE_WIFI(self), FALSE); + + return NM_DEVICE_WIFI_GET_PRIVATE(self)->scan_is_scanning; +} + +static gboolean +_scan_is_scanning_eval(NMDeviceWifiPrivate *priv) +{ + return priv->scan_request_cancellable || priv->scan_request_delay_source + || (priv->sup_iface && nm_supplicant_interface_get_scanning(priv->sup_iface)); +} + +static gboolean +_scan_notify_is_scanning(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gboolean last_scan_changed = FALSE; + NMDeviceState state; + gboolean scanning; + + scanning = _scan_is_scanning_eval(priv); + if (scanning == priv->scan_is_scanning) + return FALSE; + + priv->scan_is_scanning = scanning; + + if (!scanning || priv->scan_last_complete_msec == 0) { + last_scan_changed = TRUE; + priv->scan_last_complete_msec = nm_utils_get_monotonic_timestamp_msec(); + } + + _LOGD(LOGD_WIFI, + "wifi-scan: scanning-state: %s%s", + scanning ? "scanning" : "idle", + last_scan_changed ? " (notify last-scan)" : ""); + + state = nm_device_get_state(NM_DEVICE(self)); + + if (scanning) { + /* while the device is activating/activated, we don't need the pending + * action. The pending action exists to delay startup complete, while + * activating that is already achieved via other means. */ + if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED) + nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WIFI_SCAN, FALSE); + } + + nm_gobject_notify_together(self, PROP_SCANNING, last_scan_changed ? PROP_LAST_SCAN : PROP_0); + + _scan_kickoff(self); + + if (!_scan_is_scanning_eval(priv)) { + if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED) + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + nm_device_remove_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WIFI_SCAN, FALSE); + } + + return TRUE; +} + +static gboolean +_scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gboolean explicit_allowed; + gboolean periodic_allowed; + NMDeviceState state; + gboolean changed = FALSE; + + state = nm_device_get_state(NM_DEVICE(self)); + + explicit_allowed = FALSE; + periodic_allowed = FALSE; + + if (!c_list_is_empty(&priv->scanning_prohibited_lst_head)) { + /* something prohibits scanning. */ + } else if (NM_IN_SET(priv->mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_AP)) { + /* Don't scan when a an AP or Ad-Hoc connection is active as it will + * disrupt connected clients or peers. */ + } else if (NM_IN_SET(state, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_FAILED)) { + /* Can always scan when disconnected */ + explicit_allowed = TRUE; + periodic_allowed = TRUE; + } else if (NM_IN_SET(state, NM_DEVICE_STATE_ACTIVATED)) { + /* Prohibit periodic scans when connected; we ask the supplicant to + * background scan for us, unless the connection is locked to a specific + * BSSID (in which case scanning is effectively disabled). */ + periodic_allowed = FALSE; + + /* Prohibit scans if the supplicant is busy */ + if (priv->sup_iface) { + explicit_allowed = !NM_IN_SET(nm_supplicant_interface_get_state(priv->sup_iface), + NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING, + NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED, + NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE, + NM_SUPPLICANT_INTERFACE_STATE_GROUP_HANDSHAKE); + } else + explicit_allowed = FALSE; + } + + if (explicit_allowed != priv->scan_explicit_allowed + || periodic_allowed != priv->scan_periodic_allowed) { + priv->scan_periodic_allowed = periodic_allowed; + priv->scan_explicit_allowed = explicit_allowed; + _LOGT_scan("scan-periodic-allowed=%d, scan-explicit-allowed=%d", + periodic_allowed, + explicit_allowed); + changed = TRUE; + } + + if (do_kickoff == NM_TERNARY_TRUE || (do_kickoff == NM_TERNARY_DEFAULT && changed)) + _scan_kickoff(self); + + return changed; +} + +static void +supplicant_iface_notify_scanning_cb(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceWifi * self) +{ + _scan_notify_is_scanning(self); +} + +static gboolean +unmanaged_on_quit(NMDevice *self) +{ + /* Wi-Fi devices cannot be assumed and are always taken down. + * However, also when being disconnected, we scan and thus + * set the MAC address to a random value. + * + * We must restore the original MAC address when quitting, thus + * signal to unmanage the device. */ + return TRUE; +} + +static void +supplicant_interface_acquire_cb(NMSupplicantManager * supplicant_manager, + NMSupplMgrCreateIfaceHandle *handle, + NMSupplicantInterface * iface, + GError * error, + gpointer user_data) +{ + NMDeviceWifi * self = user_data; + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (nm_utils_error_is_cancelled(error)) + return; + + nm_assert(priv->sup_create_handle == handle); + + priv->sup_create_handle = NULL; + + if (error) { + _LOGE(LOGD_WIFI, "Couldn't initialize supplicant interface: %s", error->message); + supplicant_iface_state_down(self); + nm_device_remove_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, + TRUE); + return; + } + + priv->sup_iface = g_object_ref(iface); + + g_signal_connect(priv->sup_iface, + NM_SUPPLICANT_INTERFACE_STATE, + G_CALLBACK(supplicant_iface_state_cb), + self); + g_signal_connect(priv->sup_iface, + NM_SUPPLICANT_INTERFACE_BSS_CHANGED, + G_CALLBACK(supplicant_iface_bss_changed_cb), + self); + g_signal_connect(priv->sup_iface, + NM_SUPPLICANT_INTERFACE_WPS_CREDENTIALS, + G_CALLBACK(supplicant_iface_wps_credentials_cb), + self); + g_signal_connect(priv->sup_iface, + "notify::" NM_SUPPLICANT_INTERFACE_SCANNING, + G_CALLBACK(supplicant_iface_notify_scanning_cb), + self); + g_signal_connect(priv->sup_iface, + "notify::" NM_SUPPLICANT_INTERFACE_CURRENT_BSS, + G_CALLBACK(supplicant_iface_notify_current_bss), + self); + g_signal_connect(priv->sup_iface, + "notify::" NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE, + G_CALLBACK(supplicant_iface_notify_p2p_available), + self); + + _scan_notify_is_scanning(self); + + if (nm_supplicant_interface_get_state(priv->sup_iface) + != NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + /* fake an initial state change. */ + supplicant_iface_state(user_data, + NM_SUPPLICANT_INTERFACE_STATE_STARTING, + nm_supplicant_interface_get_state(priv->sup_iface), + 0, + FALSE); + } +} + +static void +supplicant_interface_acquire(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_assert(!priv->sup_iface); + nm_assert(!priv->sup_create_handle); + + priv->sup_create_handle = + nm_supplicant_manager_create_interface(priv->sup_mgr, + nm_device_get_ifindex(NM_DEVICE(self)), + NM_SUPPLICANT_DRIVER_WIRELESS, + supplicant_interface_acquire_cb, + self); + nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, TRUE); +} + +static void +supplicant_interface_release(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (nm_clear_pointer(&priv->sup_create_handle, nm_supplicant_manager_create_interface_cancel)) + nm_device_remove_pending_action(NM_DEVICE(self), + NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, + TRUE); + + nm_clear_g_source(&priv->scan_kickoff_timeout_id); + nm_clear_g_source_inst(&priv->scan_request_delay_source); + nm_clear_g_cancellable(&priv->scan_request_cancellable); + + _scan_request_ssids_remove_all(priv, 0, 0); + + priv->scan_periodic_interval_sec = 0; + priv->scan_periodic_next_msec = 0; + + nm_clear_g_source(&priv->ap_dump_id); + + if (priv->sup_iface) { + /* Clear supplicant interface signal handlers */ + g_signal_handlers_disconnect_by_data(priv->sup_iface, self); + + /* Tell the supplicant to disconnect from the current AP */ + nm_supplicant_interface_disconnect(priv->sup_iface); + + g_clear_object(&priv->sup_iface); + } + + if (priv->p2p_device) { + /* Signal to P2P device to also release its reference */ + nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, NULL); + } + + _scan_notify_is_scanning(self); +} + +static void +update_seen_bssids_cache(NMDeviceWifi *self, NMWifiAP *ap) +{ + g_return_if_fail(NM_IS_DEVICE_WIFI(self)); + + if (ap == NULL) + return; + + /* Don't cache the BSSID for Ad-Hoc APs */ + if (nm_wifi_ap_get_mode(ap) != NM_802_11_MODE_INFRA) + return; + + if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_ACTIVATED + && nm_device_has_unmodified_applied_connection(NM_DEVICE(self), + NM_SETTING_COMPARE_FLAG_NONE)) { + nm_settings_connection_add_seen_bssid(nm_device_get_settings_connection(NM_DEVICE(self)), + nm_wifi_ap_get_address(ap)); + } +} + +static void +set_current_ap(NMDeviceWifi *self, NMWifiAP *new_ap, gboolean recheck_available_connections) +{ + NMDeviceWifiPrivate *priv; + NMWifiAP * old_ap; + + g_return_if_fail(NM_IS_DEVICE_WIFI(self)); + + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + old_ap = priv->current_ap; + + if (old_ap == new_ap) + return; + + if (new_ap) { + priv->current_ap = g_object_ref(new_ap); + + /* Update seen BSSIDs cache */ + update_seen_bssids_cache(self, priv->current_ap); + } else + priv->current_ap = NULL; + + if (old_ap) { + NM80211Mode mode = nm_wifi_ap_get_mode(old_ap); + + /* Remove any AP from the internal list if it was created by NM or isn't known to the supplicant */ + if (NM_IN_SET(mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_AP) || nm_wifi_ap_get_fake(old_ap)) + ap_add_remove(self, FALSE, old_ap, recheck_available_connections); + g_object_unref(old_ap); + } + + _notify(self, PROP_ACTIVE_ACCESS_POINT); +} + +static void +periodic_update(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv; + int ifindex; + guint32 new_rate; + int percent; + + if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_ACTIVATED) { + /* BSSID and signal strength have meaningful values only if the device + * is activated and not scanning. + */ + return; + } + + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (!nm_supplicant_interface_state_is_associated( + nm_supplicant_interface_get_state(priv->sup_iface)) + || nm_supplicant_interface_get_scanning(priv->sup_iface)) { + /* Only update current AP if we're actually talking to something, otherwise + * assume the old one (if any) is still valid until we're told otherwise or + * the connection fails. + */ + return; + } + + if (priv->mode == NM_802_11_MODE_AP) { + /* In AP mode we currently have nothing to do. */ + return; + } + + ifindex = nm_device_get_ifindex(NM_DEVICE(self)); + if (ifindex <= 0) + g_return_if_reached(); + + if (priv->current_ap + && nm_platform_wifi_get_station(nm_device_get_platform(NM_DEVICE(self)), + ifindex, + NULL, + &percent, + &new_rate)) { + if (nm_wifi_ap_set_strength(priv->current_ap, (gint8) percent)) { +#if NM_MORE_LOGGING + _ap_dump(self, LOGL_TRACE, priv->current_ap, "updated", 0); +#endif + } + + if (new_rate != priv->rate) { + priv->rate = new_rate; + _notify(self, PROP_BITRATE); + } + } +} + +static gboolean +periodic_update_cb(gpointer user_data) +{ + periodic_update(user_data); + return TRUE; +} + +static void +ap_add_remove(NMDeviceWifi *self, + gboolean is_adding, /* or else removing */ + NMWifiAP * ap, + gboolean recheck_available_connections) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (is_adding) { + g_object_ref(ap); + ap->wifi_device = NM_DEVICE(self); + c_list_link_tail(&priv->aps_lst_head, &ap->aps_lst); + if (!g_hash_table_insert(priv->aps_idx_by_supplicant_path, + nm_wifi_ap_get_supplicant_path(ap), + ap)) + nm_assert_not_reached(); + nm_dbus_object_export(NM_DBUS_OBJECT(ap)); + _ap_dump(self, LOGL_DEBUG, ap, "added", 0); + nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, TRUE); + } else { + ap->wifi_device = NULL; + c_list_unlink(&ap->aps_lst); + if (!g_hash_table_remove(priv->aps_idx_by_supplicant_path, + nm_wifi_ap_get_supplicant_path(ap))) + nm_assert_not_reached(); + _ap_dump(self, LOGL_DEBUG, ap, "removed", 0); + } + + _notify(self, PROP_ACCESS_POINTS); + + if (!is_adding) { + nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, FALSE); + nm_dbus_object_clear_and_unexport(&ap); + } + + nm_device_emit_recheck_auto_activate(NM_DEVICE(self)); + if (recheck_available_connections) + nm_device_recheck_available_connections(NM_DEVICE(self)); +} + +static void +remove_all_aps(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMWifiAP * ap; + + if (c_list_is_empty(&priv->aps_lst_head)) + return; + + set_current_ap(self, NULL, FALSE); + + while ((ap = c_list_first_entry(&priv->aps_lst_head, NMWifiAP, aps_lst))) + ap_add_remove(self, FALSE, ap, FALSE); + + nm_device_recheck_available_connections(NM_DEVICE(self)); +} + +static gboolean +wake_on_wlan_restore(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate * priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSettingWirelessWakeOnWLan w; + + w = priv->wowlan_restore; + if (w == NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE) + return TRUE; + + priv->wowlan_restore = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE; + return nm_platform_wifi_set_wake_on_wlan(NM_PLATFORM_GET, + nm_device_get_ifindex(NM_DEVICE(self)), + w); +} + +static void +disconnect_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data) +{ + gs_unref_object NMDeviceWifi *self = NULL; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + + nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data); + + /* error will be freed by sup_iface */ + callback(NM_DEVICE(self), error, callback_user_data); +} + +static void +disconnect_cb_on_idle(gpointer user_data, GCancellable *cancellable) +{ + gs_unref_object NMDeviceWifi *self = NULL; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + gs_free_error GError *cancelled_error = NULL; + + nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data); + + g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error); + callback(NM_DEVICE(self), cancelled_error, callback_user_data); +} + +static void +deactivate_async(NMDevice * device, + GCancellable * cancellable, + NMDeviceDeactivateCallback callback, + gpointer callback_user_data) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gpointer user_data; + + nm_assert(G_IS_CANCELLABLE(cancellable)); + nm_assert(callback); + + user_data = nm_utils_user_data_pack(g_object_ref(self), callback, callback_user_data); + if (!priv->sup_iface) { + nm_utils_invoke_on_idle(cancellable, disconnect_cb_on_idle, user_data); + return; + } + + cleanup_association_attempt(self, FALSE); + + nm_supplicant_interface_disconnect_async(priv->sup_iface, + cancellable, + disconnect_cb, + user_data); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + int ifindex = nm_device_get_ifindex(device); + + nm_clear_g_source(&priv->periodic_update_id); + + cleanup_association_attempt(self, TRUE); + + priv->rate = 0; + + set_current_ap(self, NULL, TRUE); + + if (!wake_on_wlan_restore(self)) + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot unconfigure WoWLAN."); + + /* Clear any critical protocol notification in the Wi-Fi stack */ + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), ifindex, FALSE); + + /* Ensure we're in infrastructure mode after deactivation; some devices + * (usually older ones) don't scan well in adhoc mode. + */ + if (nm_platform_wifi_get_mode(nm_device_get_platform(device), ifindex) + != NM_802_11_MODE_INFRA) { + nm_device_take_down(NM_DEVICE(self), TRUE); + nm_platform_wifi_set_mode(nm_device_get_platform(device), ifindex, NM_802_11_MODE_INFRA); + nm_device_bring_up(NM_DEVICE(self), TRUE, NULL); + } + + if (priv->mode != NM_802_11_MODE_INFRA) { + priv->mode = NM_802_11_MODE_INFRA; + _notify(self, PROP_MODE); + } + + _scan_notify_allowed(self, NM_TERNARY_TRUE); +} + +static void +deactivate_reset_hw_addr(NMDevice *device) +{ + _hw_addr_set_scanning((NMDeviceWifi *) device, TRUE); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSettingWireless * s_wireless; + const char * mac; + const char *const * mac_blacklist; + int i; + const char * mode; + const char * perm_hw_addr; + + if (!NM_DEVICE_CLASS(nm_device_wifi_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + s_wireless = nm_connection_get_setting_wireless(connection); + + perm_hw_addr = nm_device_get_permanent_hw_address(device); + mac = nm_setting_wireless_get_mac_address(s_wireless); + if (perm_hw_addr) { + if (mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device MAC address does not match the profile"); + return FALSE; + } + + /* Check for MAC address blacklist */ + mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless); + for (i = 0; mac_blacklist[i]; i++) { + if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) { + g_warn_if_reached(); + return FALSE; + } + + if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "MAC address blacklisted"); + return FALSE; + } + } + } else if (mac) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has no valid MAC address as required by profile"); + return FALSE; + } + + /* Early exit if supplicant or device doesn't support requested mode */ + mode = nm_setting_wireless_get_mode(s_wireless); + if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0) { + if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_ADHOC)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "the device does not support Ad-Hoc networks"); + return FALSE; + } + } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0) { + if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_AP)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "the device does not support Access Point mode"); + return FALSE; + } + + if (priv->sup_iface) { + if (nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_AP) + == NM_TERNARY_FALSE) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "wpa_supplicant does not support Access Point mode"); + return FALSE; + } + } + } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0) { + if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_MESH)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "the device does not support Mesh mode"); + return FALSE; + } + + if (priv->sup_iface) { + if (nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_MESH) + == NM_TERNARY_FALSE) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "wpa_supplicant does not support Mesh mode"); + return FALSE; + } + } + } + + // FIXME: check channel/freq/band against bands the hardware supports + // FIXME: check encryption against device capabilities + // FIXME: check bitrate against device capabilities + + return TRUE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSettingWireless * s_wifi; + const char * mode; + + s_wifi = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wifi, FALSE); + + /* a connection that is available for a certain @specific_object, MUST + * also be available in general (without @specific_object). */ + + if (specific_object) { + NMWifiAP *ap; + + ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object); + if (!ap) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "requested access point not found"); + return FALSE; + } + if (!nm_wifi_ap_check_compatible(ap, connection)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "requested access point is not compatible with profile"); + return FALSE; + } + return TRUE; + } + + /* Ad-Hoc, AP and Mesh connections are always available because they may be + * started at any time. + */ + mode = nm_setting_wireless_get_mode(s_wifi); + if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0 + || g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0 + || g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0) + return TRUE; + + /* Hidden SSIDs obviously don't always appear in the scan list either. + * + * For an explicit user-activation-request, a connection is considered + * available because for hidden Wi-Fi, clients didn't consistently + * set the 'hidden' property to indicate hidden SSID networks. If + * activating but the network isn't available let the device recheck + * availability. + */ + if (nm_setting_wireless_get_hidden(s_wifi) + || NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP)) + return TRUE; + + if (!nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "no compatible access point found"); + return FALSE; + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSettingWireless * s_wifi; + gs_free char * ssid_utf8 = NULL; + NMWifiAP * ap; + GBytes * ssid = NULL; + GBytes * setting_ssid = NULL; + gboolean hidden = FALSE; + const char * mode; + + s_wifi = nm_connection_get_setting_wireless(connection); + + mode = s_wifi ? nm_setting_wireless_get_mode(s_wifi) : NULL; + + if (!specific_object) { + /* If not given a specific object, we need at minimum an SSID */ + if (!s_wifi) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'wireless' setting is required if no AP path was given."); + return FALSE; + } + + setting_ssid = nm_setting_wireless_get_ssid(s_wifi); + if (!setting_ssid || g_bytes_get_size(setting_ssid) == 0) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "A 'wireless' setting with a valid SSID is required if no AP path was given."); + return FALSE; + } + + if (!nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) { + /* Find a compatible AP in the scan list */ + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + + /* If we still don't have an AP, then the WiFI settings needs to be + * fully specified by the client. Might not be able to find an AP + * if the network isn't broadcasting the SSID for example. + */ + if (!ap) { + if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error)) + return FALSE; + + hidden = TRUE; + } + } else { + if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error)) + return FALSE; + ap = NULL; + } + } else if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) { + if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error)) + return FALSE; + ap = NULL; + } else { + ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object); + if (!ap) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND, + "The access point %s was not in the scan list.", + specific_object); + return FALSE; + } + } + + /* Add a wifi setting if one doesn't exist yet */ + if (!s_wifi) { + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wifi)); + } + + if (ap) + ssid = nm_wifi_ap_get_ssid(ap); + + if (ssid == NULL) { + /* The AP must be hidden. Connecting to a Wi-Fi AP requires the SSID + * as part of the initial handshake, so check the connection details + * for the SSID. The AP object will still be used for encryption + * settings and such. + */ + ssid = nm_setting_wireless_get_ssid(s_wifi); + } + + if (ssid == NULL) { + /* If there's no SSID on the AP itself, and no SSID in the + * connection data, then we cannot connect at all. Return an error. + */ + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + ap ? "A 'wireless' setting with a valid SSID is required for hidden access points." + : "Cannot create 'wireless' setting due to missing SSID."); + return FALSE; + } + + if (ap) { + /* If the SSID is a well-known SSID, lock the connection to the AP's + * specific BSSID so NM doesn't autoconnect to some random wifi net. + */ + if (!nm_wifi_ap_complete_connection(ap, + connection, + nm_wifi_utils_is_manf_default_ssid(ssid), + error)) + return FALSE; + } + + ssid_utf8 = _nm_utils_ssid_to_utf8(ssid); + nm_utils_complete_generic( + nm_device_get_platform(device), + connection, + NM_SETTING_WIRELESS_SETTING_NAME, + existing_connections, + ssid_utf8, + ssid_utf8, + NULL, + nm_setting_wireless_get_mac_address(s_wifi) ? NULL : nm_device_get_iface(device), + TRUE); + + if (hidden) + g_object_set(s_wifi, NM_SETTING_WIRELESS_HIDDEN, TRUE, NULL); + + return TRUE; +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate * priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSupplicantInterfaceState supplicant_state; + + if (!priv->enabled) + return FALSE; + + if (!priv->sup_iface) + return FALSE; + + supplicant_state = nm_supplicant_interface_get_state(priv->sup_iface); + if (supplicant_state <= NM_SUPPLICANT_INTERFACE_STATE_STARTING + || supplicant_state > NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) + return FALSE; + + return TRUE; +} + +static gboolean +get_autoconnect_allowed(NMDevice *device) +{ + return !NM_DEVICE_WIFI_GET_PRIVATE(NM_DEVICE_WIFI(device))->scan_is_scanning; +} + +static gboolean +can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMConnection * connection; + NMSettingWireless * s_wifi; + NMWifiAP * ap; + const char * method6, *mode; + gboolean auto4, auto6; + + nm_assert(!specific_object || !*specific_object); + + if (!NM_DEVICE_CLASS(nm_device_wifi_parent_class)->can_auto_connect(device, sett_conn, NULL)) + return FALSE; + + connection = nm_settings_connection_get_connection(sett_conn); + + s_wifi = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wifi, FALSE); + + /* Always allow autoconnect for AP and non-autoconf Ad-Hoc or Mesh */ + auto4 = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET), + NM_SETTING_IP4_CONFIG_METHOD_AUTO); + method6 = nm_utils_get_ip_config_method(connection, AF_INET6); + auto6 = nm_streq0(method6, NM_SETTING_IP6_CONFIG_METHOD_AUTO) + || nm_streq0(method6, NM_SETTING_IP6_CONFIG_METHOD_DHCP); + + mode = nm_setting_wireless_get_mode(s_wifi); + + if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) + return TRUE; + else if (!auto4 && nm_streq0(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) + return TRUE; + else if (!auto4 && !auto6 && nm_streq0(mode, NM_SETTING_WIRELESS_MODE_MESH)) + return TRUE; + + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + if (ap) { + /* All good; connection is usable */ + NM_SET_OUT(specific_object, g_strdup(nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)))); + return TRUE; + } + + return FALSE; +} + +const CList * +_nm_device_wifi_get_aps(NMDeviceWifi *self) +{ + return &NM_DEVICE_WIFI_GET_PRIVATE(self)->aps_lst_head; +} + +static void +_hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset) +{ + NMDevice * device = (NMDevice *) self; + NMDeviceWifiPrivate *priv; + guint32 now; + gboolean randomize; + + g_return_if_fail(NM_IS_DEVICE_WIFI(self)); + + if (nm_device_is_activating(device) || nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) + return; + + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + randomize = nm_config_data_get_device_config_boolean( + NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_RAND_MAC_ADDRESS, + device, + TRUE, + TRUE); + + if (!randomize) { + /* expire the temporary MAC address used during scanning */ + priv->hw_addr_scan_expire = 0; + + if (do_reset) { + priv->scan_last_request_started_at_msec = G_MININT64; + priv->scan_periodic_next_msec = 0; + priv->scan_periodic_interval_sec = 0; + nm_device_hw_addr_reset(device, "scanning"); + } + return; + } + + now = nm_utils_get_monotonic_timestamp_sec(); + + if (now >= priv->hw_addr_scan_expire) { + gs_free char *generate_mac_address_mask = NULL; + gs_free char *hw_addr_scan = NULL; + + /* the random MAC address for scanning expires after a while. + * + * We don't bother with to update the MAC address exactly when + * it expires, instead on the next scan request, we will generate + * a new one.*/ + priv->hw_addr_scan_expire = now + SCAN_RAND_MAC_ADDRESS_EXPIRE_SEC; + + generate_mac_address_mask = nm_config_data_get_device_config( + NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_GENERATE_MAC_ADDRESS_MASK, + device, + NULL); + + priv->scan_last_request_started_at_msec = G_MININT64; + priv->scan_periodic_next_msec = 0; + priv->scan_periodic_interval_sec = 0; + hw_addr_scan = nm_utils_hw_addr_gen_random_eth(nm_device_get_initial_hw_address(device), + generate_mac_address_mask); + nm_device_hw_addr_set(device, hw_addr_scan, "scanning", TRUE); + } +} + +static GPtrArray * +ssids_options_to_ptrarray(GVariant *value, GError **error) +{ + gs_unref_ptrarray GPtrArray *ssids = NULL; + gsize num_ssids; + gsize i; + + nm_assert(g_variant_is_of_type(value, G_VARIANT_TYPE("aay"))); + + num_ssids = g_variant_n_children(value); + if (num_ssids > 32) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "too many SSIDs requested to scan"); + return NULL; + } + + if (num_ssids) { + ssids = g_ptr_array_new_full(num_ssids, (GDestroyNotify) g_bytes_unref); + for (i = 0; i < num_ssids; i++) { + gs_unref_variant GVariant *v = NULL; + gsize len; + const guint8 * bytes; + + v = g_variant_get_child_value(value, i); + bytes = g_variant_get_fixed_array(v, &len, sizeof(guint8)); + if (len > 32) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "SSID at index %d more than 32 bytes", + (int) i); + return NULL; + } + + g_ptr_array_add(ssids, g_bytes_new(bytes, len)); + } + } + + return g_steal_pointer(&ssids); +} + +GPtrArray * +nmtst_ssids_options_to_ptrarray(GVariant *value, GError **error) +{ + return ssids_options_to_ptrarray(value, error); +} + +static void +dbus_request_scan_cb(NMDevice * device, + GDBusMethodInvocation *context, + NMAuthSubject * subject, + GError * error, + gpointer user_data) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *ssids = user_data; + + if (error) { + g_dbus_method_invocation_return_gerror(context, error); + return; + } + + _scan_request_ssids_track(priv, ssids); + priv->scan_explicit_requested = TRUE; + _scan_kickoff(self); + g_dbus_method_invocation_return_value(context, NULL); +} + +void +_nm_device_wifi_request_scan(NMDeviceWifi * self, + GVariant * options, + GDBusMethodInvocation *invocation) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + gs_unref_ptrarray GPtrArray *ssids = NULL; + + if (options) { + gs_unref_variant GVariant *val = g_variant_lookup_value(options, "ssids", NULL); + + if (val) { + gs_free_error GError *ssid_error = NULL; + + if (!g_variant_is_of_type(val, G_VARIANT_TYPE("aay"))) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "Invalid 'ssid' scan option"); + return; + } + + ssids = ssids_options_to_ptrarray(val, &ssid_error); + if (ssid_error) { + g_dbus_method_invocation_return_gerror(invocation, ssid_error); + return; + } + } + } + + if (!priv->enabled || !priv->sup_iface + || nm_device_get_state(device) < NM_DEVICE_STATE_DISCONNECTED) { + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_NOT_ALLOWED, + "Scanning not allowed while unavailable"); + return; + } + + nm_device_auth_request(device, + invocation, + NULL, + NM_AUTH_PERMISSION_WIFI_SCAN, + TRUE, + NULL, + dbus_request_scan_cb, + g_steal_pointer(&ssids)); +} + +static gboolean +hidden_filter_func(NMSettings *settings, NMSettingsConnection *set_con, gpointer user_data) +{ + NMConnection * connection = nm_settings_connection_get_connection(set_con); + NMSettingWireless *s_wifi; + + if (!nm_connection_is_type(connection, NM_SETTING_WIRELESS_SETTING_NAME)) + return FALSE; + s_wifi = nm_connection_get_setting_wireless(connection); + if (!s_wifi) + return FALSE; + if (nm_streq0(nm_setting_wireless_get_mode(s_wifi), NM_SETTING_WIRELESS_MODE_AP)) + return FALSE; + return nm_setting_wireless_get_hidden(s_wifi); +} + +static GPtrArray * +_scan_request_ssids_build_hidden(NMDeviceWifi *self, + gint64 now_msec, + gboolean * out_has_hidden_profiles) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + guint max_scan_ssids = nm_supplicant_interface_get_max_scan_ssids(priv->sup_iface); + gs_free NMSettingsConnection **connections = NULL; + gs_unref_ptrarray GPtrArray *ssids = NULL; + gs_unref_hashtable GHashTable *unique_ssids = NULL; + guint connections_len; + guint n_hidden; + guint i; + + NM_SET_OUT(out_has_hidden_profiles, FALSE); + + /* collect all pending explicit SSIDs. */ + ssids = _scan_request_ssids_fetch(priv, now_msec); + + if (max_scan_ssids == 0) { + /* no space. @ssids will be ignored. */ + return NULL; + } + + if (ssids) { + if (ssids->len < max_scan_ssids) { + /* Add wildcard SSID using a static wildcard SSID used for every scan */ + g_ptr_array_insert(ssids, 0, g_bytes_ref(nm_gbytes_get_empty())); + } + if (ssids->len >= max_scan_ssids) { + /* there is no more space. Use what we have. */ + g_ptr_array_set_size(ssids, max_scan_ssids); + return g_steal_pointer(&ssids); + } + } + + connections = nm_settings_get_connections_clone(nm_device_get_settings((NMDevice *) self), + &connections_len, + hidden_filter_func, + NULL, + NULL, + NULL); + if (!connections[0]) + return g_steal_pointer(&ssids); + + if (!ssids) { + ssids = g_ptr_array_new_full(max_scan_ssids, (GDestroyNotify) g_bytes_unref); + /* Add wildcard SSID using a static wildcard SSID used for every scan */ + g_ptr_array_insert(ssids, 0, g_bytes_ref(nm_gbytes_get_empty())); + } + + unique_ssids = g_hash_table_new(nm_gbytes_hash, nm_gbytes_equal); + for (i = 1; i < ssids->len; i++) { + if (!g_hash_table_add(unique_ssids, ssids->pdata[i])) + nm_assert_not_reached(); + } + + g_qsort_with_data(connections, + connections_len, + sizeof(NMSettingsConnection *), + nm_settings_connection_cmp_timestamp_p_with_data, + NULL); + + n_hidden = 0; + for (i = 0; i < connections_len; i++) { + NMSettingWireless *s_wifi; + GBytes * ssid; + + if (ssids->len >= max_scan_ssids) + break; + + if (n_hidden > 4) { + /* we allow at most 4 hidden profiles to be actively scanned. The + * reason is speed and to not disclose too many SSIDs. */ + break; + } + + s_wifi = nm_connection_get_setting_wireless( + nm_settings_connection_get_connection(connections[i])); + ssid = nm_setting_wireless_get_ssid(s_wifi); + + if (!g_hash_table_add(unique_ssids, ssid)) + continue; + + g_ptr_array_add(ssids, g_bytes_ref(ssid)); + n_hidden++; + } + + NM_SET_OUT(out_has_hidden_profiles, n_hidden > 0); + return g_steal_pointer(&ssids); +} + +static gboolean +_scan_request_delay_cb(gpointer user_data) +{ + NMDeviceWifi * self = user_data; + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_clear_g_source_inst(&priv->scan_request_delay_source); + + _LOGT_scan("scan request completed (after extra delay)"); + + _scan_notify_is_scanning(self); + return G_SOURCE_REMOVE; +} + +static void +_scan_supplicant_request_scan_cb(NMSupplicantInterface *supp_iface, + GCancellable * cancellable, + gpointer user_data) +{ + NMDeviceWifi * self; + NMDeviceWifiPrivate *priv; + + if (g_cancellable_is_cancelled(cancellable)) + return; + + self = user_data; + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + _LOGT_scan("scan request completed (D-Bus request)"); + + /* we just completed a scan request, but possibly the supplicant's state is not yet toggled + * to "scanning". That means, our internal scanning state "priv->scan_is_scanning" would already + * flip to idle, while in a moment the supplicant would toggle the state again. + * + * Artificially keep the scanning state on, for another SCAN_EXTRA_DELAY_MSEC msec. */ + nm_clear_g_source_inst(&priv->scan_request_delay_source); + priv->scan_request_delay_source = + nm_g_source_attach(nm_g_timeout_source_new(SCAN_EXTRA_DELAY_MSEC, + G_PRIORITY_DEFAULT, + _scan_request_delay_cb, + self, + NULL), + NULL); + + g_clear_object(&priv->scan_request_cancellable); + _scan_notify_is_scanning(self); +} + +static gboolean +_scan_kickoff_timeout_cb(gpointer user_data) +{ + NMDeviceWifi * self = user_data; + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + priv->scan_kickoff_timeout_id = 0; + _scan_kickoff(self); + return G_SOURCE_REMOVE; +} + +static void +_scan_kickoff(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *ssids = NULL; + gboolean is_explict = FALSE; + NMDeviceState device_state; + gboolean has_hidden_profiles; + gint64 now_msec; + gint64 ratelimit_duration_msec; + + if (!priv->sup_iface) { + _LOGT_scan("kickoff: don't scan (has no supplicant interface)"); + return; + } + + if (priv->scan_request_cancellable) { + _LOGT_scan("kickoff: don't scan (has scan_request_cancellable)"); + /* We are currently waiting for a scan request to complete. Wait longer. */ + return; + } + + now_msec = nm_utils_get_monotonic_timestamp_msec(); + + _scan_request_ssids_remove_all(priv, now_msec, G_MAXUINT); + + device_state = nm_device_get_state(NM_DEVICE(self)); + if (device_state > NM_DEVICE_STATE_DISCONNECTED && device_state <= NM_DEVICE_STATE_ACTIVATED) { + /* while we are activated, we rate limit more. */ + ratelimit_duration_msec = 8000; + } else + ratelimit_duration_msec = 1500; + + if (priv->scan_last_request_started_at_msec + ratelimit_duration_msec > now_msec) { + _LOGT_scan( + "kickoff: don't scan (rate limited for another %d.%03d sec%s)", + (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec) + / 1000), + (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec) + % 1000), + !priv->scan_kickoff_timeout_id ? ", schedule timeout" : ""); + if (!priv->scan_kickoff_timeout_id + && (priv->scan_explicit_allowed || priv->scan_periodic_allowed)) { + priv->scan_kickoff_timeout_id = g_timeout_add(priv->scan_last_request_started_at_msec + + ratelimit_duration_msec - now_msec, + _scan_kickoff_timeout_cb, + self); + } + return; + } + + if (priv->scan_last_complete_msec + 200 > now_msec) { + gint32 timeout_msec = priv->scan_last_complete_msec + 200 - now_msec; + + /* after a scan just completed, it is ratelimited for another 200 msec. This is in + * addition to our rate limiting above (where scanning can take longer than our rate limit + * duration). + * + * This gives the device a chance to autoconnect. Also, if a scanning just completed, + * we want to back off a bit before starting again. */ + _LOGT_scan("kickoff: don't scan (rate limited for another %d.%03d sec after previous scan)", + timeout_msec / 1000, + timeout_msec % 1000); + nm_clear_g_source(&priv->scan_kickoff_timeout_id); + priv->scan_kickoff_timeout_id = g_timeout_add(timeout_msec, _scan_kickoff_timeout_cb, self); + return; + } + + if (priv->scan_explicit_requested) { + if (!priv->scan_explicit_allowed) { + _LOGT_scan("kickoff: don't scan (explicit scan requested but not allowed)"); + return; + } + priv->scan_explicit_requested = FALSE; + is_explict = TRUE; + } else { + if (!priv->scan_periodic_allowed) { + _LOGT_scan("kickoff: don't scan (periodic scan currently not allowed)"); + priv->scan_periodic_next_msec = 0; + priv->scan_periodic_interval_sec = 0; + nm_clear_g_source(&priv->scan_kickoff_timeout_id); + return; + } + + nm_assert(priv->scan_explicit_allowed); + + if (now_msec < priv->scan_periodic_next_msec) { + _LOGT_scan("kickoff: don't scan (periodic scan waiting for another %d.%03d sec%s)", + (int) ((priv->scan_periodic_next_msec - now_msec) / 1000), + (int) ((priv->scan_periodic_next_msec - now_msec) % 1000), + !priv->scan_kickoff_timeout_id ? ", schedule timeout" : ""); + if (!priv->scan_kickoff_timeout_id) { + priv->scan_kickoff_timeout_id = + g_timeout_add_seconds((priv->scan_periodic_next_msec - now_msec + 999) / 1000, + _scan_kickoff_timeout_cb, + self); + } + return; + } + + priv->scan_periodic_interval_sec = + NM_CLAMP(((int) priv->scan_periodic_interval_sec) * 3 / 2, + SCAN_INTERVAL_SEC_MIN, + SCAN_INTERVAL_SEC_MAX); + priv->scan_periodic_next_msec = now_msec + 1000 * priv->scan_periodic_interval_sec; + } + + ssids = _scan_request_ssids_build_hidden(self, now_msec, &has_hidden_profiles); + if (has_hidden_profiles) { + if (priv->hidden_probe_scan_warn) { + priv->hidden_probe_scan_warn = FALSE; + _LOGW(LOGD_WIFI, + "wifi-scan: active scanning for networks due to profiles with wifi.hidden=yes. " + "This makes you trackable"); + } + } else if (!is_explict) + priv->hidden_probe_scan_warn = TRUE; + + if (_LOGD_ENABLED(LOGD_WIFI)) { + gs_free char *ssids_str = NULL; + guint ssids_len = 0; + + if (ssids) { + gs_strfreev char **strv = NULL; + guint i; + + strv = g_new(char *, ssids->len + 1u); + for (i = 0; i < ssids->len; i++) + strv[i] = _nm_utils_ssid_to_string(ssids->pdata[i]); + strv[i] = NULL; + + nm_assert(ssids->len > 0); + nm_assert(ssids->len == NM_PTRARRAY_LEN(strv)); + + ssids_str = g_strjoinv(", ", strv); + ssids_len = ssids->len; + } + _LOGD(LOGD_WIFI, + "wifi-scan: start %s scan (%u SSIDs to probe scan%s%s%s)", + is_explict ? "explicit" : "periodic", + ssids_len, + NM_PRINT_FMT_QUOTED(ssids_str, " [", ssids_str, "]", "")); + } + + priv->scan_last_request_started_at_msec = now_msec; + + if (is_explict) + _LOGT_scan("kickoff: explicit scan starting"); + else { + _LOGT_scan("kickoff: periodic scan starting (next scan is scheduled in %d.%03d sec)", + (int) ((priv->scan_periodic_next_msec - now_msec) / 1000), + (int) ((priv->scan_periodic_next_msec - now_msec) % 1000)); + } + + _hw_addr_set_scanning(self, FALSE); + + priv->scan_request_cancellable = g_cancellable_new(); + nm_supplicant_interface_request_scan(priv->sup_iface, + ssids ? (GBytes *const *) ssids->pdata : NULL, + ssids ? ssids->len : 0u, + priv->scan_request_cancellable, + _scan_supplicant_request_scan_cb, + self); + + /* It's OK to call _scan_notify_is_scanning() again. They mutually call each other, + * but _scan_kickoff() sets "priv->scan_request_cancellable" which will stop + * them from recursing indefinitely. */ + _scan_notify_is_scanning(self); +} + +/**************************************************************************** + * WPA Supplicant control stuff + * + */ + +static gboolean +ap_list_dump(gpointer user_data) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(user_data); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + priv->ap_dump_id = 0; + + if (_LOGD_ENABLED(LOGD_WIFI_SCAN)) { + NMWifiAP *ap; + gint64 now_msec = nm_utils_get_monotonic_timestamp_msec(); + char str_buf[100]; + + _LOGD(LOGD_WIFI_SCAN, + "APs: [now:%u.%03u, last:%s]", + (guint)(now_msec / NM_UTILS_MSEC_PER_SEC), + (guint)(now_msec % NM_UTILS_MSEC_PER_SEC), + priv->scan_last_complete_msec > 0 + ? nm_sprintf_buf(str_buf, + "%u.%03u", + (guint)(priv->scan_last_complete_msec / NM_UTILS_MSEC_PER_SEC), + (guint)(priv->scan_last_complete_msec % NM_UTILS_MSEC_PER_SEC)) + : "-1"); + c_list_for_each_entry (ap, &priv->aps_lst_head, aps_lst) + _ap_dump(self, LOGL_DEBUG, ap, "dump", now_msec); + } + return G_SOURCE_REMOVE; +} + +static void +schedule_ap_list_dump(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (!priv->ap_dump_id && _LOGD_ENABLED(LOGD_WIFI_SCAN)) + priv->ap_dump_id = g_timeout_add_seconds(1, ap_list_dump, self); +} + +static void +try_fill_ssid_for_hidden_ap(NMDeviceWifi *self, NMWifiAP *ap) +{ + const char * bssid; + NMSettingsConnection *const *connections; + guint i; + + g_return_if_fail(nm_wifi_ap_get_ssid(ap) == NULL); + + bssid = nm_wifi_ap_get_address(ap); + g_return_if_fail(bssid); + + /* Look for this AP's BSSID in the seen-bssids list of a connection, + * and if a match is found, copy over the SSID */ + connections = nm_settings_get_connections(nm_device_get_settings((NMDevice *) self), NULL); + for (i = 0; connections[i]; i++) { + NMSettingsConnection *sett_conn = connections[i]; + NMSettingWireless * s_wifi; + + if (!nm_settings_connection_has_seen_bssid(sett_conn, bssid)) + continue; + s_wifi = + nm_connection_get_setting_wireless(nm_settings_connection_get_connection(sett_conn)); + if (!s_wifi) + continue; + + nm_wifi_ap_set_ssid(ap, nm_setting_wireless_get_ssid(s_wifi)); + break; + } +} + +static void +supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface, + NMSupplicantBssInfo * bss_info, + gboolean is_present, + NMDeviceWifi * self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMWifiAP * found_ap; + GBytes * ssid; + + found_ap = g_hash_table_lookup(priv->aps_idx_by_supplicant_path, bss_info->bss_path); + + if (!is_present) { + if (!found_ap) + return; + if (found_ap == priv->current_ap) { + /* The current AP cannot be removed (to prevent NM indicating that + * it is connected, but to nothing), but it must be removed later + * when the current AP is changed or cleared. Set 'fake' to + * indicate that this AP is now unknown to the supplicant. + */ + if (nm_wifi_ap_set_fake(found_ap, TRUE)) + _ap_dump(self, LOGL_DEBUG, found_ap, "updated", 0); + } else { + ap_add_remove(self, FALSE, found_ap, TRUE); + schedule_ap_list_dump(self); + } + return; + } + + if (found_ap) { + if (!nm_wifi_ap_update_from_properties(found_ap, bss_info)) + return; + _ap_dump(self, LOGL_DEBUG, found_ap, "updated", 0); + } else { + gs_unref_object NMWifiAP *ap = NULL; + + if (!bss_info->bssid_valid) { + /* We failed to initialize the info about the AP. This can + * happen due to an error in the D-Bus communication. In this case + * we ignore the info. */ + return; + } + + ap = nm_wifi_ap_new_from_properties(bss_info); + + /* Let the manager try to fill in the SSID from seen-bssids lists */ + ssid = nm_wifi_ap_get_ssid(ap); + if (!ssid || _nm_utils_is_empty_ssid(ssid)) { + /* Try to fill the SSID from the AP database */ + try_fill_ssid_for_hidden_ap(self, ap); + + ssid = nm_wifi_ap_get_ssid(ap); + if (ssid && !_nm_utils_is_empty_ssid(ssid)) { + gs_free char *s = NULL; + + /* Yay, matched it, no longer treat as hidden */ + _LOGD(LOGD_WIFI, + "matched hidden AP %s => %s", + nm_wifi_ap_get_address(ap), + (s = _nm_utils_ssid_to_string(ssid))); + } else { + /* Didn't have an entry for this AP in the database */ + _LOGD(LOGD_WIFI, "failed to match hidden AP %s", nm_wifi_ap_get_address(ap)); + } + } + + ap_add_remove(self, TRUE, ap, TRUE); + } + + /* Update the current AP if the supplicant notified a current BSS change + * before it sent the current BSS's scan result. + */ + if (nm_supplicant_interface_get_current_bss(iface) == bss_info->bss_path) + supplicant_iface_notify_current_bss(priv->sup_iface, NULL, self); + + schedule_ap_list_dump(self); +} + +static void +cleanup_association_attempt(NMDeviceWifi *self, gboolean disconnect) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_clear_g_source(&priv->sup_timeout_id); + nm_clear_g_source(&priv->link_timeout_id); + nm_clear_g_source(&priv->wps_timeout_id); + if (disconnect && priv->sup_iface) + nm_supplicant_interface_disconnect(priv->sup_iface); +} + +static void +cleanup_supplicant_failures(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_clear_g_source(&priv->reacquire_iface_id); + priv->failed_iface_count = 0; +} + +static void +wifi_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * connection, + GError * error, + gpointer user_data) +{ + NMDevice * device = user_data; + NMDeviceWifi * self = user_data; + NMDeviceWifiPrivate *priv; + + g_return_if_fail(NM_IS_DEVICE_WIFI(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + g_return_if_fail(priv->wifi_secrets_id == call_id); + + priv->wifi_secrets_id = NULL; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail(req == nm_device_get_act_request(device)); + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + g_return_if_fail(nm_act_request_get_settings_connection(req) == connection); + + if (error) { + _LOGW(LOGD_WIFI, "no secrets: %s", error->message); + + /* Even if WPS is still pending, let's abort the activation when the secret + * request returns. + * + * This means, a user can only effectively use WPS when also running a secret + * agent, and pressing the push button while being prompted for the password. + * Note, that in the secret prompt the user can see that WPS is in progress + * (via the NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE flag). + * + * Previously, WPS was not cancelled when the secret request returns. + * Note that in common use-cases WPS is enabled in the connection profile + * but it won't succeed (because it's disabled in the AP or because the + * user is not prepared to press the push button). + * That means for example, during boot we would try to autoconnect with WPS. + * At that point, there is no secret-agent running, and WPS is pending for + * full 30 seconds. If in the meantime a secret agent registers (because + * of logging into the DE), the profile is still busy waiting for WPS to time + * out. Only after that delay, autoconnect starts again (note that autoconnect gets + * not blocked in this case, because a secret agent registered in the meantime). + * + * It seems wrong to continue doing WPS if the user is not aware + * that WPS is ongoing. The user is required to perform an action (push button), + * and must be told via the secret prompt. + * If no secret-agent is running, if the user cancels the secret-request, or any + * other error to obtain secrets, the user apparently does not want WPS either. + */ + nm_clear_g_source(&priv->wps_timeout_id); + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +wifi_secrets_cancel(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + if (priv->wifi_secrets_id) + nm_act_request_cancel_secrets(NULL, priv->wifi_secrets_id); + nm_assert(!priv->wifi_secrets_id); +} + +static void +supplicant_iface_wps_credentials_cb(NMSupplicantInterface *iface, + GVariant * credentials, + NMDeviceWifi * self) +{ + NMActRequest * req; + gs_unref_variant GVariant *val_key = NULL; + gs_unref_variant GVariant *secrets = NULL; + gs_free_error GError *error = NULL; + const char * array; + gsize psk_len = 0; + + if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_NEED_AUTH) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, "WPS: The connection can't be updated with credentials"); + return; + } + + _LOGI(LOGD_DEVICE | LOGD_WIFI, "WPS: Updating the connection with credentials"); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + val_key = g_variant_lookup_value(credentials, "Key", G_VARIANT_TYPE_BYTESTRING); + if (val_key) { + char psk[64]; + + array = g_variant_get_fixed_array(val_key, &psk_len, 1); + if (psk_len >= 8 && psk_len <= 63) { + memcpy(psk, array, psk_len); + psk[psk_len] = '\0'; + if (g_utf8_validate(psk, psk_len, NULL)) { + secrets = g_variant_new_parsed("[{%s, [{%s, <%s>}]}]", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PSK, + psk); + g_variant_ref_sink(secrets); + } + } + if (!secrets) + _LOGW(LOGD_DEVICE | LOGD_WIFI, "WPS: ignore invalid PSK"); + } + + if (!secrets) + return; + + if (!nm_settings_connection_new_secrets(nm_act_request_get_settings_connection(req), + nm_act_request_get_applied_connection(req), + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + secrets, + &error)) { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "WPS: Could not update the connection with credentials: %s", + error->message); + return; + } + + wifi_secrets_cancel(self); + nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE); +} + +static gboolean +wps_timeout_cb(gpointer user_data) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(user_data); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + priv->wps_timeout_id = 0; + if (!priv->wifi_secrets_id) { + /* Fail only if the secrets are not being requested. */ + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_NO_SECRETS); + } + + return G_SOURCE_REMOVE; +} + +static void +wifi_secrets_get_secrets(NMDeviceWifi * self, + const char * setting_name, + NMSecretAgentGetSecretsFlags flags) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMActRequest * req; + + wifi_secrets_cancel(self); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_if_fail(NM_IS_ACT_REQUEST(req)); + + priv->wifi_secrets_id = + nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, wifi_secrets_cb, self); + g_return_if_fail(priv->wifi_secrets_id); +} + +/* + * link_timeout_cb + * + * Called when the link to the access point has been down for a specified + * period of time. + */ +static gboolean +link_timeout_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + _LOGW(LOGD_WIFI, "link timed out."); + + priv->link_timeout_id = 0; + + /* Disconnect event while activated; the supplicant hasn't been able + * to reassociate within the timeout period, so the connection must + * fail. + */ + if (nm_device_get_state(device) != NM_DEVICE_STATE_ACTIVATED) + return FALSE; + + set_current_ap(self, NULL, TRUE); + + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + priv->ssid_found ? NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT + : NM_DEVICE_STATE_REASON_SSID_NOT_FOUND); + return FALSE; +} + +static gboolean +need_new_8021x_secrets(NMDeviceWifi * self, + NMSupplicantInterfaceState old_state, + const char ** setting_name) +{ + NMSetting8021x * s_8021x; + NMSettingWirelessSecurity *s_wsec; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + NMConnection * connection; + + g_return_val_if_fail(setting_name, FALSE); + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + g_return_val_if_fail(connection != NULL, FALSE); + + /* 802.1x stuff only happens in the supplicant's ASSOCIATED state when it's + * attempting to authenticate with the AP. + */ + if (old_state != NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED) + return FALSE; + + /* If it's an 802.1x or LEAP connection with "always ask"/unsaved secrets + * then we need to ask again because it might be an OTP token and the PIN + * may have changed. + */ + + s_8021x = nm_connection_get_setting_802_1x(connection); + if (s_8021x) { + if (!nm_setting_get_secret_flags(NM_SETTING(s_8021x), + NM_SETTING_802_1X_PASSWORD, + &secret_flags, + NULL)) + g_assert_not_reached(); + if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) + *setting_name = NM_SETTING_802_1X_SETTING_NAME; + return *setting_name ? TRUE : FALSE; + } + + s_wsec = nm_connection_get_setting_wireless_security(connection); + if (s_wsec) { + if (!nm_setting_get_secret_flags(NM_SETTING(s_wsec), + NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD, + &secret_flags, + NULL)) + g_assert_not_reached(); + if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) + *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME; + return *setting_name ? TRUE : FALSE; + } + + /* Not a LEAP or 802.1x connection */ + return FALSE; +} + +static gboolean +need_new_wpa_psk(NMDeviceWifi * self, + NMSupplicantInterfaceState old_state, + int disconnect_reason, + const char ** setting_name) +{ + NMSettingWirelessSecurity *s_wsec; + NMConnection * connection; + const char * key_mgmt = NULL; + + g_return_val_if_fail(setting_name, FALSE); + + connection = nm_device_get_applied_connection(NM_DEVICE(self)); + + g_return_val_if_fail(connection, FALSE); + + /* A bad PSK will cause the supplicant to disconnect during the 4-way handshake */ + if (old_state != NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE) + return FALSE; + + s_wsec = nm_connection_get_setting_wireless_security(connection); + if (s_wsec) + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + + if (g_strcmp0(key_mgmt, "wpa-psk") == 0) { +/* -4 (locally-generated WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY) usually + * means the driver missed beacons from the AP. This usually happens + * due to driver bugs or faulty power-save management. It doesn't + * indicate that the PSK is wrong. + */ +#define LOCAL_WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY -4 + if (disconnect_reason == LOCAL_WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY) + return FALSE; + + *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME; + return TRUE; + } + + /* Not a WPA-PSK connection */ + return FALSE; +} + +static gboolean +handle_8021x_or_psk_auth_fail(NMDeviceWifi * self, + NMSupplicantInterfaceState new_state, + NMSupplicantInterfaceState old_state, + int disconnect_reason) +{ + NMDevice * device = NM_DEVICE(self); + NMActRequest *req; + const char * setting_name = NULL; + gboolean handled = FALSE; + + g_return_val_if_fail(new_state == NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, FALSE); + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_val_if_fail(req != NULL, FALSE); + + if (need_new_8021x_secrets(self, old_state, &setting_name) + || need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) { + nm_act_request_clear_secrets(req); + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) disconnected during association, asking for new key"); + + cleanup_association_attempt(self, TRUE); + nm_device_state_changed(device, + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + wifi_secrets_get_secrets(self, + setting_name, + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW); + handled = TRUE; + } + + return handled; +} + +static gboolean +reacquire_interface_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + priv->reacquire_iface_id = 0; + priv->failed_iface_count++; + + _LOGW(LOGD_WIFI, "re-acquiring supplicant interface (#%d).", priv->failed_iface_count); + + if (!priv->sup_iface) + supplicant_interface_acquire(self); + + return G_SOURCE_REMOVE; +} + +static void +supplicant_iface_state_down(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + cleanup_association_attempt(self, FALSE); + + /* If the device is already in UNAVAILABLE state then the state change + * is a NOP and the interface won't be re-acquired in the device state + * change handler. So ensure we have a new one here so that we're + * ready if the supplicant comes back. + */ + supplicant_interface_release(self); + if (priv->failed_iface_count < 5) + priv->reacquire_iface_id = g_timeout_add_seconds(10, reacquire_interface_cb, self); + else + _LOGI(LOGD_DEVICE | LOGD_WIFI, "supplicant interface keeps failing, giving up"); +} + +static void +supplicant_iface_state(NMDeviceWifi * self, + NMSupplicantInterfaceState new_state, + NMSupplicantInterfaceState old_state, + int disconnect_reason, + gboolean is_real_signal) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMDeviceState devstate; + gboolean scanning; + gboolean scan_changed; + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "supplicant interface state: %s -> %s%s", + nm_supplicant_interface_state_to_string(old_state), + nm_supplicant_interface_state_to_string(new_state), + is_real_signal ? "" : " (simulated signal)"); + + if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) { + supplicant_iface_state_down(self); + goto out; + } + + devstate = nm_device_get_state(device); + scanning = nm_supplicant_interface_get_scanning(priv->sup_iface); + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) { + _LOGD(LOGD_WIFI, "supplicant ready"); + nm_device_queue_recheck_available(NM_DEVICE(device), + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + priv->scan_periodic_interval_sec = 0; + priv->scan_periodic_next_msec = 0; + } + + /* In these states we know the supplicant is actually talking to something */ + if (new_state >= NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING + && new_state <= NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) + priv->ssid_found = TRUE; + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) + recheck_p2p_availability(self); + + switch (new_state) { + case NM_SUPPLICANT_INTERFACE_STATE_COMPLETED: + nm_clear_g_source(&priv->sup_timeout_id); + nm_clear_g_source(&priv->link_timeout_id); + nm_clear_g_source(&priv->wps_timeout_id); + + /* If this is the initial association during device activation, + * schedule the next activation stage. + */ + if (devstate == NM_DEVICE_STATE_CONFIG) { + NMSettingWireless *s_wifi; + GBytes * ssid; + gs_free char * ssid_str = NULL; + + s_wifi = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_WIRELESS); + + g_return_if_fail(s_wifi); + + ssid = nm_setting_wireless_get_ssid(s_wifi); + g_return_if_fail(ssid); + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) Stage 2 of 5 (Device Configure) successful. %s %s", + priv->mode == NM_802_11_MODE_AP ? "Started Wi-Fi Hotspot" + : "Connected to wireless network", + (ssid_str = _nm_utils_ssid_to_string(ssid))); + nm_device_activate_schedule_stage3_ip_config_start(device); + } else if (devstate == NM_DEVICE_STATE_ACTIVATED) + periodic_update(self); + break; + case NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED: + if ((devstate == NM_DEVICE_STATE_ACTIVATED) || nm_device_is_activating(device)) { + /* Disconnect of an 802.1x/LEAP connection during authentication, + * or disconnect of a WPA-PSK connection during the 4-way handshake, + * often means secrets are wrong. Not always the case, but until we + * have more information from wpa_supplicant about why the + * disconnect happened this is the best we can do. + */ + if (handle_8021x_or_psk_auth_fail(self, new_state, old_state, disconnect_reason)) + break; + } + + /* Otherwise, it might be a stupid driver or some transient error, so + * let the supplicant try to reconnect a few more times. Give it more + * time if a scan is in progress since the link might be dropped during + * the scan but will be re-established when the scan is done. + */ + if (devstate == NM_DEVICE_STATE_ACTIVATED) { + if (priv->link_timeout_id == 0) { + priv->link_timeout_id = + g_timeout_add_seconds(scanning ? 30 : 15, link_timeout_cb, self); + priv->ssid_found = FALSE; + } + } + break; + case NM_SUPPLICANT_INTERFACE_STATE_INACTIVE: + /* we would clear _scan_has_pending_action_set() and trigger a new scan. + * However, we don't want to cancel the current pending action, so force + * a new scan request. */ + break; + default: + break; + } + +out: + scan_changed = _scan_notify_allowed(self, NM_TERNARY_FALSE); + scan_changed |= _scan_notify_is_scanning(self); + if (scan_changed) + _scan_kickoff(self); + + if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) + nm_device_remove_pending_action(device, NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, TRUE); +} + +static void +supplicant_iface_state_cb(NMSupplicantInterface *iface, + int new_state_i, + int old_state_i, + int disconnect_reason, + gpointer user_data) +{ + supplicant_iface_state(user_data, new_state_i, old_state_i, disconnect_reason, TRUE); +} + +static void +supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data) +{ + NMDeviceWifi *self = NM_DEVICE_WIFI(user_data); + NMDevice * device = NM_DEVICE(self); + + if (error && !nm_utils_error_is_cancelled_or_disposing(error) + && nm_device_is_activating(device)) { + cleanup_association_attempt(self, TRUE); + nm_device_queue_state(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + } +} + +static void +supplicant_iface_notify_current_bss(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceWifi * self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMRefString * current_bss; + NMWifiAP * new_ap = NULL; + + current_bss = nm_supplicant_interface_get_current_bss(iface); + if (current_bss) + new_ap = g_hash_table_lookup(priv->aps_idx_by_supplicant_path, current_bss); + + if (new_ap != priv->current_ap) { + const char * new_bssid = NULL; + GBytes * new_ssid = NULL; + const char * old_bssid = NULL; + GBytes * old_ssid = NULL; + gs_free char *new_ssid_s = NULL; + gs_free char *old_ssid_s = NULL; + + /* Don't ever replace a "fake" current AP if we don't know about the + * supplicant's current BSS yet. It'll get replaced when we receive + * the current BSS's scan result. + */ + if (new_ap == NULL && nm_wifi_ap_get_fake(priv->current_ap)) + return; + + if (new_ap) { + new_bssid = nm_wifi_ap_get_address(new_ap); + new_ssid = nm_wifi_ap_get_ssid(new_ap); + } + + if (priv->current_ap) { + old_bssid = nm_wifi_ap_get_address(priv->current_ap); + old_ssid = nm_wifi_ap_get_ssid(priv->current_ap); + } + + _LOGD(LOGD_WIFI, + "roamed from BSSID %s (%s) to %s (%s)", + old_bssid ?: "(none)", + (old_ssid_s = _nm_utils_ssid_to_string(old_ssid)), + new_bssid ?: "(none)", + (new_ssid_s = _nm_utils_ssid_to_string(new_ssid))); + + if (new_bssid) { + /* The new AP could be in a different layer 3 network + * and so the old DHCP lease could be no longer valid. + * Also, some APs (e.g. Cisco) can be configured to drop + * all traffic until DHCP completes. To support such + * cases, renew the lease when roaming to a new AP. */ + nm_device_update_dynamic_ip_setup(NM_DEVICE(self)); + } + + set_current_ap(self, new_ap, TRUE); + } +} + +/* We bind the existence of the P2P device to a wifi device that is being + * managed by NetworkManager and is capable of P2P operation. + * Note that some care must be taken here, because we don't want to re-create + * the device every time the supplicant interface is destroyed (e.g. due to + * a suspend/resume cycle). + * Therefore, this function will be called when a change in the P2P capability + * is detected and the supplicant interface has been initialised. + */ +static void +recheck_p2p_availability(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gboolean p2p_available; + + g_object_get(priv->sup_iface, NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE, &p2p_available, NULL); + + if (p2p_available && !priv->p2p_device) { + gs_free char *iface_name = NULL; + + /* Create a P2P device. "p2p-dev-" is the same prefix as chosen by + * wpa_supplicant internally. + */ + iface_name = g_strconcat("p2p-dev-", nm_device_get_iface(NM_DEVICE(self)), NULL); + + priv->p2p_device = nm_device_wifi_p2p_new(iface_name); + + nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, priv->sup_iface); + + g_signal_emit(self, signals[P2P_DEVICE_CREATED], 0, priv->p2p_device); + g_object_add_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device); + g_object_unref(priv->p2p_device); + return; + } + + if (p2p_available && priv->p2p_device) { + nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, priv->sup_iface); + return; + } + + if (!p2p_available && priv->p2p_device) { + /* Destroy the P2P device. */ + g_object_remove_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device); + nm_device_wifi_p2p_remove(g_steal_pointer(&priv->p2p_device)); + return; + } +} + +static void +supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface, + GParamSpec * pspec, + NMDeviceWifi * self) +{ + if (nm_supplicant_interface_get_state(iface) > NM_SUPPLICANT_INTERFACE_STATE_STARTING) + recheck_p2p_availability(self); +} + +static gboolean +handle_auth_or_fail(NMDeviceWifi *self, NMActRequest *req, gboolean new_secrets) +{ + NMDeviceWifiPrivate * priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + const char * setting_name; + NMConnection * applied_connection; + NMSettingWirelessSecurity * s_wsec; + const char * bssid = NULL; + NM80211ApFlags ap_flags; + NMSettingWirelessSecurityWpsMethod wps_method; + const char * type; + NMSecretAgentGetSecretsFlags get_secret_flags = + NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION; + + g_return_val_if_fail(NM_IS_DEVICE_WIFI(self), FALSE); + + if (!req) { + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_val_if_fail(req, FALSE); + } + + if (!nm_device_auth_retries_try_next(NM_DEVICE(self))) + return FALSE; + + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_NONE); + + applied_connection = nm_act_request_get_applied_connection(req); + s_wsec = nm_connection_get_setting_wireless_security(applied_connection); + wps_method = nm_setting_wireless_security_get_wps_method(s_wsec); + + /* Negotiate the WPS method */ + if (wps_method == NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_DEFAULT) + wps_method = NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO; + + if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO && priv->current_ap) { + /* Determine the method to use from AP capabilities. */ + ap_flags = nm_wifi_ap_get_flags(priv->current_ap); + if (ap_flags & NM_802_11_AP_FLAGS_WPS_PBC) + wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC; + if (ap_flags & NM_802_11_AP_FLAGS_WPS_PIN) + wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN; + if (ap_flags & NM_802_11_AP_FLAGS_WPS + && wps_method == NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO) { + /* The AP doesn't specify which methods are supported. Allow all. */ + wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC; + wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN; + } + } + + if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC) { + get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE; + type = "pbc"; + } else if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN) { + type = "pin"; + } else + type = NULL; + + if (type) { + priv->wps_timeout_id = g_timeout_add_seconds(30, wps_timeout_cb, self); + if (priv->current_ap) + bssid = nm_wifi_ap_get_address(priv->current_ap); + nm_supplicant_interface_enroll_wps(priv->sup_iface, type, bssid, NULL); + } + + nm_act_request_clear_secrets(req); + setting_name = nm_connection_need_secrets(applied_connection, NULL); + if (!setting_name) { + _LOGW(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets."); + return FALSE; + } + + if (new_secrets) + get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + wifi_secrets_get_secrets(self, setting_name, get_secret_flags); + return TRUE; +} + +/* + * supplicant_connection_timeout_cb + * + * Called when the supplicant has been unable to connect to an access point + * within a specified period of time. + */ +static gboolean +supplicant_connection_timeout_cb(gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceWifi * self = NM_DEVICE_WIFI(user_data); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMActRequest * req; + NMConnection * connection; + + cleanup_association_attempt(self, TRUE); + + if (!nm_device_is_activating(device)) + return FALSE; + + /* Timed out waiting for a successful connection to the AP; if the AP's + * security requires network-side authentication (like WPA or 802.1x) + * and the connection attempt timed out then it's likely the authentication + * information (passwords, pin codes, etc) are wrong. + */ + + req = nm_device_get_act_request(device); + g_assert(req); + + connection = nm_act_request_get_applied_connection(req); + g_assert(connection); + + if (NM_IN_SET(priv->mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_MESH, NM_802_11_MODE_AP)) { + /* In Ad-Hoc and AP modes there's nothing to check the encryption key + * (if any), so supplicant timeouts here are almost certainly the wifi + * driver being really stupid. + */ + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) %s network creation took too long, failing activation", + priv->mode == NM_802_11_MODE_ADHOC ? "Ad-Hoc" : "Hotspot"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT); + return FALSE; + } + + g_assert(priv->mode == NM_802_11_MODE_INFRA); + + if (priv->ssid_found && nm_connection_get_setting_wireless_security(connection)) { + guint64 timestamp = 0; + gboolean new_secrets = TRUE; + + /* Connection failed; either driver problems, the encryption key is + * wrong, or the passwords or certificates were wrong. + */ + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) association took too long"); + + /* Ask for new secrets only if we've never activated this connection + * before. If we've connected before, don't bother the user with + * dialogs, just retry or fail, and if we never connect the user can + * fix the password somewhere else. + */ + if (nm_settings_connection_get_timestamp(nm_act_request_get_settings_connection(req), + ×tamp)) + new_secrets = !timestamp; + + if (handle_auth_or_fail(self, req, new_secrets)) + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) asking for new secrets"); + else { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_NO_SECRETS); + } + } else { + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) association took too long, failing activation"); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + priv->ssid_found ? NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT + : NM_DEVICE_STATE_REASON_SSID_NOT_FOUND); + } + + return FALSE; +} + +static NMSupplicantConfig * +build_supplicant_config(NMDeviceWifi *self, + NMConnection *connection, + guint32 fixed_freq, + GError ** error) +{ + NMDeviceWifiPrivate * priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSupplicantConfig * config = NULL; + NMSettingWireless * s_wireless; + NMSettingWirelessSecurity * s_wireless_sec; + NMSettingWirelessSecurityPmf pmf; + NMSettingWirelessSecurityFils fils; + NMTernary ap_isolation; + + g_return_val_if_fail(priv->sup_iface, NULL); + + s_wireless = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wireless != NULL, NULL); + + config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface)); + + /* Warn if AP mode may not be supported */ + if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP) + && nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_AP) + != NM_TERNARY_TRUE) { + _LOGW(LOGD_WIFI, "Supplicant may not support AP mode; connection may time out."); + } + + if (!nm_supplicant_config_add_setting_wireless(config, s_wireless, fixed_freq, error)) { + g_prefix_error(error, "802-11-wireless: "); + goto error; + } + + if (!nm_supplicant_config_add_bgscan(config, connection, error)) { + g_prefix_error(error, "bgscan: "); + goto error; + } + + ap_isolation = nm_setting_wireless_get_ap_isolation(s_wireless); + if (ap_isolation == NM_TERNARY_DEFAULT) { + ap_isolation = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + "wifi.ap-isolation", + NM_DEVICE(self), + NM_TERNARY_FALSE, + NM_TERNARY_TRUE, + NM_TERNARY_FALSE); + } + nm_supplicant_config_set_ap_isolation(config, ap_isolation == NM_TERNARY_TRUE); + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + if (s_wireless_sec) { + NMSetting8021x *s_8021x; + const char * con_uuid = nm_connection_get_uuid(connection); + guint32 mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)), + nm_device_get_ifindex(NM_DEVICE(self))); + + g_assert(con_uuid); + + /* Configure PMF (802.11w) */ + pmf = nm_setting_wireless_security_get_pmf(s_wireless_sec); + if (pmf == NM_SETTING_WIRELESS_SECURITY_PMF_DEFAULT) { + pmf = nm_config_data_get_connection_default_int64( + NM_CONFIG_GET_DATA, + "wifi-sec.pmf", + NM_DEVICE(self), + NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE, + NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED, + NM_SETTING_WIRELESS_SECURITY_PMF_OPTIONAL); + } + + /* Configure FILS (802.11ai) */ + fils = nm_setting_wireless_security_get_fils(s_wireless_sec); + if (fils == NM_SETTING_WIRELESS_SECURITY_FILS_DEFAULT) { + fils = nm_config_data_get_connection_default_int64( + NM_CONFIG_GET_DATA, + "wifi-sec.fils", + NM_DEVICE(self), + NM_SETTING_WIRELESS_SECURITY_FILS_DISABLE, + NM_SETTING_WIRELESS_SECURITY_FILS_REQUIRED, + NM_SETTING_WIRELESS_SECURITY_FILS_OPTIONAL); + } + + s_8021x = nm_connection_get_setting_802_1x(connection); + if (!nm_supplicant_config_add_setting_wireless_security(config, + s_wireless_sec, + s_8021x, + con_uuid, + mtu, + pmf, + fils, + error)) { + g_prefix_error(error, "802-11-wireless-security: "); + goto error; + } + } else { + if (!nm_supplicant_config_add_no_security(config, error)) { + g_prefix_error(error, "unsecured-option: "); + goto error; + } + } + + return config; + +error: + g_object_unref(config); + return NULL; +} + +/*****************************************************************************/ + +static gboolean +wake_on_wlan_enable(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate * priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMSettingWirelessWakeOnWLan wowl; + NMSettingWireless * s_wireless; + + s_wireless = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_WIRELESS); + if (s_wireless) { + wowl = nm_setting_wireless_get_wake_on_wlan(s_wireless); + if (wowl != NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT) + goto found; + } + + wowl = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + "wifi.wake-on-wlan", + NM_DEVICE(self), + NM_SETTING_WIRELESS_WAKE_ON_WLAN_NONE, + G_MAXINT32, + NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT); + + if (NM_FLAGS_ANY(wowl, NM_SETTING_WIRELESS_WAKE_ON_WLAN_EXCLUSIVE_FLAGS)) { + if (!nm_utils_is_power_of_two(wowl)) { + _LOGD(LOGD_WIFI, + "invalid default value %u for wake-on-wlan: " + "'default' and 'ignore' are exclusive flags", + (guint) wowl); + wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT; + } + } else if (NM_FLAGS_ANY(wowl, ~NM_SETTING_WIRELESS_WAKE_ON_WLAN_ALL)) { + _LOGD(LOGD_WIFI, "invalid default value %u for wake-on-wlan", (guint) wowl); + wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT; + } + if (wowl != NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT) + goto found; + + wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE; +found: + if (wowl == NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE) { + priv->wowlan_restore = wowl; + return TRUE; + } + + priv->wowlan_restore = + nm_platform_wifi_get_wake_on_wlan(NM_PLATFORM_GET, nm_device_get_ifindex(NM_DEVICE(self))); + + return nm_platform_wifi_set_wake_on_wlan(NM_PLATFORM_GET, + nm_device_get_ifindex(NM_DEVICE(self)), + wowl); +} + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMWifiAP * ap = NULL; + gs_unref_object NMWifiAP *ap_fake = NULL; + NMActRequest * req; + NMConnection * connection; + NMSettingWireless * s_wireless; + const char * mode; + const char * ap_path; + + req = nm_device_get_act_request(NM_DEVICE(self)); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + connection = nm_act_request_get_applied_connection(req); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + s_wireless = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE); + + nm_supplicant_interface_cancel_wps(priv->sup_iface); + + mode = nm_setting_wireless_get_mode(s_wireless); + if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_INFRA) == 0) + priv->mode = NM_802_11_MODE_INFRA; + else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0) + priv->mode = NM_802_11_MODE_ADHOC; + else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0) { + priv->mode = NM_802_11_MODE_AP; + + /* Scanning not done in AP mode; clear the scan list */ + remove_all_aps(self); + } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0) + priv->mode = NM_802_11_MODE_MESH; + _notify(self, PROP_MODE); + + /* expire the temporary MAC address used during scanning */ + priv->hw_addr_scan_expire = 0; + + /* Set spoof MAC to the interface */ + if (!nm_device_hw_addr_set_cloned(device, connection, TRUE)) { + *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED; + return NM_ACT_STAGE_RETURN_FAILURE; + } + + /* AP and Mesh modes never use a specific object or existing scanned AP */ + if (!NM_IN_SET(priv->mode, NM_802_11_MODE_AP, NM_802_11_MODE_MESH)) { + ap_path = nm_active_connection_get_specific_object(NM_ACTIVE_CONNECTION(req)); + ap = ap_path ? nm_wifi_ap_lookup_for_device(NM_DEVICE(self), ap_path) : NULL; + } + if (!ap) + ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection); + + if (!ap) { + /* If the user is trying to connect to an AP that NM doesn't yet know about + * (hidden network or something), starting a Hotspot or joining a Mesh, + * create a fake APfrom the security settings in the connection. This "fake" + * AP gets used until the real one is found in the scan list (Ad-Hoc or Hidden), + * or until the device is deactivated (Hotspot). + */ + ap_fake = nm_wifi_ap_new_fake_from_connection(connection); + if (!ap_fake) + g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE); + + if (nm_wifi_ap_is_hotspot(ap_fake)) + nm_wifi_ap_set_address(ap_fake, nm_device_get_hw_address(device)); + + g_object_freeze_notify(G_OBJECT(self)); + ap_add_remove(self, TRUE, ap_fake, TRUE); + g_object_thaw_notify(G_OBJECT(self)); + ap = ap_fake; + } + + _scan_notify_allowed(self, NM_TERNARY_DEFAULT); + + set_current_ap(self, ap, FALSE); + nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req), + nm_dbus_object_get_path(NM_DBUS_OBJECT(ap))); + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static void +ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP *ap) +{ + NMDevice * device = NM_DEVICE(self); + const char * band = nm_setting_wireless_get_band(s_wifi); + const guint32 a_freqs[] = {5180, 5200, 5220, 5745, 5765, 5785, 5805, 0}; + const guint32 bg_freqs[] = {2412, 2437, 2462, 2472, 0}; + guint32 freq = 0; + + g_assert(ap); + + if (nm_wifi_ap_get_freq(ap)) + return; + + if (g_strcmp0(band, "a") == 0) + freq = nm_platform_wifi_find_frequency(nm_device_get_platform(device), + nm_device_get_ifindex(device), + a_freqs); + else + freq = nm_platform_wifi_find_frequency(nm_device_get_platform(device), + nm_device_get_ifindex(device), + bg_freqs); + + if (!freq) + freq = (g_strcmp0(band, "a") == 0) ? 5180 : 2462; + + if (nm_wifi_ap_set_freq(ap, freq)) + _ap_dump(self, LOGL_DEBUG, ap, "updated", 0); +} + +static void +set_powersave(NMDevice *device) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMSettingWireless * s_wireless; + NMSettingWirelessPowersave val; + + s_wireless = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS); + + g_return_if_fail(s_wireless); + + val = nm_setting_wireless_get_powersave(s_wireless); + if (val == NM_SETTING_WIRELESS_POWERSAVE_DEFAULT) { + val = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA, + "wifi.powersave", + device, + NM_SETTING_WIRELESS_POWERSAVE_IGNORE, + NM_SETTING_WIRELESS_POWERSAVE_ENABLE, + NM_SETTING_WIRELESS_POWERSAVE_IGNORE); + } + + _LOGT(LOGD_WIFI, "powersave is set to %u", (unsigned) val); + + if (val == NM_SETTING_WIRELESS_POWERSAVE_IGNORE) + return; + + nm_platform_wifi_set_powersave(nm_device_get_platform(device), + nm_device_get_ifindex(device), + val == NM_SETTING_WIRELESS_POWERSAVE_ENABLE); +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gs_unref_object NMSupplicantConfig *config = NULL; + NM80211Mode ap_mode; + NMActRequest * req; + NMWifiAP * ap; + NMConnection * connection; + const char * setting_name; + NMSettingWireless * s_wireless; + GError * error = NULL; + guint timeout; + NMActRequest * request; + NMActiveConnection * master_ac; + NMDevice * master; + + nm_clear_g_source(&priv->sup_timeout_id); + nm_clear_g_source(&priv->link_timeout_id); + nm_clear_g_source(&priv->wps_timeout_id); + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + ap = priv->current_ap; + if (!ap) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED); + goto out_fail; + } + + ap_mode = nm_wifi_ap_get_mode(ap); + + connection = nm_act_request_get_applied_connection(req); + s_wireless = nm_connection_get_setting_wireless(connection); + nm_assert(s_wireless); + + /* If we need secrets, get them */ + setting_name = nm_connection_need_secrets(connection, NULL); + if (setting_name) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) access point '%s' has security, but secrets are required.", + nm_connection_get_id(connection)); + + if (!handle_auth_or_fail(self, req, FALSE)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + goto out_fail; + } + + return NM_ACT_STAGE_RETURN_POSTPONE; + } + + if (!wake_on_wlan_enable(self)) + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot configure WoWLAN."); + + /* have secrets, or no secrets required */ + if (nm_connection_get_setting_wireless_security(connection)) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) connection '%s' has security, and secrets exist. No new secrets " + "needed.", + nm_connection_get_id(connection)); + } else { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) connection '%s' requires no security. No secrets needed.", + nm_connection_get_id(connection)); + } + + priv->ssid_found = FALSE; + + /* Supplicant requires an initial frequency for Ad-Hoc, Hotspot and Mesh; + * if the user didn't specify one and we didn't find an AP that matched + * the connection, just pick a frequency the device supports. + */ + if (NM_IN_SET(ap_mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_MESH) || nm_wifi_ap_is_hotspot(ap)) + ensure_hotspot_frequency(self, s_wireless, ap); + + if (ap_mode == NM_802_11_MODE_INFRA) + set_powersave(device); + + /* Build up the supplicant configuration */ + config = build_supplicant_config(self, connection, nm_wifi_ap_get_freq(ap), &error); + if (!config) { + _LOGE(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) couldn't build wireless configuration: %s", + error->message); + g_clear_error(&error); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED); + goto out_fail; + } + + /* Tell the supplicant in which bridge the interface is */ + if ((request = nm_device_get_act_request(device)) + && (master_ac = nm_active_connection_get_master(NM_ACTIVE_CONNECTION(request))) + && (master = nm_active_connection_get_device(master_ac)) + && nm_device_get_device_type(master) == NM_DEVICE_TYPE_BRIDGE) { + nm_supplicant_interface_set_bridge(priv->sup_iface, nm_device_get_iface(master)); + } else + nm_supplicant_interface_set_bridge(priv->sup_iface, NULL); + + nm_supplicant_interface_assoc(priv->sup_iface, config, supplicant_iface_assoc_cb, self); + + /* Set up a timeout on the association attempt */ + timeout = nm_device_get_supplicant_timeout(NM_DEVICE(self)); + priv->sup_timeout_id = g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self); + + if (!priv->periodic_update_id) + priv->periodic_update_id = g_timeout_add_seconds(6, periodic_update_cb, self); + + /* We'll get stage3 started when the supplicant connects */ + return NM_ACT_STAGE_RETURN_POSTPONE; + +out_fail: + cleanup_association_attempt(self, TRUE); + wake_on_wlan_restore(self); + return NM_ACT_STAGE_RETURN_FAILURE; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + gboolean indicate_addressing_running; + NMConnection *connection; + const char * method; + + connection = nm_device_get_applied_connection(device); + + method = nm_utils_get_ip_config_method(connection, addr_family); + if (addr_family == AF_INET) + indicate_addressing_running = NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO); + else { + indicate_addressing_running = NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_AUTO, + NM_SETTING_IP6_CONFIG_METHOD_DHCP); + } + + if (indicate_addressing_running) + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device), + TRUE); + + return NM_DEVICE_CLASS(nm_device_wifi_parent_class) + ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason); +} + +static guint32 +get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + return nm_device_get_configured_mtu_from_connection(device, + NM_TYPE_SETTING_WIRELESS, + out_source); +} + +static gboolean +is_static_wep(NMConnection *connection) +{ + NMSettingWirelessSecurity *s_wsec; + const char * str; + + g_return_val_if_fail(connection != NULL, FALSE); + + s_wsec = nm_connection_get_setting_wireless_security(connection); + if (!s_wsec) + return FALSE; + + str = nm_setting_wireless_security_get_key_mgmt(s_wsec); + if (g_strcmp0(str, "none") != 0) + return FALSE; + + str = nm_setting_wireless_security_get_auth_alg(s_wsec); + if (g_strcmp0(str, "leap") == 0) + return FALSE; + + return TRUE; +} + +static NMActStageReturn +act_stage4_ip_config_timeout(NMDevice * device, + int addr_family, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMConnection * connection; + NMSettingIPConfig * s_ip; + gboolean may_fail; + + connection = nm_device_get_applied_connection(device); + s_ip = nm_connection_get_setting_ip_config(connection, addr_family); + may_fail = nm_setting_ip_config_get_may_fail(s_ip); + + if (priv->mode == NM_802_11_MODE_AP) + goto call_parent; + + if (may_fail || !is_static_wep(connection)) { + /* Not static WEP or failure allowed; let superclass handle it */ + goto call_parent; + } + + /* If IP configuration times out and it's a static WEP connection, that + * usually means the WEP key is wrong. WEP's Open System auth mode has + * no provision for figuring out if the WEP key is wrong, so you just have + * to wait for DHCP to fail to figure it out. For all other Wi-Fi security + * types (open, WPA, 802.1x, etc) if the secrets/certs were wrong the + * connection would have failed before IP configuration. + * + * Activation failed, we must have bad encryption key */ + _LOGW(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) could not get IP configuration for connection '%s'.", + nm_connection_get_id(connection)); + + if (!handle_auth_or_fail(self, NULL, TRUE)) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + _LOGI(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) asking for new secrets"); + return NM_ACT_STAGE_RETURN_POSTPONE; + +call_parent: + return NM_DEVICE_CLASS(nm_device_wifi_parent_class) + ->act_stage4_ip_config_timeout(device, addr_family, out_failure_reason); +} + +static void +activation_success_handler(NMDevice *device) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + int ifindex = nm_device_get_ifindex(device); + NMActRequest * req; + + req = nm_device_get_act_request(device); + g_assert(req); + + /* Clear any critical protocol notification in the wifi stack */ + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), ifindex, FALSE); + + /* There should always be a current AP, either a fake one because we haven't + * seen a scan result for the activated AP yet, or a real one from the + * supplicant's scan list. + */ + g_warn_if_fail(priv->current_ap); + if (priv->current_ap) { + if (nm_wifi_ap_get_fake(priv->current_ap)) { + gboolean ap_changed = FALSE; + gboolean update_bssid = !nm_wifi_ap_get_address(priv->current_ap); + gboolean update_rate = !nm_wifi_ap_get_max_bitrate(priv->current_ap); + NMEtherAddr bssid; + guint32 rate; + + /* If the activation AP hasn't been seen by the supplicant in a scan + * yet, it will be "fake". This usually happens for Ad-Hoc and + * AP-mode connections. Fill in the details from the device itself + * until the supplicant sends the scan result. + */ + if (!nm_wifi_ap_get_freq(priv->current_ap)) + ap_changed |= nm_wifi_ap_set_freq( + priv->current_ap, + nm_platform_wifi_get_frequency(nm_device_get_platform(device), ifindex)); + + if ((update_bssid || update_rate) + && nm_platform_wifi_get_station(nm_device_get_platform(device), + ifindex, + update_bssid ? &bssid : NULL, + NULL, + update_rate ? &rate : NULL)) { + if (update_bssid && nm_ether_addr_is_valid(&bssid)) + ap_changed |= nm_wifi_ap_set_address_bin(priv->current_ap, &bssid); + if (update_rate) + ap_changed |= nm_wifi_ap_set_max_bitrate(priv->current_ap, rate); + } + + if (ap_changed) + _ap_dump(self, LOGL_DEBUG, priv->current_ap, "updated", 0); + } + + nm_active_connection_set_specific_object( + NM_ACTIVE_CONNECTION(req), + nm_dbus_object_get_path(NM_DBUS_OBJECT(priv->current_ap))); + } + + periodic_update(self); + + update_seen_bssids_cache(self, priv->current_ap); + + priv->scan_periodic_interval_sec = 0; + priv->scan_periodic_next_msec = 0; +} + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + gboolean clear_aps = FALSE; + + if (new_state > NM_DEVICE_STATE_ACTIVATED) + wifi_secrets_cancel(self); + + if (new_state <= NM_DEVICE_STATE_UNAVAILABLE) { + /* Clean up the supplicant interface because in these states the + * device cannot be used. + */ + supplicant_interface_release(self); + + nm_clear_g_source(&priv->periodic_update_id); + + cleanup_association_attempt(self, TRUE); + cleanup_supplicant_failures(self); + remove_all_aps(self); + } + + switch (new_state) { + case NM_DEVICE_STATE_UNMANAGED: + clear_aps = TRUE; + break; + case NM_DEVICE_STATE_UNAVAILABLE: + /* If the device is enabled and the supplicant manager is ready, + * acquire a supplicant interface and transition to DISCONNECTED because + * the device is now ready to use. + */ + if (priv->enabled && (nm_device_get_firmware_missing(device) == FALSE)) { + if (!priv->sup_iface) + supplicant_interface_acquire(self); + } + clear_aps = TRUE; + break; + case NM_DEVICE_STATE_NEED_AUTH: + if (priv->sup_iface) + nm_supplicant_interface_disconnect(priv->sup_iface); + break; + case NM_DEVICE_STATE_IP_CHECK: + /* Clear any critical protocol notification in the wifi stack */ + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ifindex(device), + FALSE); + break; + case NM_DEVICE_STATE_ACTIVATED: + activation_success_handler(device); + break; + case NM_DEVICE_STATE_FAILED: + /* Clear any critical protocol notification in the wifi stack */ + nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), + nm_device_get_ifindex(device), + FALSE); + break; + case NM_DEVICE_STATE_DISCONNECTED: + break; + default: + break; + } + + if (clear_aps) + remove_all_aps(self); + + _scan_notify_allowed(self, NM_TERNARY_DEFAULT); +} + +static gboolean +get_enabled(NMDevice *device) +{ + return NM_DEVICE_WIFI_GET_PRIVATE(device)->enabled; +} + +static void +set_enabled(NMDevice *device, gboolean enabled) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + NMDeviceState state; + + enabled = !!enabled; + + if (priv->enabled == enabled) + return; + + priv->enabled = enabled; + + _LOGD(LOGD_WIFI, "device now %s", enabled ? "enabled" : "disabled"); + + state = nm_device_get_state(NM_DEVICE(self)); + if (state < NM_DEVICE_STATE_UNAVAILABLE) { + _LOGD(LOGD_WIFI, "(%s): device blocked by UNMANAGED state", enabled ? "enable" : "disable"); + return; + } + + if (enabled) { + gboolean no_firmware = FALSE; + + if (state != NM_DEVICE_STATE_UNAVAILABLE) + _LOGW(LOGD_CORE, "not in expected unavailable state!"); + + if (!nm_device_bring_up(NM_DEVICE(self), TRUE, &no_firmware)) { + _LOGD(LOGD_WIFI, "enable blocked by failure to bring device up"); + + if (no_firmware) + nm_device_set_firmware_missing(NM_DEVICE(device), TRUE); + else { + /* The device sucks, or the kernel was lying to us about the killswitch state */ + priv->enabled = FALSE; + } + return; + } + + /* Re-initialize the supplicant interface and wait for it to be ready */ + cleanup_supplicant_failures(self); + supplicant_interface_release(self); + supplicant_interface_acquire(self); + + _LOGD(LOGD_WIFI, "enable waiting on supplicant state"); + } else { + nm_device_state_changed(NM_DEVICE(self), + NM_DEVICE_STATE_UNAVAILABLE, + NM_DEVICE_STATE_REASON_NONE); + nm_device_take_down(NM_DEVICE(self), TRUE); + } +} + +static gboolean +get_guessed_metered(NMDevice *device) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(device); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + return priv->current_ap && nm_wifi_ap_get_metered(priv->current_ap); +} + +static gboolean +can_reapply_change(NMDevice * device, + const char *setting_name, + NMSetting * s_old, + NMSetting * s_new, + GHashTable *diffs, + GError ** error) +{ + NMDeviceClass *device_class; + + /* Only handle wireless setting here, delegate other settings to parent class */ + if (nm_streq(setting_name, NM_SETTING_WIRELESS_SETTING_NAME)) { + return nm_device_hash_check_invalid_keys( + diffs, + NM_SETTING_WIRELESS_SETTING_NAME, + error, + NM_SETTING_WIRELESS_SEEN_BSSIDS, /* ignored */ + NM_SETTING_WIRELESS_MTU, /* reapplied with IP config */ + NM_SETTING_WIRELESS_WAKE_ON_WLAN); + } + + device_class = NM_DEVICE_CLASS(nm_device_wifi_parent_class); + return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error); +} + +static void +reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new) +{ + NMDeviceWifi *self = NM_DEVICE_WIFI(device); + NMDeviceState state = nm_device_get_state(device); + + NM_DEVICE_CLASS(nm_device_wifi_parent_class)->reapply_connection(device, con_old, con_new); + + _LOGD(LOGD_DEVICE, "reapplying wireless settings"); + + if (state >= NM_DEVICE_STATE_CONFIG && !wake_on_wlan_enable(self)) + _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot configure WoWLAN."); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(object); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + const char ** list; + + switch (prop_id) { + case PROP_MODE: + g_value_set_uint(value, priv->mode); + break; + case PROP_BITRATE: + g_value_set_uint(value, priv->rate); + break; + case PROP_CAPABILITIES: + g_value_set_uint(value, priv->capabilities); + break; + case PROP_ACCESS_POINTS: + list = nm_wifi_aps_get_paths(&priv->aps_lst_head, TRUE); + g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list)); + break; + case PROP_ACTIVE_ACCESS_POINT: + nm_dbus_utils_g_value_set_object_path(value, priv->current_ap); + break; + case PROP_SCANNING: + g_value_set_boolean(value, nm_device_wifi_get_scanning(self)); + break; + case PROP_LAST_SCAN: + g_value_set_int64( + value, + priv->scan_last_complete_msec > 0 + ? nm_utils_monotonic_timestamp_as_boottime(priv->scan_last_complete_msec, + NM_UTILS_NSEC_PER_MSEC) + : (gint64) -1); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceWifi * device = NM_DEVICE_WIFI(object); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(device); + + switch (prop_id) { + case PROP_CAPABILITIES: + /* construct-only */ + priv->capabilities = g_value_get_uint(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_wifi_init(NMDeviceWifi *self) +{ + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + c_list_init(&priv->aps_lst_head); + c_list_init(&priv->scanning_prohibited_lst_head); + c_list_init(&priv->scan_request_ssids_lst_head); + priv->aps_idx_by_supplicant_path = g_hash_table_new(nm_direct_hash, NULL); + + priv->scan_last_request_started_at_msec = G_MININT64; + priv->hidden_probe_scan_warn = TRUE; + priv->mode = NM_802_11_MODE_INFRA; + priv->wowlan_restore = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE; +} + +static void +constructed(GObject *object) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(object); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + G_OBJECT_CLASS(nm_device_wifi_parent_class)->constructed(object); + + if (priv->capabilities & NM_WIFI_DEVICE_CAP_AP) + _LOGI(LOGD_PLATFORM | LOGD_WIFI, "driver supports Access Point (AP) mode"); + + /* Connect to the supplicant manager */ + priv->sup_mgr = g_object_ref(nm_supplicant_manager_get()); +} + +NMDevice * +nm_device_wifi_new(const char *iface, NMDeviceWifiCapabilities capabilities) +{ + return g_object_new(NM_TYPE_DEVICE_WIFI, + NM_DEVICE_IFACE, + iface, + NM_DEVICE_TYPE_DESC, + "802.11 Wi-Fi", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_WIFI, + NM_DEVICE_LINK_TYPE, + NM_LINK_TYPE_WIFI, + NM_DEVICE_RFKILL_TYPE, + RFKILL_TYPE_WLAN, + NM_DEVICE_WIFI_CAPABILITIES, + (guint) capabilities, + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(object); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_assert(c_list_is_empty(&priv->scanning_prohibited_lst_head)); + + nm_clear_g_source(&priv->periodic_update_id); + + wifi_secrets_cancel(self); + + cleanup_association_attempt(self, TRUE); + supplicant_interface_release(self); + cleanup_supplicant_failures(self); + + g_clear_object(&priv->sup_mgr); + + remove_all_aps(self); + + if (priv->p2p_device) { + /* Destroy the P2P device. */ + g_object_remove_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device); + nm_device_wifi_p2p_remove(g_steal_pointer(&priv->p2p_device)); + } + + G_OBJECT_CLASS(nm_device_wifi_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMDeviceWifi * self = NM_DEVICE_WIFI(object); + NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self); + + nm_assert(c_list_is_empty(&priv->aps_lst_head)); + nm_assert(g_hash_table_size(priv->aps_idx_by_supplicant_path) == 0); + + g_hash_table_unref(priv->aps_idx_by_supplicant_path); + + G_OBJECT_CLASS(nm_device_wifi_parent_class)->finalize(object); +} + +static void +nm_device_wifi_class_init(NMDeviceWifiClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->set_property = set_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + dbus_object_class->interface_infos = + NM_DBUS_INTERFACE_INFOS(&nm_interface_info_device_wireless); + + device_class->connection_type_supported = NM_SETTING_WIRELESS_SETTING_NAME; + device_class->connection_type_check_compatible = NM_SETTING_WIRELESS_SETTING_NAME; + device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI); + + device_class->can_auto_connect = can_auto_connect; + device_class->get_autoconnect_allowed = get_autoconnect_allowed; + device_class->is_available = is_available; + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + device_class->get_enabled = get_enabled; + device_class->get_guessed_metered = get_guessed_metered; + device_class->set_enabled = set_enabled; + + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->get_configured_mtu = get_configured_mtu; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->act_stage4_ip_config_timeout = act_stage4_ip_config_timeout; + device_class->deactivate_async = deactivate_async; + device_class->deactivate = deactivate; + device_class->deactivate_reset_hw_addr = deactivate_reset_hw_addr; + device_class->unmanaged_on_quit = unmanaged_on_quit; + device_class->can_reapply_change = can_reapply_change; + device_class->reapply_connection = reapply_connection; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_MODE] = g_param_spec_uint(NM_DEVICE_WIFI_MODE, + "", + "", + NM_802_11_MODE_UNKNOWN, + NM_802_11_MODE_AP, + NM_802_11_MODE_INFRA, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BITRATE] = g_param_spec_uint(NM_DEVICE_WIFI_BITRATE, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ACCESS_POINTS] = + g_param_spec_boxed(NM_DEVICE_WIFI_ACCESS_POINTS, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ACTIVE_ACCESS_POINT] = + g_param_spec_string(NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_WIFI_CAPABILITIES, + "", + "", + 0, + G_MAXUINT32, + NM_WIFI_DEVICE_CAP_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SCANNING] = g_param_spec_boolean(NM_DEVICE_WIFI_SCANNING, + "", + "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LAST_SCAN] = g_param_spec_int64(NM_DEVICE_WIFI_LAST_SCAN, + "", + "", + -1, + G_MAXINT64, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[P2P_DEVICE_CREATED] = g_signal_new(NM_DEVICE_WIFI_P2P_DEVICE_CREATED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + NM_TYPE_DEVICE); +} diff --git a/src/core/devices/wifi/nm-device-wifi.h b/src/core/devices/wifi/nm-device-wifi.h new file mode 100644 index 00000000..d9e9038c --- /dev/null +++ b/src/core/devices/wifi/nm-device-wifi.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2016 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_WIFI_H__ +#define __NETWORKMANAGER_DEVICE_WIFI_H__ + +#include "devices/nm-device.h" + +#define NM_TYPE_DEVICE_WIFI (nm_device_wifi_get_type()) +#define NM_DEVICE_WIFI(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi)) +#define NM_DEVICE_WIFI_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass)) +#define NM_IS_DEVICE_WIFI(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI)) +#define NM_IS_DEVICE_WIFI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIFI)) +#define NM_DEVICE_WIFI_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass)) + +#define NM_DEVICE_WIFI_MODE "mode" +#define NM_DEVICE_WIFI_BITRATE "bitrate" +#define NM_DEVICE_WIFI_ACCESS_POINTS "access-points" +#define NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT "active-access-point" +#define NM_DEVICE_WIFI_CAPABILITIES "wireless-capabilities" +#define NM_DEVICE_WIFI_SCANNING "scanning" +#define NM_DEVICE_WIFI_LAST_SCAN "last-scan" + +#define NM_DEVICE_WIFI_P2P_DEVICE_CREATED "p2p-device-created" + +typedef struct _NMDeviceWifi NMDeviceWifi; +typedef struct _NMDeviceWifiClass NMDeviceWifiClass; + +GType nm_device_wifi_get_type(void); + +NMDevice *nm_device_wifi_new(const char *iface, NMDeviceWifiCapabilities capabilities); + +const CList *_nm_device_wifi_get_aps(NMDeviceWifi *self); + +void _nm_device_wifi_request_scan(NMDeviceWifi * self, + GVariant * options, + GDBusMethodInvocation *invocation); + +GPtrArray *nmtst_ssids_options_to_ptrarray(GVariant *value, GError **error); + +gboolean nm_device_wifi_get_scanning(NMDeviceWifi *self); + +void nm_device_wifi_scanning_prohibited_track(NMDeviceWifi *self, + gpointer tag, + gboolean temporarily_prohibited); + +#endif /* __NETWORKMANAGER_DEVICE_WIFI_H__ */ diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c new file mode 100644 index 00000000..b4b019d3 --- /dev/null +++ b/src/core/devices/wifi/nm-iwd-manager.c @@ -0,0 +1,1359 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Intel Corporation + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-iwd-manager.h" + +#include <net/if.h> + +#include "nm-core-internal.h" +#include "nm-manager.h" +#include "nm-device-iwd.h" +#include "nm-wifi-utils.h" +#include "nm-glib-aux/nm-random-utils.h" +#include "settings/nm-settings.h" +#include "nm-std-aux/nm-dbus-compat.h" + +/*****************************************************************************/ + +typedef struct { + const char * name; + NMIwdNetworkSecurity security; + char buf[0]; +} KnownNetworkId; + +typedef struct { + GDBusProxy * known_network; + NMSettingsConnection *mirror_connection; +} KnownNetworkData; + +typedef struct { + NMManager * manager; + NMSettings * settings; + GCancellable * cancellable; + gboolean running; + GDBusObjectManager *object_manager; + guint agent_id; + char * agent_path; + GHashTable * known_networks; + NMDeviceIwd * last_agent_call_device; +} NMIwdManagerPrivate; + +struct _NMIwdManager { + GObject parent; + NMIwdManagerPrivate _priv; +}; + +struct _NMIwdManagerClass { + GObjectClass parent; +}; + +G_DEFINE_TYPE(NMIwdManager, nm_iwd_manager, G_TYPE_OBJECT) + +#define NM_IWD_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMIwdManager, NM_IS_IWD_MANAGER) + +/*****************************************************************************/ + +#define _NMLOG_PREFIX_NAME "iwd-manager" +#define _NMLOG_DOMAIN LOGD_WIFI + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + if (nm_logging_enabled(level, _NMLOG_DOMAIN)) { \ + char __prefix[32]; \ + \ + if (self) \ + g_snprintf(__prefix, \ + sizeof(__prefix), \ + "%s[%p]", \ + ""_NMLOG_PREFIX_NAME \ + "", \ + (self)); \ + else \ + g_strlcpy(__prefix, _NMLOG_PREFIX_NAME, sizeof(__prefix)); \ + _nm_log((level), \ + (_NMLOG_DOMAIN), \ + 0, \ + NULL, \ + NULL, \ + "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + __prefix _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void mirror_connection_take_and_delete(NMSettingsConnection *sett_conn, + KnownNetworkData * data); + +/*****************************************************************************/ + +static const char * +get_variant_string_or_null(GVariant *v) +{ + if (!v) + return NULL; + + if (!g_variant_is_of_type(v, G_VARIANT_TYPE_STRING) + && !g_variant_is_of_type(v, G_VARIANT_TYPE_OBJECT_PATH)) + return NULL; + + return g_variant_get_string(v, NULL); +} + +static const char * +get_property_string_or_null(GDBusProxy *proxy, const char *property) +{ + gs_unref_variant GVariant *value = NULL; + + if (!proxy || !property) + return NULL; + + value = g_dbus_proxy_get_cached_property(proxy, property); + + return get_variant_string_or_null(value); +} + +static gboolean +get_property_bool(GDBusProxy *proxy, const char *property, gboolean default_val) +{ + gs_unref_variant GVariant *value = NULL; + + if (!proxy || !property) + return default_val; + + value = g_dbus_proxy_get_cached_property(proxy, property); + if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN)) + return default_val; + + return g_variant_get_boolean(value); +} + +static NMDeviceIwd * +get_device_from_network(NMIwdManager *self, GDBusProxy *network) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + const char * ifname; + const char * device_path; + NMDevice * device; + gs_unref_object GDBusInterface *device_obj = NULL; + + /* Try not to rely on the path of the Device being a prefix of the + * Network's object path. + */ + + device_path = get_property_string_or_null(network, "Device"); + if (!device_path) { + _LOGD("Device not cached for network at %s", g_dbus_proxy_get_object_path(network)); + return NULL; + } + + device_obj = g_dbus_object_manager_get_interface(priv->object_manager, + device_path, + NM_IWD_DEVICE_INTERFACE); + + ifname = get_property_string_or_null(G_DBUS_PROXY(device_obj), "Name"); + if (!ifname) { + _LOGD("Name not cached for device at %s", device_path); + return NULL; + } + + device = nm_manager_get_device(priv->manager, ifname, NM_DEVICE_TYPE_WIFI); + if (!device || !NM_IS_DEVICE_IWD(device)) { + _LOGD("NM device %s is not an IWD-managed device", ifname); + return NULL; + } + + return NM_DEVICE_IWD(device); +} + +static void +agent_dbus_method_cb(GDBusConnection * connection, + const char * sender, + const char * object_path, + const char * interface_name, + const char * method_name, + GVariant * parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + const char * network_path; + NMDeviceIwd * device; + gs_free char * name_owner = NULL; + gs_unref_object GDBusInterface *network = NULL; + + /* Be paranoid and check the sender address */ + name_owner = g_dbus_object_manager_client_get_name_owner( + G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager)); + if (!nm_streq0(name_owner, sender)) + goto return_error; + + if (!strcmp(method_name, "Cancel")) { + const char *reason = NULL; + + g_variant_get(parameters, "(&s)", &reason); + _LOGD("agent-request: Cancel reason: %s", reason); + + if (!priv->last_agent_call_device) + goto return_error; + + if (nm_device_iwd_agent_query(priv->last_agent_call_device, NULL)) { + priv->last_agent_call_device = NULL; + g_dbus_method_invocation_return_value(invocation, NULL); + return; + } + + priv->last_agent_call_device = NULL; + goto return_error; + } + + if (!strcmp(method_name, "RequestUserPassword")) + g_variant_get(parameters, "(&os)", &network_path, NULL); + else + g_variant_get(parameters, "(&o)", &network_path); + + network = g_dbus_object_manager_get_interface(priv->object_manager, + network_path, + NM_IWD_NETWORK_INTERFACE); + if (!network) { + _LOGE("agent-request: unable to find the network object"); + goto return_error; + } + + device = get_device_from_network(self, G_DBUS_PROXY(network)); + if (!device) { + _LOGD("agent-request: device not found in IWD Agent request"); + goto return_error; + } + + if (nm_device_iwd_agent_query(device, invocation)) { + priv->last_agent_call_device = device; + return; + } + + _LOGD("agent-request: device %s did not handle the IWD Agent request", + nm_device_get_iface(NM_DEVICE(device))); + +return_error: + /* IWD doesn't look at the specific error */ + g_dbus_method_invocation_return_error_literal(invocation, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "Secrets not available for this connection"); +} + +static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + "net.connman.iwd.Agent", + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO( + "RequestPassphrase", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("passphrase", "s"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO( + "RequestPrivateKeyPassphrase", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("passphrase", "s"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO( + "RequestUserNameAndPassword", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("user", "s"), + NM_DEFINE_GDBUS_ARG_INFO("password", "s"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO( + "RequestUserPassword", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), + NM_DEFINE_GDBUS_ARG_INFO("user", "s"), ), + .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("password", "s"), ), ), + NM_DEFINE_GDBUS_METHOD_INFO("Cancel", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("reason", "s"), ), ), ), ); + +static guint +iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_path, GError **error) +{ + static const GDBusInterfaceVTable vtable = { + .method_call = agent_dbus_method_cb, + }; + char path[50]; + unsigned int rnd; + guint id; + + nm_utils_random_bytes(&rnd, sizeof(rnd)); + + nm_sprintf_buf(path, "/agent/%u", rnd); + + id = + g_dbus_connection_register_object(connection, + path, + NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info), + &vtable, + user_data, + NULL, + error); + + if (id) + *agent_path = g_strdup(path); + return id; +} + +static void +register_agent(NMIwdManager *self) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GDBusInterface * agent_manager; + + agent_manager = g_dbus_object_manager_get_interface(priv->object_manager, + "/net/connman/iwd", /* IWD 1.0+ */ + NM_IWD_AGENT_MANAGER_INTERFACE); + if (!agent_manager) { + _LOGE("unable to register the IWD Agent: PSK/8021x Wi-Fi networks may not work"); + return; + } + + /* Register our agent */ + g_dbus_proxy_call(G_DBUS_PROXY(agent_manager), + "RegisterAgent", + g_variant_new("(o)", priv->agent_path), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); + + g_object_unref(agent_manager); +} + +/*****************************************************************************/ + +static KnownNetworkId * +known_network_id_new(const char *name, NMIwdNetworkSecurity security) +{ + KnownNetworkId *id; + gsize strsize = strlen(name) + 1; + + id = g_malloc(sizeof(KnownNetworkId) + strsize); + id->name = id->buf; + id->security = security; + memcpy(id->buf, name, strsize); + + return id; +} + +static guint +known_network_id_hash(KnownNetworkId *id) +{ + NMHashState h; + + nm_hash_init(&h, 1947951703u); + nm_hash_update_val(&h, id->security); + nm_hash_update_str(&h, id->name); + return nm_hash_complete(&h); +} + +static gboolean +known_network_id_equal(KnownNetworkId *a, KnownNetworkId *b) +{ + return a->security == b->security && nm_streq(a->name, b->name); +} + +static void +known_network_data_free(KnownNetworkData *network) +{ + if (!network) + return; + + g_object_unref(network->known_network); + mirror_connection_take_and_delete(network->mirror_connection, network); + g_slice_free(KnownNetworkData, network); +} + +/*****************************************************************************/ + +static void +set_device_dbus_object(NMIwdManager *self, GDBusProxy *proxy, GDBusObject *object) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + const char * ifname; + int ifindex; + NMDevice * device; + int errsv; + + ifname = get_property_string_or_null(proxy, "Name"); + if (!ifname) { + _LOGE("Name not cached for Device at %s", g_dbus_proxy_get_object_path(proxy)); + return; + } + + ifindex = if_nametoindex(ifname); + + if (!ifindex) { + errsv = errno; + _LOGE("if_nametoindex failed for Name %s for Device at %s: %i", + ifname, + g_dbus_proxy_get_object_path(proxy), + errsv); + return; + } + + device = nm_manager_get_device_by_ifindex(priv->manager, ifindex); + if (!NM_IS_DEVICE_IWD(device)) { + _LOGE("IWD device named %s is not a Wifi device", ifname); + return; + } + + nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), object); +} + +static void +known_network_update_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + gs_unref_variant GVariant *variant = NULL; + gs_free_error GError *error = NULL; + + variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (!variant) { + nm_log_warn(LOGD_WIFI, + "Updating %s on IWD known network %s failed: %s", + (const char *) user_data, + g_dbus_proxy_get_object_path(G_DBUS_PROXY(source)), + error->message); + } +} + +static void +sett_conn_changed(NMSettingsConnection *sett_conn, guint update_reason, KnownNetworkData *data) +{ + NMSettingsConnectionIntFlags flags; + NMConnection * conn = nm_settings_connection_get_connection(sett_conn); + NMSettingConnection * s_conn = nm_connection_get_setting_connection(conn); + gboolean nm_autoconnectable = nm_setting_connection_get_autoconnect(s_conn); + gboolean iwd_autoconnectable = get_property_bool(data->known_network, "AutoConnect", TRUE); + + nm_assert(sett_conn == data->mirror_connection); + + if (iwd_autoconnectable == nm_autoconnectable) + return; + + /* If this is a generated connection it may be ourselves updating it */ + flags = nm_settings_connection_get_flags(data->mirror_connection); + if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) + return; + + nm_log_dbg(LOGD_WIFI, + "Updating AutoConnect on known network at %s based on connection %s", + g_dbus_proxy_get_object_path(data->known_network), + nm_settings_connection_get_id(data->mirror_connection)); + g_dbus_proxy_call(data->known_network, + DBUS_INTERFACE_PROPERTIES ".Set", + g_variant_new("(ssv)", + NM_IWD_KNOWN_NETWORK_INTERFACE, + "AutoConnect", + g_variant_new_boolean(nm_autoconnectable)), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + known_network_update_cb, + "AutoConnect"); +} + +/* Look up an existing NMSettingsConnection for a network that has been + * preprovisioned with an IWD config file or has been connected to before, + * or create a new in-memory NMSettingsConnection object. This will let + * users control the few supported properties (mainly make it + * IWD-autoconnectable or not), remove/forget the network, or, for a + * WPA2-Enterprise type network it will inform the NM autoconnect mechanism + * and the clients that this networks needs no additional EAP configuration + * from the user. + */ +static NMSettingsConnection * +mirror_connection(NMIwdManager * self, + const KnownNetworkId *id, + gboolean create_new, + GDBusProxy * known_network) +{ + NMIwdManagerPrivate * priv = NM_IWD_MANAGER_GET_PRIVATE(self); + NMSettingsConnection *const *iter; + gs_unref_object NMConnection *connection = NULL; + NMSettingsConnection * settings_connection = NULL; + char uuid[37]; + NMSetting * setting; + gs_free_error GError *error = NULL; + gs_unref_bytes GBytes *new_ssid = NULL; + gsize ssid_len = strlen(id->name); + gboolean autoconnectable = TRUE; + gboolean hidden = FALSE; + gboolean exact_match = TRUE; + const char * key_mgmt = NULL; + + if (known_network) { + autoconnectable = get_property_bool(known_network, "AutoConnect", TRUE); + hidden = get_property_bool(known_network, "Hidden", FALSE); + } + + for (iter = nm_settings_get_connections(priv->settings, NULL); *iter; iter++) { + NMSettingsConnection *sett_conn = *iter; + NMConnection * conn = nm_settings_connection_get_connection(sett_conn); + NMIwdNetworkSecurity security; + NMSettingWireless * s_wifi; + const guint8 * ssid_bytes; + gsize ssid_len2; + + if (!nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &security)) + continue; + + if (security != id->security) + continue; + + s_wifi = nm_connection_get_setting_wireless(conn); + if (!s_wifi) + continue; + + /* The SSID must be UTF-8 if it matches since id->name is known to be + * valid UTF-8, so just memcmp them. + */ + ssid_bytes = g_bytes_get_data(nm_setting_wireless_get_ssid(s_wifi), &ssid_len2); + if (!ssid_bytes || ssid_len2 != ssid_len || memcmp(ssid_bytes, id->name, ssid_len)) + continue; + + exact_match = TRUE; + + if (known_network) { + NMSettingConnection *s_conn = nm_connection_get_setting_connection(conn); + + if (nm_setting_connection_get_autoconnect(s_conn) != autoconnectable + || nm_setting_wireless_get_hidden(s_wifi) != hidden) + exact_match = FALSE; + } + + switch (id->security) { + case NM_IWD_NETWORK_SECURITY_WEP: + case NM_IWD_NETWORK_SECURITY_OPEN: + case NM_IWD_NETWORK_SECURITY_PSK: + break; + case NM_IWD_NETWORK_SECURITY_8021X: + { + NMSetting8021x *s_8021x = nm_connection_get_setting_802_1x(conn); + gboolean external = FALSE; + guint i; + + for (i = 0; i < nm_setting_802_1x_get_num_eap_methods(s_8021x); i++) { + if (nm_streq(nm_setting_802_1x_get_eap_method(s_8021x, i), "external")) { + external = TRUE; + break; + } + } + + /* Prefer returning connections with EAP method "external" */ + if (!external) + exact_match = FALSE; + } + } + + if (!settings_connection || exact_match) + settings_connection = sett_conn; + + if (exact_match) + break; + } + + if (settings_connection && known_network && !exact_match) { + NMSettingsConnectionIntFlags flags = nm_settings_connection_get_flags(settings_connection); + + /* If we found a connection and it's generated (likely by ourselves) + * it may have been created on a request by + * nm_iwd_manager_get_ap_mirror_connection() when no Known Network + * was available so we didn't have access to its properties other + * than Name and Security. Copy their values to the generated + * NMConnection. + * TODO: avoid notify signals triggering our own watch. + * + * If on the other hand this is a user-created NMConnection we + * should try to copy the properties from it to IWD's Known Network + * using the Properties DBus interface in case the user created an + * NM connection before IWD appeared on the bus, or before IWD + * created its Known Network object. + */ + if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) { + NMConnection *tmp_conn = nm_settings_connection_get_connection(settings_connection); + NMSettingConnection *s_conn = nm_connection_get_setting_connection(tmp_conn); + NMSettingWireless * s_wifi = nm_connection_get_setting_wireless(tmp_conn); + + g_object_set(G_OBJECT(s_conn), + NM_SETTING_CONNECTION_AUTOCONNECT, + autoconnectable, + NULL); + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_HIDDEN, hidden, NULL); + } else { + KnownNetworkData data = {known_network, settings_connection}; + sett_conn_changed(settings_connection, 0, &data); + } + } + + if (settings_connection && known_network) { + /* Reset NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL now that the + * connection is going to be referenced by a known network, we don't + * want it to be deleted when activation fails anymore. + */ + nm_settings_connection_set_flags_full(settings_connection, + NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL, + 0); + } + + /* If we already have an NMSettingsConnection matching this + * KnownNetwork, whether it's saved or an in-memory connection + * potentially created by ourselves then we have nothing left to + * do here. + */ + if (settings_connection || !create_new) + return settings_connection; + + connection = nm_simple_connection_new(); + + setting = g_object_new(NM_TYPE_SETTING_CONNECTION, + NM_SETTING_CONNECTION_TYPE, + NM_SETTING_WIRELESS_SETTING_NAME, + NM_SETTING_CONNECTION_ID, + id->name, + NM_SETTING_CONNECTION_UUID, + nm_utils_uuid_generate_buf(uuid), + NM_SETTING_CONNECTION_AUTOCONNECT, + autoconnectable, + NULL); + nm_connection_add_setting(connection, setting); + + new_ssid = g_bytes_new(id->name, ssid_len); + setting = g_object_new(NM_TYPE_SETTING_WIRELESS, + NM_SETTING_WIRELESS_SSID, + new_ssid, + NM_SETTING_WIRELESS_MODE, + NM_SETTING_WIRELESS_MODE_INFRA, + NM_SETTING_WIRELESS_HIDDEN, + hidden, + NULL); + nm_connection_add_setting(connection, setting); + + switch (id->security) { + case NM_IWD_NETWORK_SECURITY_WEP: + key_mgmt = "none"; + break; + case NM_IWD_NETWORK_SECURITY_OPEN: + key_mgmt = NULL; + break; + case NM_IWD_NETWORK_SECURITY_PSK: + key_mgmt = "wpa-psk"; + break; + case NM_IWD_NETWORK_SECURITY_8021X: + key_mgmt = "wpa-eap"; + break; + } + + if (key_mgmt) { + setting = g_object_new(NM_TYPE_SETTING_WIRELESS_SECURITY, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + key_mgmt, + NULL); + nm_connection_add_setting(connection, setting); + } + + if (id->security == NM_IWD_NETWORK_SECURITY_8021X) { + /* "password" and "private-key-password" may be requested by the IWD agent + * from NM and IWD will implement a specific secret cache policy so by + * default respect that policy and don't save copies of those secrets in + * NM settings. The saved values can not be used anyway because of our + * use of NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW. + */ + setting = g_object_new(NM_TYPE_SETTING_802_1X, + NM_SETTING_802_1X_PASSWORD_FLAGS, + NM_SETTING_SECRET_FLAG_NOT_SAVED, + NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD_FLAGS, + NM_SETTING_SECRET_FLAG_NOT_SAVED, + NULL); + nm_setting_802_1x_add_eap_method(NM_SETTING_802_1X(setting), "external"); + nm_connection_add_setting(connection, setting); + } + + if (!nm_connection_normalize(connection, NULL, NULL, NULL)) + return NULL; + + if (!nm_settings_add_connection( + priv->settings, + connection, + NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY, + NM_SETTINGS_CONNECTION_ADD_REASON_NONE, + NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED + | (known_network ? 0 : NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL), + &settings_connection, + &error)) { + _LOGW("failed to add a mirror NMConnection for IWD's Known Network '%s': %s", + id->name, + error->message); + return NULL; + } + + return settings_connection; +} + +static void +mirror_connection_take_and_delete(NMSettingsConnection *sett_conn, KnownNetworkData *data) +{ + NMSettingsConnectionIntFlags flags; + + if (!sett_conn) + return; + + flags = nm_settings_connection_get_flags(sett_conn); + + /* If connection has not been saved since we created it + * in interface_added it too can be removed now. */ + if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) + nm_settings_connection_delete(sett_conn, FALSE); + + g_signal_handlers_disconnect_by_data(sett_conn, data); + g_object_unref(sett_conn); +} + +static void +interface_added(GDBusObjectManager *object_manager, + GDBusObject * object, + GDBusInterface * interface, + gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GDBusProxy * proxy; + const char * iface_name; + + if (!priv->running) + return; + + g_return_if_fail(G_IS_DBUS_PROXY(interface)); + + proxy = G_DBUS_PROXY(interface); + iface_name = g_dbus_proxy_get_interface_name(proxy); + + if (nm_streq(iface_name, NM_IWD_DEVICE_INTERFACE)) { + set_device_dbus_object(self, proxy, object); + return; + } + + if (nm_streq(iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) { + KnownNetworkId * id; + KnownNetworkId * orig_id; + KnownNetworkData * data; + NMIwdNetworkSecurity security; + const char * type_str, *name; + NMSettingsConnection *sett_conn = NULL; + + type_str = get_property_string_or_null(proxy, "Type"); + name = get_property_string_or_null(proxy, "Name"); + if (!type_str || !name) + return; + + if (nm_streq(type_str, "open")) + security = NM_IWD_NETWORK_SECURITY_OPEN; + else if (nm_streq(type_str, "psk")) + security = NM_IWD_NETWORK_SECURITY_PSK; + else if (nm_streq(type_str, "8021x")) + security = NM_IWD_NETWORK_SECURITY_8021X; + else + return; + + id = known_network_id_new(name, security); + + if (g_hash_table_lookup_extended(priv->known_networks, + id, + (void **) &orig_id, + (void **) &data)) { + _LOGW("DBus error: KnownNetwork already exists ('%s', %s)", name, type_str); + nm_g_object_ref_set(&data->known_network, proxy); + g_free(id); + id = orig_id; + } else { + data = g_slice_new0(KnownNetworkData); + data->known_network = g_object_ref(proxy); + g_hash_table_insert(priv->known_networks, id, data); + } + + sett_conn = mirror_connection(self, id, TRUE, proxy); + + if (sett_conn && sett_conn != data->mirror_connection) { + NMSettingsConnection *sett_conn_old = data->mirror_connection; + + data->mirror_connection = nm_g_object_ref(sett_conn); + mirror_connection_take_and_delete(sett_conn_old, data); + + g_signal_connect(sett_conn, + NM_SETTINGS_CONNECTION_UPDATED_INTERNAL, + G_CALLBACK(sett_conn_changed), + data); + } + + return; + } + + if (nm_streq(iface_name, NM_IWD_NETWORK_INTERFACE)) { + NMDeviceIwd *device = get_device_from_network(self, proxy); + + if (device) + nm_device_iwd_network_add_remove(device, proxy, TRUE); + + return; + } +} + +static void +interface_removed(GDBusObjectManager *object_manager, + GDBusObject * object, + GDBusInterface * interface, + gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GDBusProxy * proxy; + const char * iface_name; + + g_return_if_fail(G_IS_DBUS_PROXY(interface)); + + proxy = G_DBUS_PROXY(interface); + iface_name = g_dbus_proxy_get_interface_name(proxy); + + if (nm_streq(iface_name, NM_IWD_DEVICE_INTERFACE)) { + set_device_dbus_object(self, proxy, NULL); + return; + } + + if (nm_streq(iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) { + KnownNetworkId id; + const char * type_str; + + type_str = get_property_string_or_null(proxy, "Type"); + id.name = get_property_string_or_null(proxy, "Name"); + if (!type_str || !id.name) + return; + + if (nm_streq(type_str, "open")) + id.security = NM_IWD_NETWORK_SECURITY_OPEN; + else if (nm_streq(type_str, "psk")) + id.security = NM_IWD_NETWORK_SECURITY_PSK; + else if (nm_streq(type_str, "8021x")) + id.security = NM_IWD_NETWORK_SECURITY_8021X; + else + return; + + g_hash_table_remove(priv->known_networks, &id); + return; + } + + if (nm_streq(iface_name, NM_IWD_NETWORK_INTERFACE)) { + NMDeviceIwd *device = get_device_from_network(self, proxy); + + if (device) + nm_device_iwd_network_add_remove(device, proxy, FALSE); + + return; + } +} + +static void +object_added(GDBusObjectManager *object_manager, GDBusObject *object, gpointer user_data) +{ + GList *interfaces, *iter; + + interfaces = g_dbus_object_get_interfaces(object); + + for (iter = interfaces; iter; iter = iter->next) { + GDBusInterface *interface = G_DBUS_INTERFACE(iter->data); + + interface_added(NULL, object, interface, user_data); + } + + g_list_free_full(interfaces, g_object_unref); +} + +static void +object_removed(GDBusObjectManager *object_manager, GDBusObject *object, gpointer user_data) +{ + GList *interfaces, *iter; + + interfaces = g_dbus_object_get_interfaces(object); + + for (iter = interfaces; iter; iter = iter->next) { + GDBusInterface *interface = G_DBUS_INTERFACE(iter->data); + + interface_removed(NULL, object, interface, user_data); + } + + g_list_free_full(interfaces, g_object_unref); +} + +static void +connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate * priv = NM_IWD_MANAGER_GET_PRIVATE(self); + NMConnection * conn = nm_settings_connection_get_connection(sett_conn); + NMSettingWireless * s_wireless; + KnownNetworkData * data; + KnownNetworkId id; + char ssid_buf[33]; + const guint8 * ssid_bytes; + gsize ssid_len; + NMSettingsConnection *new_mirror_conn; + + if (!nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &id.security)) + return; + + s_wireless = nm_connection_get_setting_wireless(conn); + if (!s_wireless) + return; + + ssid_bytes = g_bytes_get_data(nm_setting_wireless_get_ssid(s_wireless), &ssid_len); + if (!ssid_bytes || ssid_len > 32 || memchr(ssid_bytes, 0, ssid_len)) + return; + + memcpy(ssid_buf, ssid_bytes, ssid_len); + ssid_buf[ssid_len] = '\0'; + id.name = ssid_buf; + data = g_hash_table_lookup(priv->known_networks, &id); + if (!data) + return; + + if (data->mirror_connection != sett_conn) + return; + + g_clear_object(&data->mirror_connection); + + /* Don't call Forget on the Known Network until there's no longer *any* + * matching NMSettingsConnection (debatable) + */ + new_mirror_conn = mirror_connection(self, &id, FALSE, NULL); + if (new_mirror_conn) { + data->mirror_connection = g_object_ref(new_mirror_conn); + return; + } + + if (!priv->running) + return; + + g_dbus_proxy_call(data->known_network, + "Forget", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +static gboolean +_om_has_name_owner(GDBusObjectManager *object_manager) +{ + gs_free char *name_owner = NULL; + + nm_assert(G_IS_DBUS_OBJECT_MANAGER_CLIENT(object_manager)); + + name_owner = + g_dbus_object_manager_client_get_name_owner(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager)); + return !!name_owner; +} + +static void +release_object_manager(NMIwdManager *self) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + + if (!priv->object_manager) + return; + + g_signal_handlers_disconnect_by_data(priv->object_manager, self); + + if (priv->agent_id) { + GDBusConnection * agent_connection; + GDBusObjectManagerClient *omc = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager); + + agent_connection = g_dbus_object_manager_client_get_connection(omc); + + /* We're is called when we're shutting down (i.e. our DBus connection + * is being closed, and IWD will detect this) or IWD was stopped so + * in either case calling UnregisterAgent will not do anything. + */ + g_dbus_connection_unregister_object(agent_connection, priv->agent_id); + priv->agent_id = 0; + nm_clear_g_free(&priv->agent_path); + } + + g_clear_object(&priv->object_manager); +} + +static void prepare_object_manager(NMIwdManager *self); + +static void +name_owner_changed(GObject *object, GParamSpec *pspec, gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GDBusObjectManager * object_manager = G_DBUS_OBJECT_MANAGER(object); + + nm_assert(object_manager == priv->object_manager); + + if (_om_has_name_owner(object_manager)) { + release_object_manager(self); + prepare_object_manager(self); + } else { + const CList *tmp_lst; + NMDevice * device; + + if (!priv->running) + return; + + priv->running = false; + + nm_manager_for_each_device (priv->manager, device, tmp_lst) { + if (NM_IS_DEVICE_IWD(device)) { + nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), NULL); + } + } + } +} + +static void +device_added(NMManager *manager, NMDevice *device, gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GList * objects, *iter; + + if (!NM_IS_DEVICE_IWD(device)) + return; + + if (!priv->running) + return; + + /* Here we handle a potential scenario where IWD's DBus objects for the + * new device popped up before the NMDevice. The + * interface_added/object_added signals have been received already and + * the handlers couldn't do much because the NMDevice wasn't there yet + * so now we go over the Network and Device interfaces again. In this + * exact order for "object path" property consistency -- see reasoning + * in object_compare_interfaces. + */ + objects = g_dbus_object_manager_get_objects(priv->object_manager); + + for (iter = objects; iter; iter = iter->next) { + GDBusObject * object = G_DBUS_OBJECT(iter->data); + gs_unref_object GDBusInterface *interface = NULL; + + interface = g_dbus_object_get_interface(object, NM_IWD_NETWORK_INTERFACE); + if (!interface) + continue; + + if (NM_DEVICE_IWD(device) == get_device_from_network(self, (GDBusProxy *) interface)) + nm_device_iwd_network_add_remove(NM_DEVICE_IWD(device), (GDBusProxy *) interface, TRUE); + } + + for (iter = objects; iter; iter = iter->next) { + GDBusObject * object = G_DBUS_OBJECT(iter->data); + gs_unref_object GDBusInterface *interface = NULL; + const char * obj_ifname; + + interface = g_dbus_object_get_interface(object, NM_IWD_DEVICE_INTERFACE); + obj_ifname = get_property_string_or_null((GDBusProxy *) interface, "Name"); + + if (!obj_ifname || strcmp(nm_device_get_iface(device), obj_ifname)) + continue; + + nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), object); + break; + } + + g_list_free_full(objects, g_object_unref); +} + +static void +device_removed(NMManager *manager, NMDevice *device, gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + + if (!NM_IS_DEVICE_IWD(device)) + return; + + if (priv->last_agent_call_device == NM_DEVICE_IWD(device)) + priv->last_agent_call_device = NULL; +} + +/* This is used to sort the list of objects returned by GetManagedObjects() + * based on the DBus interfaces available on these objects in such a way that + * the interface_added calls happen in the right order. The order is defined + * by how some DBus interfaces point to interfaces on other objects using + * DBus properties of the type "object path" ("o" signature). This creates + * "dependencies" between objects. + * + * When NM and IWD are running, the InterfacesAdded signals should come in + * an order that ensures consistency of those object paths. For example + * when a Network interface is added with a KnownNetwork property, or that + * property is assigned a new value, the KnownNetwork object pointed to by + * it will have been added in an earlier InterfacesAdded signal. Similarly + * Station.ConnectedNetwork and Station.GetOrdereNetworks() only point to + * existing Network objects. (There may be circular dependencies but during + * initialization we only need a subset of those properties that doesn't + * have this problem.) + * + * But GetManagedObjects doesn't guarantee this kind of consistency so we + * order the returned object list ourselves to simplify the job of + * interface_added(). Objects that don't have any interfaces listed in + * interface_order are moved to the end of the list. + */ +static int +object_compare_interfaces(gconstpointer a, gconstpointer b) +{ + static const char *interface_order[] = { + NM_IWD_KNOWN_NETWORK_INTERFACE, + NM_IWD_NETWORK_INTERFACE, + NM_IWD_DEVICE_INTERFACE, + }; + int rank_a = G_N_ELEMENTS(interface_order); + int rank_b = G_N_ELEMENTS(interface_order); + guint pos; + + for (pos = 0; interface_order[pos]; pos++) { + GDBusInterface *iface_a; + GDBusInterface *iface_b; + + if (rank_a == G_N_ELEMENTS(interface_order) + && (iface_a = g_dbus_object_get_interface(G_DBUS_OBJECT(a), interface_order[pos]))) { + rank_a = pos; + g_object_unref(iface_a); + } + + if (rank_b == G_N_ELEMENTS(interface_order) + && (iface_b = g_dbus_object_get_interface(G_DBUS_OBJECT(b), interface_order[pos]))) { + rank_b = pos; + g_object_unref(iface_b); + } + } + + return rank_a - rank_b; +} + +static void +got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data) +{ + NMIwdManager * self = user_data; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GError * error = NULL; + GDBusObjectManager * object_manager; + GDBusConnection * connection; + + object_manager = g_dbus_object_manager_client_new_for_bus_finish(result, &error); + if (object_manager == NULL) { + _LOGE("failed to acquire IWD Object Manager: Wi-Fi will not be available (%s)", + error->message); + g_clear_error(&error); + return; + } + + priv->object_manager = object_manager; + + g_signal_connect(priv->object_manager, + "notify::name-owner", + G_CALLBACK(name_owner_changed), + self); + + nm_assert(G_IS_DBUS_OBJECT_MANAGER_CLIENT(object_manager)); + + connection = + g_dbus_object_manager_client_get_connection(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager)); + + priv->agent_id = iwd_agent_export(connection, self, &priv->agent_path, &error); + if (!priv->agent_id) { + _LOGE("failed to export the IWD Agent: PSK/8021x Wi-Fi networks may not work: %s", + error->message); + g_clear_error(&error); + } + + if (_om_has_name_owner(object_manager)) { + GList *objects, *iter; + + priv->running = true; + + g_signal_connect(priv->object_manager, + "interface-added", + G_CALLBACK(interface_added), + self); + g_signal_connect(priv->object_manager, + "interface-removed", + G_CALLBACK(interface_removed), + self); + g_signal_connect(priv->object_manager, "object-added", G_CALLBACK(object_added), self); + g_signal_connect(priv->object_manager, "object-removed", G_CALLBACK(object_removed), self); + + g_hash_table_remove_all(priv->known_networks); + + objects = g_dbus_object_manager_get_objects(object_manager); + objects = g_list_sort(objects, object_compare_interfaces); + for (iter = objects; iter; iter = iter->next) + object_added(NULL, G_DBUS_OBJECT(iter->data), self); + + g_list_free_full(objects, g_object_unref); + + if (priv->agent_id) + register_agent(self); + } +} + +static void +prepare_object_manager(NMIwdManager *self) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + + g_dbus_object_manager_client_new_for_bus(NM_IWD_BUS_TYPE, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + NM_IWD_SERVICE, + "/", + NULL, + NULL, + NULL, + priv->cancellable, + got_object_manager, + self); +} + +gboolean +nm_iwd_manager_is_known_network(NMIwdManager *self, const char *name, NMIwdNetworkSecurity security) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + KnownNetworkId kn_id = {name, security}; + + return g_hash_table_contains(priv->known_networks, &kn_id); +} + +NMSettingsConnection * +nm_iwd_manager_get_ap_mirror_connection(NMIwdManager *self, NMWifiAP *ap) +{ + NMIwdManagerPrivate * priv = NM_IWD_MANAGER_GET_PRIVATE(self); + KnownNetworkData * data; + char name_buf[33]; + KnownNetworkId kn_id = {name_buf, NM_IWD_NETWORK_SECURITY_OPEN}; + const guint8 * ssid_bytes; + gsize ssid_len; + NM80211ApFlags flags = nm_wifi_ap_get_flags(ap); + NM80211ApSecurityFlags sec_flags = nm_wifi_ap_get_wpa_flags(ap) | nm_wifi_ap_get_rsn_flags(ap); + + ssid_bytes = g_bytes_get_data(nm_wifi_ap_get_ssid(ap), &ssid_len); + ssid_len = MIN(ssid_len, 32); + memcpy(name_buf, ssid_bytes, ssid_len); + name_buf[ssid_len] = '\0'; + + if (flags & NM_802_11_AP_FLAGS_PRIVACY) + kn_id.security = NM_IWD_NETWORK_SECURITY_WEP; + + if (sec_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK) + kn_id.security = NM_IWD_NETWORK_SECURITY_PSK; + else if (sec_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X) + kn_id.security = NM_IWD_NETWORK_SECURITY_8021X; + + /* Right now it's easier for us to do a name+security lookup than to use + * the Network.KnownNetwork property to look up by path. + */ + data = g_hash_table_lookup(priv->known_networks, &kn_id); + if (data) + return data->mirror_connection; + + /* We have no KnownNetwork for this AP, we're probably connecting to it for + * the first time. This is not a usual/supported scenario so we don't need + * to bother too much about creating a great mirror connection, we don't + * even have any more information than the Name & Type properties on the + * Network interface. This *should* never happen for an 8021x type network. + */ + return mirror_connection(self, &kn_id, TRUE, NULL); +} + +GDBusProxy * +nm_iwd_manager_get_dbus_interface(NMIwdManager *self, const char *path, const char *name) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + GDBusInterface * interface; + + if (!priv->object_manager) + return NULL; + + interface = g_dbus_object_manager_get_interface(priv->object_manager, path, name); + + return interface ? G_DBUS_PROXY(interface) : NULL; +} + +/*****************************************************************************/ + +NM_DEFINE_SINGLETON_GETTER(NMIwdManager, nm_iwd_manager_get, NM_TYPE_IWD_MANAGER); + +static void +nm_iwd_manager_init(NMIwdManager *self) +{ + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + + priv->manager = g_object_ref(NM_MANAGER_GET); + g_signal_connect(priv->manager, NM_MANAGER_DEVICE_ADDED, G_CALLBACK(device_added), self); + g_signal_connect(priv->manager, NM_MANAGER_DEVICE_REMOVED, G_CALLBACK(device_removed), self); + + priv->settings = g_object_ref(NM_SETTINGS_GET); + g_signal_connect(priv->settings, + NM_SETTINGS_SIGNAL_CONNECTION_REMOVED, + G_CALLBACK(connection_removed), + self); + + priv->cancellable = g_cancellable_new(); + + priv->known_networks = g_hash_table_new_full((GHashFunc) known_network_id_hash, + (GEqualFunc) known_network_id_equal, + g_free, + (GDestroyNotify) known_network_data_free); + + prepare_object_manager(self); +} + +static void +dispose(GObject *object) +{ + NMIwdManager * self = (NMIwdManager *) object; + NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self); + + release_object_manager(self); + + nm_clear_g_cancellable(&priv->cancellable); + + if (priv->settings) { + g_signal_handlers_disconnect_by_data(priv->settings, self); + g_clear_object(&priv->settings); + } + + /* This may trigger mirror connection removals so it happens + * after the g_signal_handlers_disconnect_by_data above. + */ + nm_clear_pointer(&priv->known_networks, g_hash_table_destroy); + + if (priv->manager) { + g_signal_handlers_disconnect_by_data(priv->manager, self); + g_clear_object(&priv->manager); + } + + priv->last_agent_call_device = NULL; + + G_OBJECT_CLASS(nm_iwd_manager_parent_class)->dispose(object); +} + +static void +nm_iwd_manager_class_init(NMIwdManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dispose; +} diff --git a/src/core/devices/wifi/nm-iwd-manager.h b/src/core/devices/wifi/nm-iwd-manager.h new file mode 100644 index 00000000..466f67c7 --- /dev/null +++ b/src/core/devices/wifi/nm-iwd-manager.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 Intel Corporation + */ + +#ifndef __NETWORKMANAGER_IWD_MANAGER_H__ +#define __NETWORKMANAGER_IWD_MANAGER_H__ + +#include "devices/nm-device.h" +#include "nm-wifi-utils.h" +#include "nm-wifi-ap.h" + +#define NM_IWD_BUS_TYPE G_BUS_TYPE_SYSTEM +#define NM_IWD_SERVICE "net.connman.iwd" + +#define NM_IWD_AGENT_MANAGER_INTERFACE "net.connman.iwd.AgentManager" +#define NM_IWD_WIPHY_INTERFACE "net.connman.iwd.Adapter" +#define NM_IWD_DEVICE_INTERFACE "net.connman.iwd.Device" +#define NM_IWD_NETWORK_INTERFACE "net.connman.iwd.Network" +#define NM_IWD_AGENT_INTERFACE "net.connman.iwd.Agent" +#define NM_IWD_WSC_INTERFACE "net.connman.iwd.WiFiSimpleConfiguration" +#define NM_IWD_KNOWN_NETWORK_INTERFACE "net.connman.iwd.KnownNetwork" +#define NM_IWD_SIGNAL_AGENT_INTERFACE "net.connman.iwd.SignalLevelAgent" +#define NM_IWD_AP_INTERFACE "net.connman.iwd.AccessPoint" +#define NM_IWD_ADHOC_INTERFACE "net.connman.iwd.AdHoc" +#define NM_IWD_STATION_INTERFACE "net.connman.iwd.Station" + +#define NM_TYPE_IWD_MANAGER (nm_iwd_manager_get_type()) +#define NM_IWD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager)) +#define NM_IWD_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_IWD_MANAGER, NMIwdManagerClass)) +#define NM_IS_IWD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_IWD_MANAGER)) +#define NM_IS_IWD_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_IWD_MANAGER)) +#define NM_IWD_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_IWD_MANAGER, NMIwdManagerClass)) + +typedef struct _NMIwdManager NMIwdManager; +typedef struct _NMIwdManagerClass NMIwdManagerClass; + +GType nm_iwd_manager_get_type(void); + +NMIwdManager *nm_iwd_manager_get(void); + +gboolean nm_iwd_manager_is_known_network(NMIwdManager * self, + const char * name, + NMIwdNetworkSecurity security); + +NMSettingsConnection *nm_iwd_manager_get_ap_mirror_connection(NMIwdManager *self, NMWifiAP *ap); + +GDBusProxy * +nm_iwd_manager_get_dbus_interface(NMIwdManager *self, const char *path, const char *name); + +#endif /* __NETWORKMANAGER_IWD_MANAGER_H__ */ diff --git a/src/core/devices/wifi/nm-wifi-ap.c b/src/core/devices/wifi/nm-wifi-ap.c new file mode 100644 index 00000000..08fa10ec --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-ap.c @@ -0,0 +1,1051 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2004 - 2017 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-wifi-ap.h" + +#include <stdlib.h> +#include <linux/if_ether.h> + +#include "NetworkManagerUtils.h" +#include "devices/nm-device.h" +#include "nm-core-internal.h" +#include "nm-dbus-manager.h" +#include "nm-glib-aux/nm-ref-string.h" +#include "nm-setting-wireless.h" +#include "nm-utils.h" +#include "nm-wifi-utils.h" +#include "platform/nm-platform.h" +#include "supplicant/nm-supplicant-interface.h" + +#define PROTO_WPA "wpa" +#define PROTO_RSN "rsn" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMWifiAP, + PROP_FLAGS, + PROP_WPA_FLAGS, + PROP_RSN_FLAGS, + PROP_SSID, + PROP_FREQUENCY, + PROP_HW_ADDRESS, + PROP_MODE, + PROP_MAX_BITRATE, + PROP_STRENGTH, + PROP_LAST_SEEN, ); + +struct _NMWifiAPPrivate { + /* Scanned or cached values */ + GBytes * ssid; + char * address; + NM80211Mode mode; + guint8 strength; + guint32 freq; /* Frequency in MHz; ie 2412 (== 2.412 GHz) */ + guint32 max_bitrate; /* Maximum bitrate of the AP in Kbit/s (ie 54000 Kb/s == 54Mbit/s) */ + + gint64 + last_seen_msec; /* Timestamp when the AP was seen lastly (in nm_utils_get_monotonic_timestamp_*() scale). + * Note that this value might be negative! */ + + NM80211ApFlags flags; /* General flags */ + NM80211ApSecurityFlags wpa_flags; /* WPA-related flags */ + NM80211ApSecurityFlags rsn_flags; /* RSN (WPA2) -related flags */ + + bool metered : 1; + + /* Non-scanned attributes */ + bool fake : 1; /* Whether or not the AP is from a scan */ + bool hotspot : 1; /* Whether the AP is a local device's hotspot network */ +}; + +typedef struct _NMWifiAPPrivate NMWifiAPPrivate; + +struct _NMWifiAPClass { + NMDBusObjectClass parent; +}; + +G_DEFINE_TYPE(NMWifiAP, nm_wifi_ap, NM_TYPE_DBUS_OBJECT) + +#define NM_WIFI_AP_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMWifiAP, NM_IS_WIFI_AP) + +/*****************************************************************************/ + +GBytes * +nm_wifi_ap_get_ssid(const NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL); + + return NM_WIFI_AP_GET_PRIVATE(ap)->ssid; +} + +gboolean +nm_wifi_ap_set_ssid(NMWifiAP *ap, GBytes *ssid) +{ + NMWifiAPPrivate *priv; + gsize l; + + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + + if (!ssid) { + /* we don't clear the SSID, once we have it. We can only update + * it by a better value. */ + return FALSE; + } + + l = g_bytes_get_size(ssid); + if (l == 0 || l > 32) + g_return_val_if_reached(FALSE); + + priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (ssid == priv->ssid) + return FALSE; + if (priv->ssid && g_bytes_equal(ssid, priv->ssid)) + return FALSE; + + g_bytes_ref(ssid); + nm_clear_pointer(&priv->ssid, g_bytes_unref); + priv->ssid = ssid; + + _notify(ap, PROP_SSID); + return TRUE; +} + +static gboolean +nm_wifi_ap_set_flags(NMWifiAP *ap, NM80211ApFlags flags) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->flags != flags) { + priv->flags = flags; + _notify(ap, PROP_FLAGS); + return TRUE; + } + return FALSE; +} + +static gboolean +nm_wifi_ap_set_wpa_flags(NMWifiAP *ap, NM80211ApSecurityFlags flags) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->wpa_flags != flags) { + priv->wpa_flags = flags; + _notify(ap, PROP_WPA_FLAGS); + return TRUE; + } + return FALSE; +} + +static gboolean +nm_wifi_ap_set_rsn_flags(NMWifiAP *ap, NM80211ApSecurityFlags flags) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->rsn_flags != flags) { + priv->rsn_flags = flags; + _notify(ap, PROP_RSN_FLAGS); + return TRUE; + } + return FALSE; +} + +const char * +nm_wifi_ap_get_address(const NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL); + + return NM_WIFI_AP_GET_PRIVATE(ap)->address; +} + +gboolean +nm_wifi_ap_set_address_bin(NMWifiAP *ap, const NMEtherAddr *addr) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + nm_assert(addr); + + if (!priv->address || !nm_utils_hwaddr_matches(addr, ETH_ALEN, priv->address, -1)) { + g_free(priv->address); + priv->address = nm_utils_hwaddr_ntoa(addr, ETH_ALEN); + _notify(ap, PROP_HW_ADDRESS); + return TRUE; + } + return FALSE; +} + +gboolean +nm_wifi_ap_set_address(NMWifiAP *ap, const char *addr) +{ + NMEtherAddr addr_buf; + + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + if (!addr || !nm_utils_hwaddr_aton(addr, &addr_buf, sizeof(addr_buf))) + g_return_val_if_reached(FALSE); + + return nm_wifi_ap_set_address_bin(ap, &addr_buf); +} + +NM80211Mode +nm_wifi_ap_get_mode(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), NM_802_11_MODE_UNKNOWN); + + return NM_WIFI_AP_GET_PRIVATE(ap)->mode; +} + +static gboolean +nm_wifi_ap_set_mode(NMWifiAP *ap, NM80211Mode mode) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + nm_assert(NM_IN_SET(mode, + NM_802_11_MODE_UNKNOWN, + NM_802_11_MODE_ADHOC, + NM_802_11_MODE_INFRA, + NM_802_11_MODE_MESH)); + + if (priv->mode != mode) { + priv->mode = mode; + _notify(ap, PROP_MODE); + return TRUE; + } + return FALSE; +} + +gboolean +nm_wifi_ap_is_hotspot(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + + return NM_WIFI_AP_GET_PRIVATE(ap)->hotspot; +} + +gint8 +nm_wifi_ap_get_strength(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0); + + return NM_WIFI_AP_GET_PRIVATE(ap)->strength; +} + +gboolean +nm_wifi_ap_set_strength(NMWifiAP *ap, gint8 strength) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->strength != strength) { + priv->strength = strength; + _notify(ap, PROP_STRENGTH); + return TRUE; + } + return FALSE; +} + +guint32 +nm_wifi_ap_get_freq(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0); + + return NM_WIFI_AP_GET_PRIVATE(ap)->freq; +} + +gboolean +nm_wifi_ap_set_freq(NMWifiAP *ap, guint32 freq) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->freq != freq) { + priv->freq = freq; + _notify(ap, PROP_FREQUENCY); + return TRUE; + } + return FALSE; +} + +guint32 +nm_wifi_ap_get_max_bitrate(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0); + g_return_val_if_fail(nm_dbus_object_is_exported(NM_DBUS_OBJECT(ap)), 0); + + return NM_WIFI_AP_GET_PRIVATE(ap)->max_bitrate; +} + +gboolean +nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate) +{ + NMWifiAPPrivate *priv; + + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + + priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->max_bitrate != bitrate) { + priv->max_bitrate = bitrate; + _notify(ap, PROP_MAX_BITRATE); + return TRUE; + } + return FALSE; +} + +gboolean +nm_wifi_ap_get_fake(const NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + + return NM_WIFI_AP_GET_PRIVATE(ap)->fake; +} + +gboolean +nm_wifi_ap_set_fake(NMWifiAP *ap, gboolean fake) +{ + NMWifiAPPrivate *priv; + + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + + priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->fake != !!fake) { + priv->fake = fake; + return TRUE; + } + return FALSE; +} + +NM80211ApFlags +nm_wifi_ap_get_flags(const NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), NM_802_11_AP_FLAGS_NONE); + + return NM_WIFI_AP_GET_PRIVATE(ap)->flags; +} + +static gboolean +nm_wifi_ap_set_last_seen(NMWifiAP *ap, gint32 last_seen_msec) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap); + + if (priv->last_seen_msec != last_seen_msec) { + priv->last_seen_msec = last_seen_msec; + _notify(ap, PROP_LAST_SEEN); + return TRUE; + } + return FALSE; +} + +gboolean +nm_wifi_ap_get_metered(const NMWifiAP *self) +{ + return NM_WIFI_AP_GET_PRIVATE(self)->metered; +} + +NM80211ApSecurityFlags +nm_wifi_ap_get_wpa_flags(const NMWifiAP *self) +{ + return NM_WIFI_AP_GET_PRIVATE(self)->wpa_flags; +} + +NM80211ApSecurityFlags +nm_wifi_ap_get_rsn_flags(const NMWifiAP *self) +{ + return NM_WIFI_AP_GET_PRIVATE(self)->rsn_flags; +} + +/*****************************************************************************/ + +gboolean +nm_wifi_ap_update_from_properties(NMWifiAP *ap, const NMSupplicantBssInfo *bss_info) +{ + NMWifiAPPrivate *priv; + gboolean changed = FALSE; + + g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE); + g_return_val_if_fail(bss_info, FALSE); + nm_assert(NM_IS_REF_STRING(bss_info->bss_path)); + + priv = NM_WIFI_AP_GET_PRIVATE(ap); + + nm_assert(!ap->_supplicant_path || ap->_supplicant_path == bss_info->bss_path); + + g_object_freeze_notify(G_OBJECT(ap)); + + if (!ap->_supplicant_path) { + ap->_supplicant_path = nm_ref_string_ref(bss_info->bss_path); + changed = TRUE; + } + + changed |= nm_wifi_ap_set_flags(ap, bss_info->ap_flags); + changed |= nm_wifi_ap_set_mode(ap, bss_info->mode); + changed |= nm_wifi_ap_set_strength(ap, bss_info->signal_percent); + changed |= nm_wifi_ap_set_freq(ap, bss_info->frequency); + changed |= nm_wifi_ap_set_ssid(ap, bss_info->ssid); + + if (bss_info->bssid_valid) + changed |= nm_wifi_ap_set_address_bin(ap, &bss_info->bssid); + else { + /* we don't actually clear the value. */ + } + + changed |= nm_wifi_ap_set_max_bitrate(ap, bss_info->max_rate); + + if (priv->metered != bss_info->metered) { + priv->metered = bss_info->metered; + changed = TRUE; + } + + changed |= nm_wifi_ap_set_wpa_flags(ap, bss_info->wpa_flags); + changed |= nm_wifi_ap_set_rsn_flags(ap, bss_info->rsn_flags); + + changed |= nm_wifi_ap_set_last_seen(ap, bss_info->last_seen_msec); + + changed |= nm_wifi_ap_set_fake(ap, FALSE); + + g_object_thaw_notify(G_OBJECT(ap)); + + return changed; +} + +static gboolean +has_proto(NMSettingWirelessSecurity *sec, const char *proto) +{ + guint32 num_protos = nm_setting_wireless_security_get_num_protos(sec); + guint32 i; + + if (num_protos == 0) + return TRUE; /* interpret no protos as "all" */ + + for (i = 0; i < num_protos; i++) { + if (!strcmp(nm_setting_wireless_security_get_proto(sec, i), proto)) + return TRUE; + } + return FALSE; +} + +static void +add_pair_ciphers(NMWifiAP *ap, NMSettingWirelessSecurity *sec) +{ + NMWifiAPPrivate * priv = NM_WIFI_AP_GET_PRIVATE(ap); + guint32 num = nm_setting_wireless_security_get_num_pairwise(sec); + NM80211ApSecurityFlags flags = NM_802_11_AP_SEC_NONE; + guint32 i; + + /* If no ciphers are specified, that means "all" WPA ciphers */ + if (num == 0) { + flags |= NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP; + } else { + for (i = 0; i < num; i++) { + const char *cipher = nm_setting_wireless_security_get_pairwise(sec, i); + + if (!strcmp(cipher, "tkip")) + flags |= NM_802_11_AP_SEC_PAIR_TKIP; + else if (!strcmp(cipher, "ccmp")) + flags |= NM_802_11_AP_SEC_PAIR_CCMP; + } + } + + if (has_proto(sec, PROTO_WPA)) + nm_wifi_ap_set_wpa_flags(ap, priv->wpa_flags | flags); + if (has_proto(sec, PROTO_RSN)) + nm_wifi_ap_set_rsn_flags(ap, priv->rsn_flags | flags); +} + +static void +add_group_ciphers(NMWifiAP *ap, NMSettingWirelessSecurity *sec) +{ + NMWifiAPPrivate * priv = NM_WIFI_AP_GET_PRIVATE(ap); + guint32 num = nm_setting_wireless_security_get_num_groups(sec); + NM80211ApSecurityFlags flags = NM_802_11_AP_SEC_NONE; + guint32 i; + + /* If no ciphers are specified, that means "all" WPA ciphers */ + if (num == 0) { + flags |= NM_802_11_AP_SEC_GROUP_TKIP | NM_802_11_AP_SEC_GROUP_CCMP; + } else { + for (i = 0; i < num; i++) { + const char *cipher = nm_setting_wireless_security_get_group(sec, i); + + if (!strcmp(cipher, "wep40")) + flags |= NM_802_11_AP_SEC_GROUP_WEP40; + else if (!strcmp(cipher, "wep104")) + flags |= NM_802_11_AP_SEC_GROUP_WEP104; + else if (!strcmp(cipher, "tkip")) + flags |= NM_802_11_AP_SEC_GROUP_TKIP; + else if (!strcmp(cipher, "ccmp")) + flags |= NM_802_11_AP_SEC_GROUP_CCMP; + } + } + + if (has_proto(sec, PROTO_WPA)) + nm_wifi_ap_set_wpa_flags(ap, priv->wpa_flags | flags); + if (has_proto(sec, PROTO_RSN)) + nm_wifi_ap_set_rsn_flags(ap, priv->rsn_flags | flags); +} + +const char * +nm_wifi_ap_to_string(const NMWifiAP *self, char *str_buf, gulong buf_len, gint64 now_msec) +{ + const NMWifiAPPrivate *priv; + const char * supplicant_id = "-"; + const char * export_path; + guint32 chan; + gs_free char * ssid_to_free = NULL; + char str_buf_ts[100]; + + g_return_val_if_fail(NM_IS_WIFI_AP(self), NULL); + + priv = NM_WIFI_AP_GET_PRIVATE(self); + + chan = nm_utils_wifi_freq_to_channel(priv->freq); + if (self->_supplicant_path) + supplicant_id = strrchr(self->_supplicant_path->str, '/') ?: supplicant_id; + + export_path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self)); + if (export_path) + export_path = strrchr(export_path, '/') ?: export_path; + else + export_path = "/"; + + nm_utils_get_monotonic_timestamp_msec_cached(&now_msec); + + g_snprintf(str_buf, + buf_len, + "%17s %-35s [ %c %3u %3u%% %c%c %c%c W:%04X R:%04X ] %s sup:%s [nm:%s]", + priv->address ?: "(none)", + (ssid_to_free = _nm_utils_ssid_to_string(priv->ssid)), + (priv->mode == NM_802_11_MODE_ADHOC + ? '*' + : (priv->hotspot + ? '#' + : (priv->fake ? 'f' : (priv->mode == NM_802_11_MODE_MESH ? 'm' : 'a')))), + chan, + priv->strength, + priv->flags & NM_802_11_AP_FLAGS_PRIVACY ? 'P' : '_', + priv->metered ? 'M' : '_', + priv->flags & NM_802_11_AP_FLAGS_WPS ? 'W' : '_', + priv->flags & NM_802_11_AP_FLAGS_WPS_PIN + ? 'p' + : (priv->flags & NM_802_11_AP_FLAGS_WPS_PBC ? '#' : '_'), + priv->wpa_flags & 0xFFFF, + priv->rsn_flags & 0xFFFF, + priv->last_seen_msec != G_MININT64 + ? nm_sprintf_buf(str_buf_ts, + "%3u.%03us", + (guint)((now_msec - priv->last_seen_msec) / 1000), + (guint)((now_msec - priv->last_seen_msec) % 1000)) + : " ", + supplicant_id, + export_path); + return str_buf; +} + +static guint +freq_to_band(guint32 freq) +{ + if (freq >= 4915 && freq <= 5825) + return 5; + else if (freq >= 2412 && freq <= 2484) + return 2; + return 0; +} + +gboolean +nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection) +{ + NMWifiAPPrivate * priv; + NMSettingWireless * s_wireless; + NMSettingWirelessSecurity *s_wireless_sec; + GBytes * ssid; + const char * mode; + const char * band; + const char * bssid; + guint32 channel; + + g_return_val_if_fail(NM_IS_WIFI_AP(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE); + + priv = NM_WIFI_AP_GET_PRIVATE(self); + + s_wireless = nm_connection_get_setting_wireless(connection); + if (s_wireless == NULL) + return FALSE; + + ssid = nm_setting_wireless_get_ssid(s_wireless); + if (ssid != priv->ssid) { + if (!ssid || !priv->ssid) + return FALSE; + if (!g_bytes_equal(ssid, priv->ssid)) + return FALSE; + } + + bssid = nm_setting_wireless_get_bssid(s_wireless); + if (bssid && (!priv->address || !nm_utils_hwaddr_matches(bssid, -1, priv->address, -1))) + return FALSE; + + mode = nm_setting_wireless_get_mode(s_wireless); + if (mode) { + if (!strcmp(mode, "infrastructure") && (priv->mode != NM_802_11_MODE_INFRA)) + return FALSE; + if (!strcmp(mode, "adhoc") && (priv->mode != NM_802_11_MODE_ADHOC)) + return FALSE; + if (!strcmp(mode, "ap") && (priv->mode != NM_802_11_MODE_INFRA || priv->hotspot != TRUE)) + return FALSE; + if (!strcmp(mode, "mesh") && (priv->mode != NM_802_11_MODE_MESH)) + return FALSE; + } + + band = nm_setting_wireless_get_band(s_wireless); + if (band) { + guint ap_band = freq_to_band(priv->freq); + + if (!strcmp(band, "a") && ap_band != 5) + return FALSE; + else if (!strcmp(band, "bg") && ap_band != 2) + return FALSE; + } + + channel = nm_setting_wireless_get_channel(s_wireless); + if (channel) { + guint32 ap_chan = nm_utils_wifi_freq_to_channel(priv->freq); + + if (channel != ap_chan) + return FALSE; + } + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + + return nm_setting_wireless_ap_security_compatible(s_wireless, + s_wireless_sec, + priv->flags, + priv->wpa_flags, + priv->rsn_flags, + priv->mode); +} + +gboolean +nm_wifi_ap_complete_connection(NMWifiAP * self, + NMConnection *connection, + gboolean lock_bssid, + GError ** error) +{ + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self); + + g_return_val_if_fail(connection != NULL, FALSE); + + return nm_wifi_utils_complete_connection(priv->ssid, + priv->address, + priv->mode, + priv->freq, + priv->flags, + priv->wpa_flags, + priv->rsn_flags, + connection, + lock_bssid, + error); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMWifiAP * self = NM_WIFI_AP(object); + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_FLAGS: + g_value_set_uint(value, priv->flags); + break; + case PROP_WPA_FLAGS: + g_value_set_uint(value, priv->wpa_flags); + break; + case PROP_RSN_FLAGS: + g_value_set_uint(value, priv->rsn_flags); + break; + case PROP_SSID: + g_value_take_variant(value, nm_utils_gbytes_to_variant_ay(priv->ssid)); + break; + case PROP_FREQUENCY: + g_value_set_uint(value, priv->freq); + break; + case PROP_HW_ADDRESS: + g_value_set_string(value, priv->address); + break; + case PROP_MODE: + g_value_set_uint(value, priv->mode); + break; + case PROP_MAX_BITRATE: + g_value_set_uint(value, priv->max_bitrate); + break; + case PROP_STRENGTH: + g_value_set_uchar(value, priv->strength); + break; + case PROP_LAST_SEEN: + g_value_set_int(value, + priv->last_seen_msec != G_MININT64 ? (int) NM_MAX( + nm_utils_monotonic_timestamp_as_boottime(priv->last_seen_msec, + NM_UTILS_NSEC_PER_MSEC) + / 1000, + 1) + : -1); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_wifi_ap_init(NMWifiAP *self) +{ + NMWifiAPPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_WIFI_AP, NMWifiAPPrivate); + + self->_priv = priv; + + c_list_init(&self->aps_lst); + + priv->mode = NM_802_11_MODE_INFRA; + priv->flags = NM_802_11_AP_FLAGS_NONE; + priv->wpa_flags = NM_802_11_AP_SEC_NONE; + priv->rsn_flags = NM_802_11_AP_SEC_NONE; + priv->last_seen_msec = G_MININT64; +} + +NMWifiAP * +nm_wifi_ap_new_from_properties(const NMSupplicantBssInfo *bss_info) +{ + NMWifiAP *ap; + + ap = g_object_new(NM_TYPE_WIFI_AP, NULL); + nm_wifi_ap_update_from_properties(ap, bss_info); + return ap; +} + +NMWifiAP * +nm_wifi_ap_new_fake_from_connection(NMConnection *connection) +{ + NMWifiAP * ap; + NMWifiAPPrivate * priv; + NMSettingWireless * s_wireless; + NMSettingWirelessSecurity *s_wireless_sec; + const char * mode, *band, *key_mgmt; + guint32 channel; + NM80211ApSecurityFlags flags; + gboolean psk = FALSE, eap = FALSE, adhoc = FALSE; + + g_return_val_if_fail(connection != NULL, NULL); + + s_wireless = nm_connection_get_setting_wireless(connection); + g_return_val_if_fail(s_wireless != NULL, NULL); + + ap = g_object_new(NM_TYPE_WIFI_AP, NULL); + priv = NM_WIFI_AP_GET_PRIVATE(ap); + priv->fake = TRUE; + + nm_wifi_ap_set_ssid(ap, nm_setting_wireless_get_ssid(s_wireless)); + + // FIXME: bssid too? + + mode = nm_setting_wireless_get_mode(s_wireless); + if (mode) { + if (!strcmp(mode, "infrastructure")) + nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA); + else if (!strcmp(mode, "adhoc")) { + nm_wifi_ap_set_mode(ap, NM_802_11_MODE_ADHOC); + adhoc = TRUE; + } else if (!strcmp(mode, "mesh")) + nm_wifi_ap_set_mode(ap, NM_802_11_MODE_MESH); + else if (!strcmp(mode, "ap")) { + nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA); + NM_WIFI_AP_GET_PRIVATE(ap)->hotspot = TRUE; + } else + goto error; + } else { + nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA); + } + + band = nm_setting_wireless_get_band(s_wireless); + channel = nm_setting_wireless_get_channel(s_wireless); + + if (band && channel) { + guint32 freq = nm_utils_wifi_channel_to_freq(channel, band); + + if (freq == 0) + goto error; + + nm_wifi_ap_set_freq(ap, freq); + } + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + /* Assume presence of a security setting means the AP is encrypted */ + if (!s_wireless_sec) + goto done; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wireless_sec); + + /* Everything below here uses encryption */ + nm_wifi_ap_set_flags(ap, priv->flags | NM_802_11_AP_FLAGS_PRIVACY); + + /* Static & Dynamic WEP */ + if (!strcmp(key_mgmt, "none") || !strcmp(key_mgmt, "ieee8021x")) + goto done; + + psk = nm_streq(key_mgmt, "wpa-psk"); + eap = nm_streq(key_mgmt, "wpa-eap") || nm_streq(key_mgmt, "wpa-eap-suite-b-192"); + if (!adhoc && (psk || eap)) { + if (has_proto(s_wireless_sec, PROTO_WPA)) { + flags = priv->wpa_flags + | (eap ? NM_802_11_AP_SEC_KEY_MGMT_802_1X : NM_802_11_AP_SEC_KEY_MGMT_PSK); + nm_wifi_ap_set_wpa_flags(ap, flags); + } + if (has_proto(s_wireless_sec, PROTO_RSN)) { + flags = priv->rsn_flags + | (eap ? NM_802_11_AP_SEC_KEY_MGMT_802_1X : NM_802_11_AP_SEC_KEY_MGMT_PSK); + nm_wifi_ap_set_rsn_flags(ap, flags); + } + + add_pair_ciphers(ap, s_wireless_sec); + add_group_ciphers(ap, s_wireless_sec); + } else if (adhoc && psk) { + /* Ad-Hoc has special requirements: proto=RSN, pairwise=CCMP and + * group=CCMP. + */ + flags = priv->wpa_flags | NM_802_11_AP_SEC_KEY_MGMT_PSK; + + /* Clear ciphers; only CCMP is supported */ + flags &= ~(NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 + | NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_WEP40 + | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_GROUP_TKIP); + + flags |= NM_802_11_AP_SEC_PAIR_CCMP; + flags |= NM_802_11_AP_SEC_GROUP_CCMP; + nm_wifi_ap_set_rsn_flags(ap, flags); + + /* Don't use Ad-Hoc WPA (WPA-none) anymore */ + nm_wifi_ap_set_wpa_flags(ap, NM_802_11_AP_SEC_NONE); + } +done: + return ap; + +error: + g_object_unref(ap); + return NULL; +} + +static void +finalize(GObject *object) +{ + NMWifiAP * self = NM_WIFI_AP(object); + NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self); + + nm_assert(!self->wifi_device); + nm_assert(c_list_is_empty(&self->aps_lst)); + + nm_ref_string_unref(self->_supplicant_path); + if (priv->ssid) + g_bytes_unref(priv->ssid); + g_free(priv->address); + + G_OBJECT_CLASS(nm_wifi_ap_parent_class)->finalize(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_access_point = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_ACCESS_POINT, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Flags", "u", NM_WIFI_AP_FLAGS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("WpaFlags", "u", NM_WIFI_AP_WPA_FLAGS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("RsnFlags", "u", NM_WIFI_AP_RSN_FLAGS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ssid", "ay", NM_WIFI_AP_SSID), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Frequency", + "u", + NM_WIFI_AP_FREQUENCY), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_WIFI_AP_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_WIFI_AP_MODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("MaxBitrate", + "u", + NM_WIFI_AP_MAX_BITRATE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Strength", "y", NM_WIFI_AP_STRENGTH), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("LastSeen", + "i", + NM_WIFI_AP_LAST_SEEN), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_wifi_ap_class_init(NMWifiAPClass *ap_class) +{ +#define ALL_SEC_FLAGS \ + (NM_802_11_AP_SEC_NONE | NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 \ + | NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_WEP40 \ + | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_GROUP_TKIP | NM_802_11_AP_SEC_GROUP_CCMP \ + | NM_802_11_AP_SEC_KEY_MGMT_PSK | NM_802_11_AP_SEC_KEY_MGMT_802_1X \ + | NM_802_11_AP_SEC_KEY_MGMT_SAE | NM_802_11_AP_SEC_KEY_MGMT_OWE \ + | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM | NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192) + + GObjectClass * object_class = G_OBJECT_CLASS(ap_class); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(ap_class); + + g_type_class_add_private(object_class, sizeof(NMWifiAPPrivate)); + + dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH_ACCESS_POINT); + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_access_point); + + object_class->get_property = get_property; + object_class->finalize = finalize; + + obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_WIFI_AP_FLAGS, + "", + "", + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_FLAGS_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_WPA_FLAGS] = g_param_spec_uint(NM_WIFI_AP_WPA_FLAGS, + "", + "", + NM_802_11_AP_SEC_NONE, + ALL_SEC_FLAGS, + NM_802_11_AP_SEC_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_RSN_FLAGS] = g_param_spec_uint(NM_WIFI_AP_RSN_FLAGS, + "", + "", + NM_802_11_AP_SEC_NONE, + ALL_SEC_FLAGS, + NM_802_11_AP_SEC_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SSID] = g_param_spec_variant(NM_WIFI_AP_SSID, + "", + "", + G_VARIANT_TYPE("ay"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_FREQUENCY] = g_param_spec_uint(NM_WIFI_AP_FREQUENCY, + "", + "", + 0, + 10000, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_HW_ADDRESS] = + g_param_spec_string(NM_WIFI_AP_HW_ADDRESS, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MODE] = g_param_spec_uint(NM_WIFI_AP_MODE, + "", + "", + NM_802_11_MODE_ADHOC, + NM_802_11_MODE_INFRA, + NM_802_11_MODE_INFRA, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MAX_BITRATE] = g_param_spec_uint(NM_WIFI_AP_MAX_BITRATE, + "", + "", + 0, + G_MAXUINT16, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_STRENGTH] = g_param_spec_uchar(NM_WIFI_AP_STRENGTH, + "", + "", + 0, + G_MAXINT8, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LAST_SEEN] = g_param_spec_int(NM_WIFI_AP_LAST_SEEN, + "", + "", + -1, + G_MAXINT, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} + +/*****************************************************************************/ + +const char ** +nm_wifi_aps_get_paths(const CList *aps_lst_head, gboolean include_without_ssid) +{ + NMWifiAP * ap; + gsize i, n; + const char **list; + const char * path; + + n = c_list_length(aps_lst_head); + list = g_new(const char *, n + 1); + + i = 0; + if (n > 0) { + c_list_for_each_entry (ap, aps_lst_head, aps_lst) { + nm_assert(i < n); + if (!include_without_ssid && !nm_wifi_ap_get_ssid(ap)) + continue; + + path = nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)); + nm_assert(path); + + list[i++] = path; + } + nm_assert(i <= n); + nm_assert(!include_without_ssid || i == n); + } + list[i] = NULL; + return list; +} + +NMWifiAP * +nm_wifi_aps_find_first_compatible(const CList *aps_lst_head, NMConnection *connection) +{ + NMWifiAP *ap; + + g_return_val_if_fail(connection, NULL); + + c_list_for_each_entry (ap, aps_lst_head, aps_lst) { + if (nm_wifi_ap_check_compatible(ap, connection)) + return ap; + } + return NULL; +} + +/*****************************************************************************/ + +NMWifiAP * +nm_wifi_ap_lookup_for_device(NMDevice *device, const char *exported_path) +{ + NMWifiAP *ap; + + g_return_val_if_fail(NM_IS_DEVICE(device), NULL); + + ap = nm_dbus_manager_lookup_object(nm_dbus_object_get_manager(NM_DBUS_OBJECT(device)), + exported_path); + if (!ap || !NM_IS_WIFI_AP(ap) || ap->wifi_device != device) + return NULL; + + return ap; +} diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h new file mode 100644 index 00000000..bdd72415 --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-ap.h @@ -0,0 +1,96 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2004 - 2017 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#ifndef __NM_WIFI_AP_H__ +#define __NM_WIFI_AP_H__ + +#include "nm-dbus-object.h" +#include "nm-dbus-interface.h" +#include "nm-connection.h" + +#define NM_TYPE_WIFI_AP (nm_wifi_ap_get_type()) +#define NM_WIFI_AP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP)) +#define NM_WIFI_AP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_AP, NMWifiAPClass)) +#define NM_IS_WIFI_AP(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_AP)) +#define NM_IS_WIFI_AP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_AP)) +#define NM_WIFI_AP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_AP, NMWifiAPClass)) + +#define NM_WIFI_AP_FLAGS "flags" +#define NM_WIFI_AP_WPA_FLAGS "wpa-flags" +#define NM_WIFI_AP_RSN_FLAGS "rsn-flags" +#define NM_WIFI_AP_SSID "ssid" +#define NM_WIFI_AP_FREQUENCY "frequency" +#define NM_WIFI_AP_HW_ADDRESS "hw-address" +#define NM_WIFI_AP_MODE "mode" +#define NM_WIFI_AP_MAX_BITRATE "max-bitrate" +#define NM_WIFI_AP_STRENGTH "strength" +#define NM_WIFI_AP_LAST_SEEN "last-seen" + +typedef struct { + NMDBusObject parent; + NMDevice * wifi_device; + CList aps_lst; + NMRefString * _supplicant_path; + struct _NMWifiAPPrivate *_priv; +} NMWifiAP; + +struct _NMSupplicantBssInfo; + +typedef struct _NMWifiAPClass NMWifiAPClass; + +GType nm_wifi_ap_get_type(void); + +NMWifiAP *nm_wifi_ap_new_from_properties(const struct _NMSupplicantBssInfo *bss_info); +NMWifiAP *nm_wifi_ap_new_fake_from_connection(NMConnection *connection); + +gboolean nm_wifi_ap_update_from_properties(NMWifiAP * ap, + const struct _NMSupplicantBssInfo *bss_info); + +gboolean nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection); + +gboolean nm_wifi_ap_complete_connection(NMWifiAP * self, + NMConnection *connection, + gboolean lock_bssid, + GError ** error); + +static inline NMRefString * +nm_wifi_ap_get_supplicant_path(NMWifiAP *ap) +{ + g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL); + + return ap->_supplicant_path; +} + +GBytes * nm_wifi_ap_get_ssid(const NMWifiAP *ap); +gboolean nm_wifi_ap_set_ssid(NMWifiAP *ap, GBytes *ssid); +const char * nm_wifi_ap_get_address(const NMWifiAP *ap); +gboolean nm_wifi_ap_set_address(NMWifiAP *ap, const char *addr); +gboolean nm_wifi_ap_set_address_bin(NMWifiAP *ap, const NMEtherAddr *addr); +NM80211Mode nm_wifi_ap_get_mode(NMWifiAP *ap); +gboolean nm_wifi_ap_is_hotspot(NMWifiAP *ap); +gint8 nm_wifi_ap_get_strength(NMWifiAP *ap); +gboolean nm_wifi_ap_set_strength(NMWifiAP *ap, gint8 strength); +guint32 nm_wifi_ap_get_freq(NMWifiAP *ap); +gboolean nm_wifi_ap_set_freq(NMWifiAP *ap, guint32 freq); +guint32 nm_wifi_ap_get_max_bitrate(NMWifiAP *ap); +gboolean nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate); +gboolean nm_wifi_ap_get_fake(const NMWifiAP *ap); +gboolean nm_wifi_ap_set_fake(NMWifiAP *ap, gboolean fake); +NM80211ApFlags nm_wifi_ap_get_flags(const NMWifiAP *self); +gboolean nm_wifi_ap_get_metered(const NMWifiAP *self); +NM80211ApSecurityFlags nm_wifi_ap_get_wpa_flags(const NMWifiAP *self); +NM80211ApSecurityFlags nm_wifi_ap_get_rsn_flags(const NMWifiAP *self); + +const char * +nm_wifi_ap_to_string(const NMWifiAP *self, char *str_buf, gulong buf_len, gint64 now_msec); + +const char **nm_wifi_aps_get_paths(const CList *aps_lst_head, gboolean include_without_ssid); + +NMWifiAP *nm_wifi_aps_find_first_compatible(const CList *aps_lst_head, NMConnection *connection); + +NMWifiAP *nm_wifi_ap_lookup_for_device(NMDevice *device, const char *exported_path); + +#endif /* __NM_WIFI_AP_H__ */ diff --git a/src/core/devices/wifi/nm-wifi-common.c b/src/core/devices/wifi/nm-wifi-common.c new file mode 100644 index 00000000..c715c07c --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-common.c @@ -0,0 +1,176 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-wifi-common.h" + +#include "devices/nm-device.h" +#include "nm-wifi-ap.h" +#include "nm-device-wifi.h" +#include "nm-dbus-manager.h" + +#if WITH_IWD + #include "nm-device-iwd.h" +#endif + +/*****************************************************************************/ + +void +nm_device_wifi_emit_signal_access_point(NMDevice *device, + NMWifiAP *ap, + gboolean is_added /* or else is_removed */) +{ + nm_dbus_object_emit_signal(NM_DBUS_OBJECT(device), + &nm_interface_info_device_wireless, + is_added ? &nm_signal_info_wireless_access_point_added + : &nm_signal_info_wireless_access_point_removed, + "(o)", + nm_dbus_object_get_path(NM_DBUS_OBJECT(ap))); +} + +/*****************************************************************************/ + +static const CList * +_dispatch_get_aps(NMDevice *device) +{ +#if WITH_IWD + if (NM_IS_DEVICE_IWD(device)) + return _nm_device_iwd_get_aps(NM_DEVICE_IWD(device)); +#endif + return _nm_device_wifi_get_aps(NM_DEVICE_WIFI(device)); +} + +static void +_dispatch_request_scan(NMDevice *device, GVariant *options, GDBusMethodInvocation *invocation) +{ +#if WITH_IWD + if (NM_IS_DEVICE_IWD(device)) { + _nm_device_iwd_request_scan(NM_DEVICE_IWD(device), options, invocation); + return; + } +#endif + _nm_device_wifi_request_scan(NM_DEVICE_WIFI(device), options, invocation); +} + +static void +impl_device_wifi_get_access_points(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + gs_free const char **list = NULL; + GVariant * v; + const CList * all_aps; + + /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */ + + all_aps = _dispatch_get_aps(NM_DEVICE(obj)); + list = nm_wifi_aps_get_paths(all_aps, FALSE); + v = g_variant_new_objv(list, -1); + g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&v, 1)); +} + +static void +impl_device_wifi_get_all_access_points(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + gs_free const char **list = NULL; + GVariant * v; + const CList * all_aps; + + /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */ + + all_aps = _dispatch_get_aps(NM_DEVICE(obj)); + list = nm_wifi_aps_get_paths(all_aps, TRUE); + v = g_variant_new_objv(list, -1); + g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&v, 1)); +} + +static void +impl_device_wifi_request_scan(NMDBusObject * obj, + const NMDBusInterfaceInfoExtended *interface_info, + const NMDBusMethodInfoExtended * method_info, + GDBusConnection * connection, + const char * sender, + GDBusMethodInvocation * invocation, + GVariant * parameters) +{ + gs_unref_variant GVariant *options = NULL; + + /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */ + + g_variant_get(parameters, "(@a{sv})", &options); + + _dispatch_request_scan(NM_DEVICE(obj), options, invocation); +} + +const GDBusSignalInfo nm_signal_info_wireless_access_point_added = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT( + "AccessPointAdded", + .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("access_point", "o"), ), ); + +const GDBusSignalInfo nm_signal_info_wireless_access_point_removed = + NM_DEFINE_GDBUS_SIGNAL_INFO_INIT( + "AccessPointRemoved", + .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("access_point", "o"), ), ); + +const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_WIRELESS, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "GetAccessPoints", + .out_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ), ), + .handle = impl_device_wifi_get_access_points, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "GetAllAccessPoints", + .out_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ), ), + .handle = impl_device_wifi_get_all_access_points, ), + NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( + NM_DEFINE_GDBUS_METHOD_INFO_INIT( + "RequestScan", + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("options", "a{sv}"), ), ), + .handle = impl_device_wifi_request_scan, ), ), + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, + &nm_signal_info_wireless_access_point_added, + &nm_signal_info_wireless_access_point_removed, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress", + "s", + NM_DEVICE_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PermHwAddress", + "s", + NM_DEVICE_PERM_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_DEVICE_WIFI_MODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Bitrate", + "u", + NM_DEVICE_WIFI_BITRATE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AccessPoints", + "ao", + NM_DEVICE_WIFI_ACCESS_POINTS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ActiveAccessPoint", + "o", + NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("WirelessCapabilities", + "u", + NM_DEVICE_WIFI_CAPABILITIES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("LastScan", + "x", + NM_DEVICE_WIFI_LAST_SCAN), ), ), + .legacy_property_changed = TRUE, +}; diff --git a/src/core/devices/wifi/nm-wifi-common.h b/src/core/devices/wifi/nm-wifi-common.h new file mode 100644 index 00000000..fd6f47ed --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-common.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#ifndef __NM_WIFI_COMMON_H__ +#define __NM_WIFI_COMMON_H__ + +#include "nm-dbus-utils.h" +#include "nm-wifi-ap.h" + +/*****************************************************************************/ + +void nm_device_wifi_emit_signal_access_point(NMDevice *device, + NMWifiAP *ap, + gboolean is_added /* or else is_removed */); + +extern const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless; +extern const GDBusSignalInfo nm_signal_info_wireless_access_point_added; +extern const GDBusSignalInfo nm_signal_info_wireless_access_point_removed; + +#endif /* __NM_WIFI_COMMON_H__ */ diff --git a/src/core/devices/wifi/nm-wifi-factory.c b/src/core/devices/wifi/nm-wifi-factory.c new file mode 100644 index 00000000..40375e1c --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-factory.c @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 - 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <gmodule.h> + +#include "devices/nm-device-factory.h" +#include "nm-setting-wireless.h" +#include "nm-setting-olpc-mesh.h" +#include "nm-device-wifi.h" +#include "nm-device-wifi-p2p.h" +#include "nm-device-olpc-mesh.h" +#include "nm-device-iwd.h" +#include "settings/nm-settings-connection.h" +#include "platform/nm-platform.h" +#include "nm-config.h" + +/*****************************************************************************/ + +#define NM_TYPE_WIFI_FACTORY (nm_wifi_factory_get_type()) +#define NM_WIFI_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory)) +#define NM_WIFI_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass)) +#define NM_IS_WIFI_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_FACTORY)) +#define NM_IS_WIFI_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_FACTORY)) +#define NM_WIFI_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass)) + +typedef struct { + NMDeviceFactory parent; +} NMWifiFactory; + +typedef struct { + NMDeviceFactoryClass parent; +} NMWifiFactoryClass; + +static GType nm_wifi_factory_get_type(void); + +G_DEFINE_TYPE(NMWifiFactory, nm_wifi_factory, NM_TYPE_DEVICE_FACTORY) + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES( + NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WIFI, NM_LINK_TYPE_OLPC_MESH) + NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIRELESS_SETTING_NAME, + NM_SETTING_OLPC_MESH_SETTING_NAME)) + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + return g_object_new(NM_TYPE_WIFI_FACTORY, NULL); +} + +/*****************************************************************************/ + +static void +p2p_device_created(NMDeviceWifi *device, NMDeviceWifiP2P *p2p_device, NMDeviceFactory *self) +{ + nm_log_info(LOGD_PLATFORM | LOGD_WIFI, + "Wi-Fi P2P device controlled by interface %s created", + nm_device_get_iface(NM_DEVICE(device))); + + g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, p2p_device); +} + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + gs_free char *backend = NULL; + + g_return_val_if_fail(iface != NULL, NULL); + g_return_val_if_fail(plink != NULL, NULL); + g_return_val_if_fail(g_strcmp0(iface, plink->name) == 0, NULL); + g_return_val_if_fail(NM_IN_SET(plink->type, NM_LINK_TYPE_WIFI, NM_LINK_TYPE_OLPC_MESH), NULL); + + if (plink->type != NM_LINK_TYPE_WIFI) + return nm_device_olpc_mesh_new(iface); + + backend = nm_config_data_get_device_config_by_pllink(NM_CONFIG_GET_DATA, + NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_BACKEND, + plink, + "wifi", + NULL); + nm_strstrip(backend); + + nm_log_dbg(LOGD_PLATFORM | LOGD_WIFI, + "(%s) config: backend is %s%s%s%s", + iface, + NM_PRINT_FMT_QUOTE_STRING(backend), + WITH_IWD ? " (iwd support enabled)" : ""); + if (!backend || !g_ascii_strcasecmp(backend, "wpa_supplicant")) { + NMDevice * device; + NMDeviceWifiCapabilities capabilities; + NM80211Mode mode; + + if (!nm_platform_wifi_get_capabilities(NM_PLATFORM_GET, plink->ifindex, &capabilities)) { + nm_log_warn(LOGD_PLATFORM | LOGD_WIFI, + "(%s) failed to initialize Wi-Fi driver for ifindex %d", + iface, + plink->ifindex); + return NULL; + } + + /* Ignore monitor-mode and other unhandled interface types. + * FIXME: keep TYPE_MONITOR devices in UNAVAILABLE state and manage + * them if/when they change to a handled type. + */ + mode = nm_platform_wifi_get_mode(NM_PLATFORM_GET, plink->ifindex); + if (mode == NM_802_11_MODE_UNKNOWN) { + *out_ignore = TRUE; + return NULL; + } + + device = nm_device_wifi_new(iface, capabilities); + + g_signal_connect_object(device, + NM_DEVICE_WIFI_P2P_DEVICE_CREATED, + G_CALLBACK(p2p_device_created), + factory, + 0); + + return device; + } +#if WITH_IWD + else if (!g_ascii_strcasecmp(backend, "iwd")) + return nm_device_iwd_new(iface); +#endif + + nm_log_warn(LOGD_PLATFORM | LOGD_WIFI, + "(%s) config: unknown or unsupported wifi-backend %s", + iface, + backend); + return NULL; +} + +/*****************************************************************************/ + +static void +nm_wifi_factory_init(NMWifiFactory *self) +{} + +static void +nm_wifi_factory_class_init(NMWifiFactoryClass *klass) +{ + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + factory_class->create_device = create_device; + factory_class->get_supported_types = get_supported_types; +} diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.c b/src/core/devices/wifi/nm-wifi-p2p-peer.c new file mode 100644 index 00000000..8488f32d --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-p2p-peer.c @@ -0,0 +1,699 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-wifi-p2p-peer.h" + +#include <stdlib.h> +#include <linux/if_ether.h> + +#include "NetworkManagerUtils.h" +#include "devices/nm-device.h" +#include "nm-core-internal.h" +#include "nm-dbus-manager.h" +#include "nm-glib-aux/nm-ref-string.h" +#include "nm-setting-wireless.h" +#include "nm-utils.h" +#include "nm-wifi-utils.h" +#include "platform/nm-platform.h" +#include "supplicant/nm-supplicant-types.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMWifiP2PPeer, + PROP_NAME, + PROP_MANUFACTURER, + PROP_MODEL, + PROP_MODEL_NUMBER, + PROP_SERIAL, + PROP_WFD_IES, + PROP_HW_ADDRESS, + PROP_STRENGTH, + PROP_LAST_SEEN, + PROP_FLAGS, ); + +struct _NMWifiP2PPeerPrivate { + NMRefString *supplicant_path; /* D-Bus object path of this Peer from wpa_supplicant */ + + /* Scanned or cached values */ + char *name; + char *manufacturer; + char *model; + char *model_number; + char *serial; + + char *address; + + GBytes *wfd_ies; + + const char **groups; + + guint8 strength; + + NM80211ApFlags flags; /* General flags */ + + /* Non-scanned attributes */ + gint32 + last_seen; /* Timestamp when the Peer was seen lastly (obtained via nm_utils_get_monotonic_timestamp_sec()) */ +}; + +typedef struct _NMWifiP2PPeerPrivate NMWifiP2PPeerPrivate; + +struct _NMWifiP2PPeerClass { + NMDBusObjectClass parent; +}; + +G_DEFINE_TYPE(NMWifiP2PPeer, nm_wifi_p2p_peer, NM_TYPE_DBUS_OBJECT) + +#define NM_WIFI_P2P_PEER_GET_PRIVATE(self) \ + _NM_GET_PRIVATE_PTR(self, NMWifiP2PPeer, NM_IS_WIFI_P2P_PEER) + +/*****************************************************************************/ + +const char ** +nm_wifi_p2p_peers_get_paths(const CList *peers_lst_head) +{ + NMWifiP2PPeer *peer; + const char ** list; + const char * path; + gsize i, n; + + n = c_list_length(peers_lst_head); + list = g_new(const char *, n + 1); + + i = 0; + if (n > 0) { + c_list_for_each_entry (peer, peers_lst_head, peers_lst) { + nm_assert(i < n); + path = nm_dbus_object_get_path(NM_DBUS_OBJECT(peer)); + nm_assert(path); + + list[i++] = path; + } + nm_assert(i <= n); + } + list[i] = NULL; + return list; +} + +NMWifiP2PPeer * +nm_wifi_p2p_peers_find_first_compatible(const CList *peers_lst_head, NMConnection *connection) +{ + NMWifiP2PPeer *peer; + + g_return_val_if_fail(connection, NULL); + + c_list_for_each_entry (peer, peers_lst_head, peers_lst) { + if (nm_wifi_p2p_peer_check_compatible(peer, connection)) + return peer; + } + return NULL; +} + +NMWifiP2PPeer * +nm_wifi_p2p_peers_find_by_supplicant_path(const CList *peers_lst_head, const char *path) +{ + NMWifiP2PPeer *peer; + + g_return_val_if_fail(path != NULL, NULL); + + c_list_for_each_entry (peer, peers_lst_head, peers_lst) { + if (nm_streq0(path, nm_wifi_p2p_peer_get_supplicant_path(peer))) + return peer; + } + return NULL; +} + +/*****************************************************************************/ + +NMWifiP2PPeer * +nm_wifi_p2p_peer_lookup_for_device(NMDevice *device, const char *exported_path) +{ + NMWifiP2PPeer *peer; + + g_return_val_if_fail(NM_IS_DEVICE(device), NULL); + + peer = (NMWifiP2PPeer *) nm_dbus_manager_lookup_object( + nm_dbus_object_get_manager(NM_DBUS_OBJECT(device)), + exported_path); + if (!peer || !NM_IS_WIFI_P2P_PEER(peer) || peer->wifi_device != device) + return NULL; + + return peer; +} + +/*****************************************************************************/ + +const char * +nm_wifi_p2p_peer_get_supplicant_path(NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return nm_ref_string_get_str(NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->supplicant_path); +} + +const char * +nm_wifi_p2p_peer_get_name(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->name; +} + +gboolean +nm_wifi_p2p_peer_set_name(NMWifiP2PPeer *peer, const char *str) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (!nm_utils_strdup_reset(&priv->name, str)) + return FALSE; + _notify(peer, PROP_NAME); + return TRUE; +} + +const char * +nm_wifi_p2p_peer_get_manufacturer(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->manufacturer; +} + +gboolean +nm_wifi_p2p_peer_set_manufacturer(NMWifiP2PPeer *peer, const char *str) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (!nm_utils_strdup_reset(&priv->manufacturer, str)) + return FALSE; + _notify(peer, PROP_MANUFACTURER); + return TRUE; +} + +const char * +nm_wifi_p2p_peer_get_model(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->model; +} + +gboolean +nm_wifi_p2p_peer_set_model(NMWifiP2PPeer *peer, const char *str) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (!nm_utils_strdup_reset(&priv->model, str)) + return FALSE; + _notify(peer, PROP_MODEL); + return TRUE; +} + +const char * +nm_wifi_p2p_peer_get_model_number(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->model_number; +} + +gboolean +nm_wifi_p2p_peer_set_model_number(NMWifiP2PPeer *peer, const char *str) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (!nm_utils_strdup_reset(&priv->model_number, str)) + return FALSE; + _notify(peer, PROP_MODEL_NUMBER); + return TRUE; +} + +const char * +nm_wifi_p2p_peer_get_serial(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->serial; +} + +gboolean +nm_wifi_p2p_peer_set_serial(NMWifiP2PPeer *peer, const char *str) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (!nm_utils_strdup_reset(&priv->serial, str)) + return FALSE; + _notify(peer, PROP_SERIAL); + return TRUE; +} + +GBytes * +nm_wifi_p2p_peer_get_wfd_ies(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->wfd_ies; +} + +gboolean +nm_wifi_p2p_peer_set_wfd_ies(NMWifiP2PPeer *peer, GBytes *wfd_ies) +{ + NMWifiP2PPeerPrivate *priv; + gs_unref_bytes GBytes *wfd_ies_old = NULL; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE); + + priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (nm_gbytes_equal0(priv->wfd_ies, wfd_ies)) + return FALSE; + + wfd_ies_old = g_steal_pointer(&priv->wfd_ies); + priv->wfd_ies = wfd_ies ? g_bytes_ref(wfd_ies) : NULL; + + _notify(peer, PROP_WFD_IES); + return TRUE; +} + +const char *const * +nm_wifi_p2p_peer_get_groups(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->groups; +} + +const char * +nm_wifi_p2p_peer_get_address(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->address; +} + +static gboolean +nm_wifi_p2p_peer_set_address_bin(NMWifiP2PPeer *peer, const guint8 addr[static ETH_ALEN]) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (priv->address && nm_utils_hwaddr_matches(addr, ETH_ALEN, priv->address, -1)) + return FALSE; + + g_free(priv->address); + priv->address = nm_utils_hwaddr_ntoa(addr, ETH_ALEN); + _notify(peer, PROP_HW_ADDRESS); + return TRUE; +} + +gboolean +nm_wifi_p2p_peer_set_address(NMWifiP2PPeer *peer, const char *addr) +{ + guint8 addr_buf[ETH_ALEN]; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE); + + if (!addr || !nm_utils_hwaddr_aton(addr, addr_buf, sizeof(addr_buf))) + g_return_val_if_reached(FALSE); + + return nm_wifi_p2p_peer_set_address_bin(peer, addr_buf); +} + +gint8 +nm_wifi_p2p_peer_get_strength(NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), 0); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->strength; +} + +gboolean +nm_wifi_p2p_peer_set_strength(NMWifiP2PPeer *peer, const gint8 strength) +{ + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (priv->strength != strength) { + priv->strength = strength; + _notify(peer, PROP_STRENGTH); + return TRUE; + } + return FALSE; +} + +NM80211ApFlags +nm_wifi_p2p_peer_get_flags(const NMWifiP2PPeer *peer) +{ + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NM_802_11_AP_FLAGS_NONE); + + return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->flags; +} + +static gboolean +nm_wifi_p2p_peer_set_last_seen(NMWifiP2PPeer *peer, gint32 last_seen) +{ + NMWifiP2PPeerPrivate *priv; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE); + + priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + if (priv->last_seen != last_seen) { + priv->last_seen = last_seen; + _notify(peer, PROP_LAST_SEEN); + return TRUE; + } + return FALSE; +} + +/*****************************************************************************/ + +gboolean +nm_wifi_p2p_peer_update_from_properties(NMWifiP2PPeer *peer, const NMSupplicantPeerInfo *peer_info) +{ + NMWifiP2PPeerPrivate *priv; + gboolean changed = FALSE; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE); + g_return_val_if_fail(peer_info, FALSE); + nm_assert(NM_IS_REF_STRING(peer_info->peer_path)); + + priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer); + + nm_assert(!priv->supplicant_path || priv->supplicant_path == peer_info->peer_path); + + g_object_freeze_notify(G_OBJECT(peer)); + + if (!priv->supplicant_path) { + priv->supplicant_path = nm_ref_string_ref(peer_info->peer_path); + changed = TRUE; + } + + changed |= nm_wifi_p2p_peer_set_strength(peer, peer_info->signal_percent); + changed |= nm_wifi_p2p_peer_set_name(peer, peer_info->device_name); + changed |= nm_wifi_p2p_peer_set_manufacturer(peer, peer_info->manufacturer); + changed |= nm_wifi_p2p_peer_set_model(peer, peer_info->model); + changed |= nm_wifi_p2p_peer_set_model_number(peer, peer_info->model_number); + changed |= nm_wifi_p2p_peer_set_serial(peer, peer_info->serial); + + if (peer_info->address_valid) + changed |= nm_wifi_p2p_peer_set_address_bin(peer, peer_info->address); + else { + /* we don't reset the address. */ + } + + changed |= nm_wifi_p2p_peer_set_wfd_ies(peer, peer_info->ies); + changed |= nm_wifi_p2p_peer_set_last_seen(peer, peer_info->last_seen_msec / 1000u); + + /* We currently only use the groups information internally to check if + * the peer is still joined. */ + if (!nm_utils_strv_equal(priv->groups, peer_info->groups)) { + g_free(priv->groups); + priv->groups = nm_utils_strv_dup_packed(peer_info->groups, -1); + changed |= TRUE; + } + + g_object_thaw_notify(G_OBJECT(peer)); + + return changed; +} + +const char * +nm_wifi_p2p_peer_to_string(const NMWifiP2PPeer *self, char *str_buf, gsize buf_len, gint32 now_s) +{ + const NMWifiP2PPeerPrivate *priv; + const char * supplicant_id = "-"; + const char * export_path; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(self), NULL); + + priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self); + + if (priv->supplicant_path) + supplicant_id = strrchr(priv->supplicant_path->str, '/') ?: supplicant_id; + + export_path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self)); + if (export_path) + export_path = strrchr(export_path, '/') ?: export_path; + else + export_path = "/"; + + g_snprintf(str_buf, + buf_len, + "%17s [n:%s, m:%s, mod:%s, mod_num:%s, ser:%s] %3us sup:%s [nm:%s]", + priv->address ?: "(none)", + priv->name, + priv->manufacturer, + priv->model, + priv->model_number, + priv->serial, + priv->last_seen > 0 ? ((now_s > 0 ? now_s : nm_utils_get_monotonic_timestamp_sec()) + - priv->last_seen) + : -1, + supplicant_id, + export_path); + + return str_buf; +} + +gboolean +nm_wifi_p2p_peer_check_compatible(NMWifiP2PPeer *self, NMConnection *connection) +{ + NMWifiP2PPeerPrivate *priv; + NMSettingWifiP2P * s_wifi_p2p; + const char * hwaddr; + + g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(self), FALSE); + g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE); + + priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self); + + s_wifi_p2p = + NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)); + if (s_wifi_p2p == NULL) + return FALSE; + + hwaddr = nm_setting_wifi_p2p_get_peer(s_wifi_p2p); + if (hwaddr && (!priv->address || !nm_utils_hwaddr_matches(hwaddr, -1, priv->address, -1))) + return FALSE; + + return TRUE; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMWifiP2PPeer * self = NM_WIFI_P2P_PEER(object); + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_FLAGS: + g_value_set_uint(value, priv->flags); + break; + case PROP_NAME: + g_value_set_string(value, priv->name); + break; + case PROP_MANUFACTURER: + g_value_set_string(value, priv->manufacturer); + break; + case PROP_MODEL: + g_value_set_string(value, priv->model); + break; + case PROP_MODEL_NUMBER: + g_value_set_string(value, priv->model_number); + break; + case PROP_SERIAL: + g_value_set_string(value, priv->serial); + break; + case PROP_WFD_IES: + g_value_take_variant(value, nm_utils_gbytes_to_variant_ay(priv->wfd_ies)); + break; + case PROP_HW_ADDRESS: + g_value_set_string(value, priv->address); + break; + case PROP_STRENGTH: + g_value_set_uchar(value, priv->strength); + break; + case PROP_LAST_SEEN: + g_value_set_int(value, + priv->last_seen > 0 + ? (int) nm_utils_monotonic_timestamp_as_boottime(priv->last_seen, + NM_UTILS_NSEC_PER_SEC) + : -1); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_wifi_p2p_peer_init(NMWifiP2PPeer *self) +{ + NMWifiP2PPeerPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerPrivate); + + self->_priv = priv; + + c_list_init(&self->peers_lst); + + priv->flags = NM_802_11_AP_FLAGS_NONE; + priv->last_seen = -1; +} + +NMWifiP2PPeer * +nm_wifi_p2p_peer_new_from_properties(const NMSupplicantPeerInfo *peer_info) +{ + NMWifiP2PPeer *peer; + + g_return_val_if_fail(peer_info, NULL); + + peer = g_object_new(NM_TYPE_WIFI_P2P_PEER, NULL); + nm_wifi_p2p_peer_update_from_properties(peer, peer_info); + return peer; +} + +static void +finalize(GObject *object) +{ + NMWifiP2PPeer * self = NM_WIFI_P2P_PEER(object); + NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self); + + nm_assert(!self->wifi_device); + nm_assert(c_list_is_empty(&self->peers_lst)); + + nm_ref_string_unref(priv->supplicant_path); + g_free(priv->name); + g_free(priv->manufacturer); + g_free(priv->model); + g_free(priv->model_number); + g_free(priv->serial); + g_free(priv->address); + g_bytes_unref(priv->wfd_ies); + g_free(priv->groups); + + G_OBJECT_CLASS(nm_wifi_p2p_peer_parent_class)->finalize(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_p2p_peer = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_WIFI_P2P_PEER, + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + /* Before 1.24, we wrongly exposed a property "Groups" of type "as". Don't reuse that property name. */ + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Flags", "u", NM_WIFI_P2P_PEER_FLAGS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Name", "s", NM_WIFI_P2P_PEER_NAME), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Manufacturer", + "s", + NM_WIFI_P2P_PEER_MANUFACTURER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Model", "s", NM_WIFI_P2P_PEER_MODEL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ModelNumber", + "s", + NM_WIFI_P2P_PEER_MODEL_NUMBER), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Serial", "s", NM_WIFI_P2P_PEER_SERIAL), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WfdIEs", + "ay", + NM_WIFI_P2P_PEER_WFD_IES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", + "s", + NM_WIFI_P2P_PEER_HW_ADDRESS), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Strength", + "y", + NM_WIFI_P2P_PEER_STRENGTH), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("LastSeen", + "i", + NM_WIFI_P2P_PEER_LAST_SEEN), ), ), + .legacy_property_changed = FALSE, +}; + +static void +nm_wifi_p2p_peer_class_init(NMWifiP2PPeerClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + + g_type_class_add_private(object_class, sizeof(NMWifiP2PPeerPrivate)); + + dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH_WIFI_P2P_PEER); + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_p2p_peer); + + object_class->get_property = get_property; + object_class->finalize = finalize; + + obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_WIFI_P2P_PEER_FLAGS, + "", + "", + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_FLAGS_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_NAME] = g_param_spec_string(NM_WIFI_P2P_PEER_NAME, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MANUFACTURER] = + g_param_spec_string(NM_WIFI_P2P_PEER_MANUFACTURER, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MODEL] = g_param_spec_string(NM_WIFI_P2P_PEER_MODEL, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MODEL_NUMBER] = + g_param_spec_string(NM_WIFI_P2P_PEER_MODEL_NUMBER, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SERIAL] = g_param_spec_string(NM_WIFI_P2P_PEER_SERIAL, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_WFD_IES] = g_param_spec_variant(NM_WIFI_P2P_PEER_WFD_IES, + "", + "", + G_VARIANT_TYPE("ay"), + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_HW_ADDRESS] = + g_param_spec_string(NM_WIFI_P2P_PEER_HW_ADDRESS, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_STRENGTH] = g_param_spec_uchar(NM_WIFI_P2P_PEER_STRENGTH, + "", + "", + 0, + G_MAXINT8, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_LAST_SEEN] = g_param_spec_int(NM_WIFI_P2P_PEER_LAST_SEEN, + "", + "", + -1, + G_MAXINT, + -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.h b/src/core/devices/wifi/nm-wifi-p2p-peer.h new file mode 100644 index 00000000..ee4bfb53 --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-p2p-peer.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2018 Red Hat, Inc. + */ + +#ifndef __NM_WIFI_P2P_PEER_H__ +#define __NM_WIFI_P2P_PEER_H__ + +#include "nm-dbus-object.h" +#include "nm-dbus-interface.h" +#include "nm-connection.h" + +#define NM_TYPE_WIFI_P2P_PEER (nm_wifi_p2p_peer_get_type()) +#define NM_WIFI_P2P_PEER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer)) +#define NM_WIFI_P2P_PEER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass)) +#define NM_IS_WIFI_P2P_PEER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_P2P_PEER)) +#define NM_IS_WIFI_P2P_PEER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_P2P_PEER)) +#define NM_WIFI_P2P_PEER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass)) + +#define NM_WIFI_P2P_PEER_FLAGS "flags" +#define NM_WIFI_P2P_PEER_NAME "name" +#define NM_WIFI_P2P_PEER_MANUFACTURER "manufacturer" +#define NM_WIFI_P2P_PEER_MODEL "model" +#define NM_WIFI_P2P_PEER_MODEL_NUMBER "model-number" +#define NM_WIFI_P2P_PEER_SERIAL "serial" +#define NM_WIFI_P2P_PEER_WFD_IES "wfd-ies" +#define NM_WIFI_P2P_PEER_HW_ADDRESS "hw-address" +#define NM_WIFI_P2P_PEER_STRENGTH "strength" +#define NM_WIFI_P2P_PEER_LAST_SEEN "last-seen" + +typedef struct { + NMDBusObject parent; + NMDevice * wifi_device; + CList peers_lst; + struct _NMWifiP2PPeerPrivate *_priv; +} NMWifiP2PPeer; + +typedef struct _NMWifiP2PPeerClass NMWifiP2PPeerClass; + +struct _NMSupplicantPeerInfo; + +GType nm_wifi_p2p_peer_get_type(void); + +NMWifiP2PPeer *nm_wifi_p2p_peer_new_from_properties(const struct _NMSupplicantPeerInfo *peer_info); + +gboolean nm_wifi_p2p_peer_update_from_properties(NMWifiP2PPeer * peer, + const struct _NMSupplicantPeerInfo *peer_info); + +gboolean nm_wifi_p2p_peer_check_compatible(NMWifiP2PPeer *self, NMConnection *connection); + +const char *nm_wifi_p2p_peer_get_supplicant_path(NMWifiP2PPeer *peer); + +const char *nm_wifi_p2p_peer_get_name(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_name(NMWifiP2PPeer *peer, const char *name); +const char *nm_wifi_p2p_peer_get_manufacturer(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_manufacturer(NMWifiP2PPeer *peer, const char *manufacturer); +const char *nm_wifi_p2p_peer_get_model(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_model(NMWifiP2PPeer *peer, const char *model); +const char *nm_wifi_p2p_peer_get_model_number(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_model_number(NMWifiP2PPeer *peer, const char *number); +const char *nm_wifi_p2p_peer_get_serial(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_serial(NMWifiP2PPeer *peer, const char *serial); + +GBytes * nm_wifi_p2p_peer_get_wfd_ies(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_wfd_ies(NMWifiP2PPeer *peer, GBytes *bytes); + +const char *const *nm_wifi_p2p_peer_get_groups(const NMWifiP2PPeer *peer); + +const char * nm_wifi_p2p_peer_get_address(const NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_address(NMWifiP2PPeer *peer, const char *addr); +gint8 nm_wifi_p2p_peer_get_strength(NMWifiP2PPeer *peer); +gboolean nm_wifi_p2p_peer_set_strength(NMWifiP2PPeer *peer, gint8 strength); +NM80211ApFlags nm_wifi_p2p_peer_get_flags(const NMWifiP2PPeer *self); + +const char * +nm_wifi_p2p_peer_to_string(const NMWifiP2PPeer *self, char *str_buf, gsize buf_len, gint32 now_s); + +const char **nm_wifi_p2p_peers_get_paths(const CList *peers_lst_head); + +NMWifiP2PPeer *nm_wifi_p2p_peers_find_first_compatible(const CList * peers_lst_head, + NMConnection *connection); + +NMWifiP2PPeer *nm_wifi_p2p_peers_find_by_supplicant_path(const CList *peers_lst_head, + const char * path); + +NMWifiP2PPeer *nm_wifi_p2p_peer_lookup_for_device(NMDevice *device, const char *exported_path); + +#endif /* __NM_WIFI_P2P_PEER_H__ */ diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c new file mode 100644 index 00000000..aed236cc --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-utils.c @@ -0,0 +1,945 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-wifi-utils.h" + +#include <stdlib.h> + +#include "nm-utils.h" +#include "nm-core-internal.h" + +static gboolean +verify_no_wep(NMSettingWirelessSecurity *s_wsec, const char *tag, GError **error) +{ + if (nm_setting_wireless_security_get_wep_key(s_wsec, 0) + || nm_setting_wireless_security_get_wep_key(s_wsec, 1) + || nm_setting_wireless_security_get_wep_key(s_wsec, 2) + || nm_setting_wireless_security_get_wep_key(s_wsec, 3) + || nm_setting_wireless_security_get_wep_tx_keyidx(s_wsec) + || nm_setting_wireless_security_get_wep_key_type(s_wsec)) { + /* Dynamic WEP cannot have any WEP keys set */ + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("%s is incompatible with static WEP keys"), + tag); + g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME); + return FALSE; + } + + return TRUE; +} + +static gboolean +verify_leap(NMSettingWirelessSecurity *s_wsec, + NMSetting8021x * s_8021x, + gboolean adhoc, + GError ** error) +{ + const char *key_mgmt, *auth_alg, *leap_username; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + leap_username = nm_setting_wireless_security_get_leap_username(s_wsec); + + /* One (or both) of two things indicates we want LEAP: + * 1) auth_alg == 'leap' + * 2) valid leap_username + * + * LEAP always requires a LEAP username. + */ + + if (auth_alg) { + if (!strcmp(auth_alg, "leap")) { + /* LEAP authentication requires at least a LEAP username */ + if (!leap_username) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_MISSING_PROPERTY, + _("LEAP authentication requires a LEAP username")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME); + return FALSE; + } + } else if (leap_username) { + /* Leap username requires 'leap' auth */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("LEAP username requires 'leap' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME); + return FALSE; + } + } + + if (leap_username) { + if (key_mgmt && strcmp(key_mgmt, "ieee8021x")) { + /* LEAP requires ieee8021x key management */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("LEAP authentication requires IEEE 802.1x key management")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + } + + /* At this point if auth_alg is set it must be 'leap', and if key_mgmt + * is set it must be 'ieee8021x'. + */ + if (leap_username) { + if (auth_alg) + g_assert(strcmp(auth_alg, "leap") == 0); + if (key_mgmt) + g_assert(strcmp(key_mgmt, "ieee8021x") == 0); + + if (adhoc) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("LEAP authentication is incompatible with Ad-Hoc mode")); + g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME); + return FALSE; + } + + if (!verify_no_wep(s_wsec, "LEAP", error)) + return FALSE; + + if (s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("LEAP authentication is incompatible with 802.1x setting")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +verify_no_wpa(NMSettingWirelessSecurity *s_wsec, const char *tag, GError **error) +{ + const char *key_mgmt; + int n, i; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + if (key_mgmt && !strncmp(key_mgmt, "wpa", 3)) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("a connection using '%s' authentication cannot use WPA key management"), + tag); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + + if (nm_setting_wireless_security_get_num_protos(s_wsec)) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("a connection using '%s' authentication cannot specify WPA protocols"), + tag); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PROTO); + return FALSE; + } + + n = nm_setting_wireless_security_get_num_pairwise(s_wsec); + for (i = 0; i < n; i++) { + const char *pw; + + pw = nm_setting_wireless_security_get_pairwise(s_wsec, i); + if (!strcmp(pw, "tkip") || !strcmp(pw, "ccmp")) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("a connection using '%s' authentication cannot specify WPA ciphers"), + tag); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PAIRWISE); + return FALSE; + } + } + + n = nm_setting_wireless_security_get_num_groups(s_wsec); + for (i = 0; i < n; i++) { + const char *gr; + + gr = nm_setting_wireless_security_get_group(s_wsec, i); + if (strcmp(gr, "wep40") && strcmp(gr, "wep104")) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("a connection using '%s' authentication cannot specify WPA ciphers"), + tag); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_GROUP); + return FALSE; + } + } + + if (nm_setting_wireless_security_get_psk(s_wsec)) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("a connection using '%s' authentication cannot specify a WPA password"), + tag); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PSK); + return FALSE; + } + + return TRUE; +} + +static gboolean +verify_dynamic_wep(NMSettingWirelessSecurity *s_wsec, + NMSetting8021x * s_8021x, + gboolean adhoc, + GError ** error) +{ + const char *key_mgmt, *auth_alg, *leap_username; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + leap_username = nm_setting_wireless_security_get_leap_username(s_wsec); + + g_return_val_if_fail(leap_username == NULL, TRUE); + + if (key_mgmt) { + if (!strcmp(key_mgmt, "ieee8021x")) { + if (!s_8021x) { + /* 802.1x key management requires an 802.1x setting */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_MISSING_SETTING, + _("Dynamic WEP requires an 802.1x setting")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + + if (auth_alg && strcmp(auth_alg, "open")) { + /* 802.1x key management must use "open" authentication */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Dynamic WEP requires 'open' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + /* Dynamic WEP incompatible with anything static WEP related */ + if (!verify_no_wep(s_wsec, "Dynamic WEP", error)) + return FALSE; + } else if (!strcmp(key_mgmt, "none")) { + if (s_8021x) { + /* 802.1x setting requires 802.1x key management */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Dynamic WEP requires 'ieee8021x' key management")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + } + } else if (s_8021x) { + /* 802.1x setting incompatible with anything but 'open' auth */ + if (auth_alg && strcmp(auth_alg, "open")) { + /* 802.1x key management must use "open" authentication */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Dynamic WEP requires 'open' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + /* Dynamic WEP incompatible with anything static WEP related */ + if (!verify_no_wep(s_wsec, "Dynamic WEP", error)) + return FALSE; + } + + return TRUE; +} + +static gboolean +verify_wpa_psk(NMSettingWirelessSecurity *s_wsec, + NMSetting8021x * s_8021x, + gboolean adhoc, + guint32 wpa_flags, + guint32 rsn_flags, + GError ** error) +{ + const char *key_mgmt, *auth_alg; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + + if (!nm_streq0(key_mgmt, "wpa-psk")) + return TRUE; + + if (s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("WPA-PSK authentication is incompatible with 802.1x")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + + if (auth_alg && !nm_streq(auth_alg, "open")) { + /* WPA must use "open" authentication */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA-PSK requires 'open' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + /* Make sure the AP's capabilities support WPA-PSK */ + if (!(wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK) + && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Access point does not support PSK but setting requires it")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + + if (adhoc) { + /* Ad-Hoc RSN requires 'rsn' proto, 'ccmp' pairwise, and 'ccmp' group */ + if (nm_setting_wireless_security_get_num_protos(s_wsec) != 1 + || !nm_streq0(nm_setting_wireless_security_get_proto(s_wsec, 0), "rsn")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA Ad-Hoc authentication requires 'rsn' protocol")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PROTO); + return FALSE; + } + + if (nm_setting_wireless_security_get_num_pairwise(s_wsec) != 1 + || !nm_streq0(nm_setting_wireless_security_get_pairwise(s_wsec, 0), "ccmp")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA Ad-Hoc authentication requires 'ccmp' pairwise cipher")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_PAIRWISE); + return FALSE; + } + + if (nm_setting_wireless_security_get_num_groups(s_wsec) != 1 + || !nm_streq0(nm_setting_wireless_security_get_group(s_wsec, 0), "ccmp")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA Ad-Hoc requires 'ccmp' group cipher")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_GROUP); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +verify_wpa_eap(NMSettingWirelessSecurity *s_wsec, + NMSetting8021x * s_8021x, + guint32 wpa_flags, + guint32 rsn_flags, + GError ** error) +{ + const char *key_mgmt, *auth_alg; + gboolean is_wpa_eap = FALSE; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + + if (key_mgmt) { + if (NM_IN_STRSET(key_mgmt, "wpa-eap", "wpa-eap-suite-b-192")) { + if (!s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_MISSING_SETTING, + _("WPA-EAP authentication requires an 802.1x setting")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + + if (auth_alg && strcmp(auth_alg, "open")) { + /* WPA must use "open" authentication */ + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA-EAP requires 'open' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + is_wpa_eap = TRUE; + } else if (s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("802.1x setting requires 'wpa-eap' key management")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + } + + if (is_wpa_eap || s_8021x) { + /* Make sure the AP's capabilities support WPA-EAP */ + if (!(wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X) + && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X) + && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("Access point does not support 802.1x but setting requires it")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +verify_adhoc(NMSettingWirelessSecurity *s_wsec, + NMSetting8021x * s_8021x, + gboolean adhoc, + GError ** error) +{ + const char *key_mgmt = NULL, *leap_username = NULL, *auth_alg = NULL; + + if (!adhoc) + return TRUE; + + if (s_wsec) { + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + leap_username = nm_setting_wireless_security_get_leap_username(s_wsec); + } + + if (key_mgmt && !NM_IN_STRSET(key_mgmt, "none", "wpa-psk")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Ad-Hoc mode requires 'none' or 'wpa-psk' key management")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + + if (s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("Ad-Hoc mode is incompatible with 802.1x security")); + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + + if (leap_username) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Ad-Hoc mode is incompatible with LEAP security")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + if (auth_alg && !nm_streq(auth_alg, "open")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("Ad-Hoc mode requires 'open' authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + return TRUE; +} + +gboolean +nm_wifi_utils_complete_connection(GBytes * ap_ssid, + const char * bssid, + NM80211Mode ap_mode, + guint32 ap_freq, + guint32 ap_flags, + guint32 ap_wpa_flags, + guint32 ap_rsn_flags, + NMConnection *connection, + gboolean lock_bssid, + GError ** error) +{ + NMSettingWireless * s_wifi; + NMSettingWirelessSecurity *s_wsec; + NMSetting8021x * s_8021x; + GBytes * ssid; + const char * mode, *key_mgmt, *auth_alg, *leap_username; + gboolean adhoc = FALSE; + gboolean mesh = FALSE; + + s_wifi = nm_connection_get_setting_wireless(connection); + g_assert(s_wifi); + s_wsec = nm_connection_get_setting_wireless_security(connection); + s_8021x = nm_connection_get_setting_802_1x(connection); + + /* Fill in missing SSID */ + ssid = nm_setting_wireless_get_ssid(s_wifi); + if (!ssid) + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_SSID, ap_ssid, NULL); + else if (!ap_ssid || !g_bytes_equal(ssid, ap_ssid)) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("connection does not match access point")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SETTING_NAME, + NM_SETTING_WIRELESS_SSID); + return FALSE; + } + + if (lock_bssid && !nm_setting_wireless_get_bssid(s_wifi)) + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_BSSID, bssid, NULL); + + /* And mode */ + mode = nm_setting_wireless_get_mode(s_wifi); + if (mode) { + gboolean valid = FALSE; + + /* Make sure the supplied mode matches the AP's */ + if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_INFRA) + || !strcmp(mode, NM_SETTING_WIRELESS_MODE_AP)) { + if (ap_mode == NM_802_11_MODE_INFRA) + valid = TRUE; + } else if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) { + if (ap_mode == NM_802_11_MODE_ADHOC) + valid = TRUE; + adhoc = TRUE; + } else if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_MESH)) { + if (ap_mode == NM_802_11_MODE_MESH) + valid = TRUE; + mesh = TRUE; + } + + if (valid == FALSE) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("connection does not match access point")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SETTING_NAME, + NM_SETTING_WIRELESS_MODE); + return FALSE; + } + } else { + mode = NM_SETTING_WIRELESS_MODE_INFRA; + if (ap_mode == NM_802_11_MODE_ADHOC) { + mode = NM_SETTING_WIRELESS_MODE_ADHOC; + adhoc = TRUE; + } else if (ap_mode == NM_802_11_MODE_MESH) { + mode = NM_SETTING_WIRELESS_MODE_MESH; + mesh = TRUE; + } + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, mode, NULL); + } + + /* For now mesh requires channel and band, fill them only if both not present. + * Do not check existing values against an existing ap/mesh point, + * mesh join will start a new network if required */ + if (mesh) { + const char *band; + guint32 channel; + gboolean band_valid = TRUE; + gboolean chan_valid = TRUE; + gboolean valid; + + band = nm_setting_wireless_get_band(s_wifi); + channel = nm_setting_wireless_get_channel(s_wifi); + + valid = ((band == NULL) && (channel == 0)) || ((band != NULL) && (channel != 0)); + + if ((band == NULL) && (channel == 0)) { + channel = nm_utils_wifi_freq_to_channel(ap_freq); + if (channel) { + g_object_set(s_wifi, NM_SETTING_WIRELESS_CHANNEL, channel, NULL); + } else { + chan_valid = FALSE; + } + + band = nm_utils_wifi_freq_to_band(ap_freq); + if (band) { + g_object_set(s_wifi, NM_SETTING_WIRELESS_BAND, band, NULL); + } else { + band_valid = FALSE; + } + } + + if (!valid || !chan_valid || !band_valid) { + g_set_error(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("connection does not match mesh point")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SETTING_NAME, + NM_SETTING_WIRELESS_MODE); + return FALSE; + } + } + + /* Security */ + + /* Open */ + if (!(ap_flags & NM_802_11_AP_FLAGS_PRIVACY) && (ap_wpa_flags == NM_802_11_AP_SEC_NONE) + && (ap_rsn_flags == NM_802_11_AP_SEC_NONE)) { + /* Make sure the connection doesn't specify security */ + if (s_wsec || s_8021x) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_SETTING, + _("Access point is unencrypted but setting specifies security")); + if (s_wsec) + g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME); + else + g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME); + return FALSE; + } + return TRUE; + } + + /* Everything else requires security */ + if (!s_wsec) { + s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wsec)); + } + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); + auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec); + leap_username = nm_setting_wireless_security_get_leap_username(s_wsec); + + /* Ad-Hoc checks */ + if (!verify_adhoc(s_wsec, s_8021x, adhoc, error)) + return FALSE; + + /* Static WEP, Dynamic WEP, or LEAP */ + if ((ap_flags & NM_802_11_AP_FLAGS_PRIVACY) && (ap_wpa_flags == NM_802_11_AP_SEC_NONE) + && (ap_rsn_flags == NM_802_11_AP_SEC_NONE)) { + const char *tag = "WEP"; + gboolean is_dynamic_wep = FALSE; + + if (!verify_leap(s_wsec, s_8021x, adhoc, error)) + return FALSE; + + if (leap_username) { + tag = "LEAP"; + } else { + /* Static or Dynamic WEP */ + if (!verify_dynamic_wep(s_wsec, s_8021x, adhoc, error)) + return FALSE; + + if (s_8021x || (key_mgmt && !strcmp(key_mgmt, "ieee8021x"))) { + is_dynamic_wep = TRUE; + tag = "Dynamic WEP"; + } + } + + /* Nothing WPA-related can be set */ + if (!verify_no_wpa(s_wsec, tag, error)) + return FALSE; + + if (leap_username) { + /* LEAP */ + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "ieee8021x", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "leap", + NULL); + } else if (is_dynamic_wep) { + /* Dynamic WEP */ + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "ieee8021x", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + + if (s_8021x) { + /* Dynamic WEP requires a valid 802.1x setting since we can't + * autocomplete 802.1x. + */ + if (!nm_setting_verify(NM_SETTING(s_8021x), NULL, error)) + return FALSE; + } + } else { + /* Static WEP */ + g_object_set(s_wsec, NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "none", NULL); + } + + return TRUE; + } + + /* WPA/RSN */ + g_assert(ap_wpa_flags || ap_rsn_flags); + + /* Ensure key management is valid for WPA */ + if ((key_mgmt && !strcmp(key_mgmt, "ieee8021x")) || leap_username) { + g_set_error_literal( + error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA authentication is incompatible with non-EAP (original) LEAP or Dynamic WEP")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT); + return FALSE; + } + + /* 'shared' auth incompatible with any type of WPA */ + if (auth_alg && strcmp(auth_alg, "open")) { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + _("WPA authentication is incompatible with Shared Key authentication")); + g_prefix_error(error, + "%s.%s: ", + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG); + return FALSE; + } + + if (!verify_no_wep(s_wsec, "WPA", error)) + return FALSE; + + if (!verify_wpa_psk(s_wsec, s_8021x, adhoc, ap_wpa_flags, ap_rsn_flags, error)) + return FALSE; + + if (!adhoc && !verify_wpa_eap(s_wsec, s_8021x, ap_wpa_flags, ap_rsn_flags, error)) + return FALSE; + + if (adhoc) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "wpa-psk", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + nm_setting_wireless_security_add_proto(s_wsec, "rsn"); + nm_setting_wireless_security_add_pairwise(s_wsec, "ccmp"); + nm_setting_wireless_security_add_group(s_wsec, "ccmp"); + } else if (s_8021x) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "wpa-eap", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + /* Leave proto/pairwise/group as client set them; if they are unset the + * supplicant will figure out the best combination at connect time. + */ + + /* 802.1x also requires the client to completely fill in the 8021x + * setting. Since there's so much configuration required for it, there's + * no way it can be automatically completed. + */ + } else if ((key_mgmt && !strcmp(key_mgmt, "sae")) + || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE)) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "sae", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + } else if ((key_mgmt && !strcmp(key_mgmt, "owe")) + || NM_FLAGS_ANY(ap_rsn_flags, + NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "owe", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + } else if ((key_mgmt && !strcmp(key_mgmt, "wpa-psk")) + || (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK) + || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "wpa-psk", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + /* Leave proto/pairwise/group as client set them; if they are unset the + * supplicant will figure out the best combination at connect time. + */ + } else if ((key_mgmt && !strcmp(key_mgmt, "wpa-eap-suite-b-192")) + || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)) { + g_object_set(s_wsec, + NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, + "wpa-eap-suite-b-192", + NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open", + NULL); + } else { + g_set_error_literal(error, + NM_CONNECTION_ERROR, + NM_CONNECTION_ERROR_FAILED, + _("Failed to determine AP security information")); + return FALSE; + } + + return TRUE; +} + +gboolean +nm_wifi_utils_is_manf_default_ssid(GBytes *ssid) +{ + const guint8 *ssid_p; + gsize ssid_l; + int i; + /* + * List of manufacturer default SSIDs that are often unchanged by users. + * + * NOTE: this list should *not* contain networks that you would like to + * automatically roam to like "Starbucks" or "AT&T" or "T-Mobile HotSpot". + */ + static const char *manf_defaults[] = { + "linksys", + "linksys-a", + "linksys-g", + "default", + "belkin54g", + "NETGEAR", + "o2DSL", + "WLAN", + "ALICE-WLAN", + "Speedport W 501V", + "TURBONETT", + }; + + ssid_p = g_bytes_get_data(ssid, &ssid_l); + + for (i = 0; i < G_N_ELEMENTS(manf_defaults); i++) { + if (ssid_l == strlen(manf_defaults[i])) { + if (memcmp(manf_defaults[i], ssid_p, ssid_l) == 0) + return TRUE; + } + } + return FALSE; +} + +/* To be used for connections where the SSID has been validated before */ +gboolean +nm_wifi_connection_get_iwd_ssid_and_security(NMConnection * connection, + char ** ssid, + NMIwdNetworkSecurity *security) +{ + NMSettingWireless * s_wireless; + NMSettingWirelessSecurity *s_wireless_sec; + const char * key_mgmt = NULL; + + s_wireless = nm_connection_get_setting_wireless(connection); + if (!s_wireless) + return FALSE; + + if (ssid) { + GBytes * bytes = nm_setting_wireless_get_ssid(s_wireless); + gsize ssid_len; + const char *ssid_str = (const char *) g_bytes_get_data(bytes, &ssid_len); + + nm_assert(bytes && g_utf8_validate(ssid_str, ssid_len, NULL)); + NM_SET_OUT(ssid, g_strndup(ssid_str, ssid_len)); + } + + if (!security) + return TRUE; + + s_wireless_sec = nm_connection_get_setting_wireless_security(connection); + if (!s_wireless_sec) { + NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_OPEN); + return TRUE; + } + + key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wireless_sec); + nm_assert(key_mgmt); + + if (NM_IN_STRSET(key_mgmt, "none", "ieee8021x")) + NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_WEP); + else if (nm_streq(key_mgmt, "owe")) + NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_OPEN); + else if (NM_IN_STRSET(key_mgmt, "wpa-psk", "sae")) + NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_PSK); + else if (nm_streq(key_mgmt, "wpa-eap")) + NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_8021X); + else + return FALSE; + + return TRUE; +} diff --git a/src/core/devices/wifi/nm-wifi-utils.h b/src/core/devices/wifi/nm-wifi-utils.h new file mode 100644 index 00000000..474bea41 --- /dev/null +++ b/src/core/devices/wifi/nm-wifi-utils.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#ifndef __NM_WIFI_UTILS_H__ +#define __NM_WIFI_UTILS_H__ + +#include "nm-dbus-interface.h" +#include "nm-connection.h" +#include "nm-setting-wireless.h" +#include "nm-setting-wireless-security.h" +#include "nm-setting-8021x.h" + +typedef enum { + NM_IWD_NETWORK_SECURITY_OPEN, + NM_IWD_NETWORK_SECURITY_WEP, + NM_IWD_NETWORK_SECURITY_PSK, + NM_IWD_NETWORK_SECURITY_8021X, +} NMIwdNetworkSecurity; + +gboolean nm_wifi_utils_complete_connection(GBytes * ssid, + const char * bssid, + NM80211Mode mode, + guint32 ap_freq, + guint32 flags, + guint32 wpa_flags, + guint32 rsn_flags, + NMConnection *connection, + gboolean lock_bssid, + GError ** error); + +gboolean nm_wifi_utils_is_manf_default_ssid(GBytes *ssid); + +gboolean nm_wifi_connection_get_iwd_ssid_and_security(NMConnection * connection, + char ** ssid, + NMIwdNetworkSecurity *security); + +#endif /* __NM_WIFI_UTILS_H__ */ diff --git a/src/core/devices/wifi/tests/test-devices-wifi.c b/src/core/devices/wifi/tests/test-devices-wifi.c new file mode 100644 index 00000000..bc0ba126 --- /dev/null +++ b/src/core/devices/wifi/tests/test-devices-wifi.c @@ -0,0 +1,1609 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "devices/wifi/nm-wifi-utils.h" +#include "devices/wifi/nm-device-wifi.h" +#include "nm-core-internal.h" + +#include "nm-test-utils-core.h" + +#define DEBUG 1 + +/*****************************************************************************/ + +#define COMPARE(src, expected, success, error, edomain, ecode) \ + { \ + if (expected) { \ + if (!success) { \ + g_assert(error != NULL); \ + g_warning("Failed to complete connection: %s", error->message); \ + } \ + g_assert(success == TRUE); \ + g_assert(error == NULL); \ + \ + success = nm_connection_compare(src, expected, NM_SETTING_COMPARE_FLAG_EXACT); \ + if (success == FALSE && DEBUG) { \ + g_print("\n- COMPLETED ---------------------------------\n"); \ + nm_connection_dump(src); \ + g_print("+ EXPECTED ++++++++++++++++++++++++++++++++++++\n"); \ + nm_connection_dump(expected); \ + g_print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n"); \ + } \ + g_assert(success == TRUE); \ + } else { \ + if (success) { \ + g_print("\n- COMPLETED ---------------------------------\n"); \ + nm_connection_dump(src); \ + } \ + g_assert(success == FALSE); \ + g_assert_error(error, edomain, ecode); \ + } \ + \ + g_clear_error(&error); \ + } + +static gboolean +complete_connection(const char * ssid, + const char * bssid, + NM80211Mode mode, + guint32 flags, + guint32 wpa_flags, + guint32 rsn_flags, + gboolean lock_bssid, + NMConnection *src, + GError ** error) +{ + gs_unref_bytes GBytes *ssid_b = NULL; + NMSettingWireless * s_wifi; + + /* Add a wifi setting if one doesn't exist */ + s_wifi = nm_connection_get_setting_wireless(src); + if (!s_wifi) { + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(src, NM_SETTING(s_wifi)); + } + + ssid_b = g_bytes_new(ssid, strlen(ssid)); + + return nm_wifi_utils_complete_connection(ssid_b, + bssid, + mode, + 0, + flags, + wpa_flags, + rsn_flags, + src, + lock_bssid, + error); +} + +typedef struct { + const char *key; + const char *str; + guint32 uint; +} KeyData; + +static void +set_items(NMSetting *setting, const KeyData *items) +{ + const KeyData *item; + GParamSpec * pspec; + GBytes * tmp; + + for (item = items; item && item->key; item++) { + g_assert(item->key); + pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), item->key); + g_assert(pspec); + + if (pspec->value_type == G_TYPE_STRING) { + g_assert(item->uint == 0); + if (item->str) + g_object_set(G_OBJECT(setting), item->key, item->str, NULL); + } else if (pspec->value_type == G_TYPE_UINT) { + g_assert(item->str == NULL); + g_object_set(G_OBJECT(setting), item->key, item->uint, NULL); + } else if (pspec->value_type == G_TYPE_INT) { + int foo = (int) item->uint; + + g_assert(item->str == NULL); + g_object_set(G_OBJECT(setting), item->key, foo, NULL); + } else if (pspec->value_type == G_TYPE_BOOLEAN) { + gboolean foo = !!(item->uint); + + g_assert(item->str == NULL); + g_object_set(G_OBJECT(setting), item->key, foo, NULL); + } else if (pspec->value_type == G_TYPE_BYTES) { + g_assert(item->str); + tmp = g_bytes_new(item->str, strlen(item->str)); + g_object_set(G_OBJECT(setting), item->key, tmp, NULL); + g_bytes_unref(tmp); + } else { + /* Special types, check based on property name */ + if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_PROTO)) + nm_setting_wireless_security_add_proto(NM_SETTING_WIRELESS_SECURITY(setting), + item->str); + else if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_PAIRWISE)) + nm_setting_wireless_security_add_pairwise(NM_SETTING_WIRELESS_SECURITY(setting), + item->str); + else if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_GROUP)) + nm_setting_wireless_security_add_group(NM_SETTING_WIRELESS_SECURITY(setting), + item->str); + else if (!strcmp(item->key, NM_SETTING_802_1X_EAP)) + nm_setting_802_1x_add_eap_method(NM_SETTING_802_1X(setting), item->str); + } + } +} + +static NMSettingWireless * +fill_wifi_empty(NMConnection *connection) +{ + NMSettingWireless *s_wifi; + + s_wifi = nm_connection_get_setting_wireless(connection); + if (!s_wifi) { + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wifi)); + } + return s_wifi; +} + +static NMSettingWireless * +fill_wifi(NMConnection *connection, const KeyData items[]) +{ + NMSettingWireless *s_wifi; + + s_wifi = nm_connection_get_setting_wireless(connection); + if (!s_wifi) { + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wifi)); + } + + set_items(NM_SETTING(s_wifi), items); + return s_wifi; +} + +static NMSettingWirelessSecurity * +fill_wsec(NMConnection *connection, const KeyData items[]) +{ + NMSettingWirelessSecurity *s_wsec; + + s_wsec = nm_connection_get_setting_wireless_security(connection); + if (!s_wsec) { + s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wsec)); + } + + set_items(NM_SETTING(s_wsec), items); + return s_wsec; +} + +static NMSetting8021x * +fill_8021x(NMConnection *connection, const KeyData items[]) +{ + NMSetting8021x *s_8021x; + + s_8021x = nm_connection_get_setting_802_1x(connection); + if (!s_8021x) { + s_8021x = (NMSetting8021x *) nm_setting_802_1x_new(); + nm_connection_add_setting(connection, NM_SETTING(s_8021x)); + } + + set_items(NM_SETTING(s_8021x), items); + return s_8021x; +} + +static NMConnection * +create_basic(const char *ssid, const char *bssid, NM80211Mode mode) +{ + NMConnection * connection; + NMSettingWireless *s_wifi = NULL; + GBytes * tmp; + + connection = nm_simple_connection_new(); + + s_wifi = (NMSettingWireless *) nm_setting_wireless_new(); + nm_connection_add_setting(connection, NM_SETTING(s_wifi)); + + /* SSID */ + tmp = g_bytes_new(ssid, strlen(ssid)); + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_SSID, tmp, NULL); + g_bytes_unref(tmp); + + /* BSSID */ + if (bssid) + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_BSSID, bssid, NULL); + + if (mode == NM_802_11_MODE_INFRA) + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, "infrastructure", NULL); + else if (mode == NM_802_11_MODE_ADHOC) + g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, "adhoc", NULL); + else + g_assert_not_reached(); + + return connection; +} + +/*****************************************************************************/ + +static void +test_lock_bssid(void) +{ + NMConnection *src, *expected; + const char * bssid = "01:02:03:04:05:06"; + const char * ssid = "blahblah"; + gboolean success; + GError * error = NULL; + + src = nm_simple_connection_new(); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + TRUE, + src, + &error); + expected = create_basic(ssid, bssid, NM_802_11_MODE_INFRA); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_open_ap_empty_connection(void) +{ + NMConnection *src, *expected; + const char * bssid = "01:02:03:04:05:06"; + const char * ssid = "blahblah"; + gboolean success; + GError * error = NULL; + + /* Test that an empty source connection is correctly filled with the + * SSID and Infra modes of the given AP details. + */ + + src = nm_simple_connection_new(); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_open_ap_leap_connection_1(gconstpointer add_wifi) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, "Bill Smith", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that a basic connection filled with a LEAP username is + * rejected when completion is attempted with an open AP. LEAP requires + * the AP to have the Privacy bit set. + */ + + src = nm_simple_connection_new(); + if (add_wifi) + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* We expect failure */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_open_ap_leap_connection_2(void) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that a basic connection specifying IEEE8021x security (ie, Dynamic + * WEP or LEAP) is rejected when completion is attempted with an open AP. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* We expect failure */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_open_ap_wep_connection(gconstpointer add_wifi) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = { + {NM_SETTING_WIRELESS_SECURITY_WEP_KEY0, "11111111111111111111111111", 0}, + {NM_SETTING_WIRELESS_SECURITY_WEP_TX_KEYIDX, NULL, 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that a static WEP connection is rejected when completion is + * attempted with an open AP. + */ + + src = nm_simple_connection_new(); + if (add_wifi) + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* We expect failure */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_ap_wpa_psk_connection_base(const char * key_mgmt, + const char * auth_alg, + guint32 flags, + guint32 wpa_flags, + guint32 rsn_flags, + gboolean add_wifi, + guint error_code, + NMConnection *expected) +{ + NMConnection *src; + const char * ssid = "blahblah"; + const char * bssid = "01:02:03:04:05:06"; + const KeyData exp_wifi[] = {{NM_SETTING_WIRELESS_SSID, ssid, 0}, + {NM_SETTING_WIRELESS_MODE, "infrastructure", 0}, + {NULL}}; + const KeyData both_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0}, + {NM_SETTING_WIRELESS_SECURITY_PSK, "asdfasdfasdfasdfasdfafs", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + src = nm_simple_connection_new(); + if (add_wifi) + fill_wifi_empty(src); + fill_wsec(src, both_wsec); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + flags, + wpa_flags, + rsn_flags, + FALSE, + src, + &error); + if (expected) { + fill_wifi(expected, exp_wifi); + fill_wsec(expected, both_wsec); + } + COMPARE(src, expected, success, error, NM_CONNECTION_ERROR, error_code); + + g_object_unref(src); +} + +static void +test_open_ap_wpa_psk_connection_1(void) +{ + /* Test that a WPA-PSK connection filling only the PSK itself and *not* + * filling the wifi setting is rejected when completion is attempted with + * an open AP. + */ + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_SETTING, + NULL); +} + +static void +test_open_ap_wpa_psk_connection_2(void) +{ + /* Test that a WPA-PSK connection filling only the PSK itself and also + * filling the wifi setting is rejected when completion is attempted with + * an open AP. + */ + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + TRUE, + NM_CONNECTION_ERROR_INVALID_SETTING, + NULL); +} + +static void +test_open_ap_wpa_psk_connection_3(void) +{ + /* Test that a WPA-PSK connection filling the PSK and setting the auth alg + * to 'open' is rejected when completion is attempted with an open AP. + */ + test_ap_wpa_psk_connection_base(NULL, + "open", + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_SETTING, + NULL); +} + +static void +test_open_ap_wpa_psk_connection_4(void) +{ + /* Test that a WPA-PSK connection filling the PSK and setting the auth alg + * to 'shared' is rejected when completion is attempted with an open AP. + * Shared auth cannot be used with WPA. + */ + test_ap_wpa_psk_connection_base(NULL, + "shared", + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_SETTING, + NULL); +} + +static void +test_open_ap_wpa_psk_connection_5(void) +{ + /* Test that a WPA-PSK connection filling the PSK, the auth algorithm, and + * key management is rejected when completion is attempted with an open AP. + */ + test_ap_wpa_psk_connection_base("wpa-psk", + "open", + NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_SETTING, + NULL); +} + +/*****************************************************************************/ + +static void +test_ap_wpa_eap_connection_base(const char *key_mgmt, + const char *auth_alg, + guint32 flags, + guint32 wpa_flags, + guint32 rsn_flags, + gboolean add_wifi, + guint error_code) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_empty[] = {{NULL}}; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + src = nm_simple_connection_new(); + if (add_wifi) + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + fill_8021x(src, src_empty); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + flags, + wpa_flags, + rsn_flags, + FALSE, + src, + &error); + /* Failure expected */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, error_code); + + g_object_unref(src); +} + +enum { + IDX_NONE = 0, + IDX_OPEN, + IDX_PRIV, + IDX_WPA_PSK_PTKIP_GTKIP, + IDX_WPA_PSK_PTKIP_PCCMP_GTKIP, + IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP, + IDX_WPA_RSN_PSK_PCCMP_GCCMP, + IDX_RSN_PSK_PCCMP_GCCMP, + IDX_RSN_PSK_PTKIP_PCCMP_GTKIP, + IDX_WPA_8021X, + IDX_RSN_8021X, +}; + +static guint32 +flags_for_idx(guint32 idx) +{ + if (idx == IDX_OPEN) + return NM_802_11_AP_FLAGS_NONE; + else if (idx == IDX_PRIV || idx == IDX_WPA_PSK_PTKIP_GTKIP + || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_RSN_PSK_PCCMP_GCCMP + || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP + || idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP || idx == IDX_WPA_8021X || idx == IDX_RSN_8021X) + return NM_802_11_AP_FLAGS_PRIVACY; + else + g_assert_not_reached(); +} + +static guint32 +wpa_flags_for_idx(guint32 idx) +{ + if (idx == IDX_OPEN || idx == IDX_PRIV || idx == IDX_RSN_8021X || idx == IDX_RSN_PSK_PCCMP_GCCMP + || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP) + return NM_802_11_AP_SEC_NONE; + else if (idx == IDX_WPA_PSK_PTKIP_GTKIP) + return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_TKIP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP) + return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (NM_IN_SET(idx, IDX_WPA_PSK_PTKIP_PCCMP_GTKIP, IDX_WPA_RSN_PSK_PCCMP_GCCMP)) + return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_WPA_8021X) + return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_TKIP + | NM_802_11_AP_SEC_KEY_MGMT_802_1X; + else + g_assert_not_reached(); +} + +static guint32 +rsn_flags_for_idx(guint32 idx) +{ + if (idx == IDX_OPEN || idx == IDX_PRIV || idx == IDX_WPA_8021X || idx == IDX_WPA_PSK_PTKIP_GTKIP + || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP) + return NM_802_11_AP_SEC_NONE; + else if (idx == IDX_RSN_PSK_PCCMP_GCCMP) + return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP) + return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP) + return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP) + return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP + | NM_802_11_AP_SEC_KEY_MGMT_PSK; + else if (idx == IDX_RSN_8021X) + return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP + | NM_802_11_AP_SEC_KEY_MGMT_802_1X; + else + g_assert_not_reached(); +} + +static guint32 +error_code_for_idx(guint32 idx, guint num) +{ + if (idx == IDX_OPEN) + return NM_CONNECTION_ERROR_INVALID_SETTING; + else if (idx == IDX_PRIV) { + if (num <= 3) + return NM_CONNECTION_ERROR_MISSING_PROPERTY; + else + return NM_CONNECTION_ERROR_INVALID_PROPERTY; + } else if (idx == IDX_WPA_PSK_PTKIP_GTKIP || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP + || idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP || idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP + || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_RSN_PSK_PCCMP_GCCMP) + if (num == 4) + return NM_CONNECTION_ERROR_INVALID_PROPERTY; + else + return NM_CONNECTION_ERROR_INVALID_SETTING; + else + g_assert_not_reached(); +} + +static void +test_ap_wpa_eap_connection_1(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + + test_ap_wpa_eap_connection_base(NULL, + NULL, + flags_for_idx(idx), + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + error_code_for_idx(idx, 1)); +} + +static void +test_ap_wpa_eap_connection_2(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + + test_ap_wpa_eap_connection_base(NULL, + NULL, + flags_for_idx(idx), + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + TRUE, + error_code_for_idx(idx, 2)); +} + +static void +test_ap_wpa_eap_connection_3(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + + test_ap_wpa_eap_connection_base(NULL, + "open", + flags_for_idx(idx), + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + error_code_for_idx(idx, 3)); +} + +static void +test_ap_wpa_eap_connection_4(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + + test_ap_wpa_eap_connection_base(NULL, + "shared", + flags_for_idx(idx), + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + error_code_for_idx(idx, 4)); +} + +static void +test_ap_wpa_eap_connection_5(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + + test_ap_wpa_eap_connection_base("wpa-eap", + "open", + flags_for_idx(idx), + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + error_code_for_idx(idx, 5)); +} + +/*****************************************************************************/ + +static void +test_priv_ap_empty_connection(void) +{ + NMConnection *src, *expected; + const char * bssid = "01:02:03:04:05:06"; + const char * ssid = "blahblah"; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "none", 0}, {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that an empty connection is completed to a valid Static WEP + * connection when completed with an AP with the Privacy bit set. + */ + + src = nm_simple_connection_new(); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + + /* Static WEP connection expected */ + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + fill_wsec(expected, exp_wsec); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_priv_ap_leap_connection_1(gconstpointer add_wifi) +{ + NMConnection *src, *expected; + const char * ssid = "blahblah"; + const char * bssid = "01:02:03:04:05:06"; + const char * leap_username = "Bill Smith"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0}, + {NULL}}; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0}, + {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that an minimal LEAP connection specifying only key management and + * the LEAP username is completed to a full LEAP connection when completed + * with an AP with the Privacy bit set. + */ + + src = nm_simple_connection_new(); + if (add_wifi) + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* We expect success here; since LEAP APs just set the 'privacy' flag + * there's no way to determine from the AP's beacon whether it's static WEP, + * dynamic WEP, or LEAP. + */ + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + fill_wsec(expected, exp_wsec); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_priv_ap_leap_connection_2(void) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that an minimal LEAP connection specifying only key management and + * the LEAP auth alg is completed to a full LEAP connection when completed + * with an AP with the Privacy bit set. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* We expect failure here, we need a LEAP username */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_PROPERTY); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_priv_ap_dynamic_wep_1(void) +{ + NMConnection *src, *expected; + const char * ssid = "blahblah"; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + const KeyData both_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0}, + {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0}, + {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0}, + {NULL}}; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that an minimal Dynamic WEP connection specifying key management, + * the auth algorithm, and valid 802.1x setting is completed to a valid + * Dynamic WEP connection when completed with an AP with the Privacy bit set. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + fill_8021x(src, both_8021x); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + + /* We expect a completed Dynamic WEP connection */ + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + fill_wsec(expected, exp_wsec); + fill_8021x(expected, both_8021x); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_priv_ap_dynamic_wep_2(void) +{ + NMConnection *src, *expected; + const char * ssid = "blahblah"; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, {NULL}}; + const KeyData both_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0}, + {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0}, + {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0}, + {NULL}}; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that an minimal Dynamic WEP connection specifying only the auth + * algorithm and a valid 802.1x setting is completed to a valid Dynamic + * WEP connection when completed with an AP with the Privacy bit set. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + fill_8021x(src, both_8021x); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + + /* We expect a completed Dynamic WEP connection */ + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + fill_wsec(expected, exp_wsec); + fill_8021x(expected, both_8021x); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_priv_ap_dynamic_wep_3(void) +{ + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "shared", 0}, {NULL}}; + const KeyData src_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0}, + {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0}, + {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Ensure that a basic connection specifying 'shared' auth and an 802.1x + * setting is rejected, as 802.1x is incompatible with 'shared' auth. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + fill_8021x(src, src_8021x); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + src, + &error); + /* Expect failure; shared is not compatible with dynamic WEP */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_priv_ap_wpa_psk_connection_1(void) +{ + /* Test that a basic WPA-PSK connection is rejected when completion is + * attempted with an AP with just the Privacy bit set. Lack of WPA/RSN + * flags means the AP provides Static/Dynamic WEP or LEAP, not WPA. + */ + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +static void +test_priv_ap_wpa_psk_connection_2(void) +{ + /* Test that a basic WPA-PSK connection is rejected when completion is + * attempted with an AP with just the Privacy bit set. Lack of WPA/RSN + * flags means the AP provides Static/Dynamic WEP or LEAP, not WPA. + */ + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + TRUE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +static void +test_priv_ap_wpa_psk_connection_3(void) +{ + /* Test that a basic WPA-PSK connection specifying only the auth algorithm + * is rejected when completion is attempted with an AP with just the Privacy + * bit set. Lack of WPA/RSN flags means the AP provides Static/Dynamic WEP + * or LEAP, not WPA. + */ + test_ap_wpa_psk_connection_base(NULL, + "open", + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +static void +test_priv_ap_wpa_psk_connection_4(void) +{ + /* Test that a basic WPA-PSK connection specifying only the auth algorithm + * is rejected when completion is attempted with an AP with just the Privacy + * bit set. Lack of WPA/RSN flags means the AP provides Static/Dynamic WEP + * or LEAP, not WPA. Second, 'shared' auth is incompatible with WPA. + */ + test_ap_wpa_psk_connection_base(NULL, + "shared", + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +static void +test_priv_ap_wpa_psk_connection_5(void) +{ + /* Test that a WPA-PSK connection specifying both the key management and + * auth algorithm is rejected when completion is attempted with an AP with + * just the Privacy bit set. Lack of WPA/RSN flags means the AP provides + * Static/Dynamic WEP or LEAP, not WPA. + */ + test_ap_wpa_psk_connection_base("wpa-psk", + "open", + NM_802_11_AP_FLAGS_PRIVACY, + NM_802_11_AP_SEC_NONE, + NM_802_11_AP_SEC_NONE, + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +/*****************************************************************************/ + +static void +test_wpa_ap_empty_connection(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *src, *expected; + const char * bssid = "01:02:03:04:05:06"; + const char * ssid = "blahblah"; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that a basic WPA-PSK connection specifying just key management and + * the auth algorithm is completed successfully when given an AP with WPA + * or RSN flags. + */ + + src = nm_simple_connection_new(); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + src, + &error); + + /* WPA connection expected */ + expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA); + fill_wsec(expected, exp_wsec); + COMPARE(src, expected, success, error, 0, 0); + + g_object_unref(src); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_wpa_ap_leap_connection_1(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *src; + const char * ssid = "blahblah"; + const char * bssid = "01:02:03:04:05:06"; + const char * leap_username = "Bill Smith"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that completion of a LEAP connection with a WPA-enabled AP is + * rejected since WPA APs (usually) do not support LEAP. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection(ssid, + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + src, + &error); + /* Expect failure here; WPA APs don't support old-school LEAP */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_wpa_ap_leap_connection_2(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0}, + {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that completion of a LEAP connection with a WPA-enabled AP is + * rejected since WPA APs (usually) do not support LEAP. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + src, + &error); + /* We expect failure here, we need a LEAP username */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_wpa_ap_dynamic_wep_connection(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *src; + const char * bssid = "01:02:03:04:05:06"; + const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, {NULL}}; + gboolean success; + GError * error = NULL; + + /* Test that completion of a Dynamic WEP connection with a WPA-enabled AP is + * rejected since WPA APs (usually) do not support Dynamic WEP. + */ + + src = nm_simple_connection_new(); + fill_wifi_empty(src); + fill_wsec(src, src_wsec); + success = complete_connection("blahblah", + bssid, + NM_802_11_MODE_INFRA, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + src, + &error); + /* We expect failure here since Dynamic WEP is incompatible with WPA */ + COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY); + + g_object_unref(src); +} + +/*****************************************************************************/ + +static void +test_wpa_ap_wpa_psk_connection_1(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *expected; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + + expected = nm_simple_connection_new(); + fill_wsec(expected, exp_wsec); + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + expected); + g_object_unref(expected); +} + +static void +test_wpa_ap_wpa_psk_connection_2(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *expected; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + + expected = nm_simple_connection_new(); + fill_wsec(expected, exp_wsec); + test_ap_wpa_psk_connection_base(NULL, + NULL, + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + TRUE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + expected); + g_object_unref(expected); +} + +static void +test_wpa_ap_wpa_psk_connection_3(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *expected; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + + expected = nm_simple_connection_new(); + fill_wsec(expected, exp_wsec); + test_ap_wpa_psk_connection_base(NULL, + "open", + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + expected); + g_object_unref(expected); +} + +static void +test_wpa_ap_wpa_psk_connection_4(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + test_ap_wpa_psk_connection_base(NULL, + "shared", + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + NULL); +} + +static void +test_wpa_ap_wpa_psk_connection_5(gconstpointer data) +{ + guint idx = GPOINTER_TO_UINT(data); + NMConnection *expected; + const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0}, + {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, + {NULL}}; + + expected = nm_simple_connection_new(); + fill_wsec(expected, exp_wsec); + test_ap_wpa_psk_connection_base("wpa-psk", + "open", + NM_802_11_AP_FLAGS_PRIVACY, + wpa_flags_for_idx(idx), + rsn_flags_for_idx(idx), + FALSE, + NM_CONNECTION_ERROR_INVALID_PROPERTY, + expected); + g_object_unref(expected); +} + +/*****************************************************************************/ + +static void +test_strength_dbm(void) +{ + /* boundary conditions first */ + g_assert_cmpint(nm_wifi_utils_level_to_quality(-1), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-40), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-30), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-100), ==, 0); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-200), ==, 0); + + g_assert_cmpint(nm_wifi_utils_level_to_quality(-81), ==, 32); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-92), ==, 14); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-74), ==, 44); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-81), ==, 32); + g_assert_cmpint(nm_wifi_utils_level_to_quality(-66), ==, 57); +} + +static void +test_strength_percent(void) +{ + int i; + + /* boundary conditions first */ + g_assert_cmpint(nm_wifi_utils_level_to_quality(0), ==, 0); + g_assert_cmpint(nm_wifi_utils_level_to_quality(100), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(110), ==, 100); + + for (i = 0; i <= 100; i++) + g_assert_cmpint(nm_wifi_utils_level_to_quality(i), ==, i); +} + +static void +test_strength_wext(void) +{ + /* boundary conditions that we assume aren't WEXT first */ + g_assert_cmpint(nm_wifi_utils_level_to_quality(256), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(110), ==, 100); + + /* boundary conditions that we assume are WEXT */ + g_assert_cmpint(nm_wifi_utils_level_to_quality(111), ==, 0); + g_assert_cmpint(nm_wifi_utils_level_to_quality(150), ==, 0); + g_assert_cmpint(nm_wifi_utils_level_to_quality(225), ==, 100); + g_assert_cmpint(nm_wifi_utils_level_to_quality(255), ==, 100); + + g_assert_cmpint(nm_wifi_utils_level_to_quality(157), ==, 2); + g_assert_cmpint(nm_wifi_utils_level_to_quality(200), ==, 74); + g_assert_cmpint(nm_wifi_utils_level_to_quality(215), ==, 99); +} + +#define _assert_strength_in_range(x) \ + ({ \ + guint32 _x = (x); \ + g_assert_cmpint(_x, >=, 0); \ + g_assert_cmpint(_x, <=, 100); \ + }) + +static void +test_strength_all(void) +{ + int val; + + for (val = -200; val < 300; val++) + _assert_strength_in_range(nm_wifi_utils_level_to_quality(val)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT32)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT32)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT16)); + _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT16)); +} + +/*****************************************************************************/ + +static void +do_test_ssids_options_to_ptrarray(const char *const *ssids) +{ + GVariantBuilder builder; + gs_unref_variant GVariant *variant = NULL; + gs_unref_ptrarray GPtrArray *ssids_arr = NULL; + gs_free_error GError *error = NULL; + gsize len; + gsize i; + + g_assert(ssids); + + len = NM_PTRARRAY_LEN(ssids); + + g_variant_builder_init(&builder, G_VARIANT_TYPE("aay")); + for (i = 0; i < len; i++) { + const char *ssid = ssids[i]; + + g_variant_builder_add( + &builder, + "@ay", + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, ssid, strlen(ssid), 1)); + } + variant = g_variant_builder_end(&builder); + + if (nmtst_get_rand_bool()) + g_variant_ref_sink(variant); + + ssids_arr = nmtst_ssids_options_to_ptrarray(variant, &error); + g_assert(!error); + if (len == 0) { + g_assert(!ssids_arr); + return; + } + g_assert_cmpint(len, ==, ssids_arr->len); + for (i = 0; i < len; i++) { + const char *ssid = ssids[i]; + GBytes * bytes = ssids_arr->pdata[i]; + + g_assert(nm_utils_gbytes_equal_mem(bytes, ssid, strlen(ssid))); + } +} + +static void +test_ssids_options_to_ptrarray(void) +{ + do_test_ssids_options_to_ptrarray(NM_PTRARRAY_EMPTY(const char *)); + do_test_ssids_options_to_ptrarray(NM_MAKE_STRV("ab")); + do_test_ssids_options_to_ptrarray(NM_MAKE_STRV("ab", "cd", "fsdfdsf")); +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + gsize i; + + nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT"); + + g_test_add_func("/wifi/lock_bssid", test_lock_bssid); + + /* Open AP tests; make sure that connections to be completed that have + * various security-related settings already set cause the completion + * to fail. + */ + g_test_add_func("/wifi/open_ap/empty_connection", test_open_ap_empty_connection); + g_test_add_data_func("/wifi/open_ap/leap_connection/1", + (gconstpointer) TRUE, + test_open_ap_leap_connection_1); + g_test_add_data_func("/wifi/open_ap/leap_connection/1_no_add_wifi", + (gconstpointer) FALSE, + test_open_ap_leap_connection_1); + g_test_add_func("/wifi/open_ap/leap_connection/2", test_open_ap_leap_connection_2); + g_test_add_data_func("/wifi/open_ap/wep_connection_true", + (gconstpointer) TRUE, + test_open_ap_wep_connection); + g_test_add_data_func("/wifi/open_ap/wep_connection_false", + (gconstpointer) FALSE, + test_open_ap_wep_connection); + + g_test_add_func("/wifi/open_ap/wpa_psk_connection/1", test_open_ap_wpa_psk_connection_1); + g_test_add_func("/wifi/open_ap/wpa_psk_connection/2", test_open_ap_wpa_psk_connection_2); + g_test_add_func("/wifi/open_ap/wpa_psk_connection/3", test_open_ap_wpa_psk_connection_3); + g_test_add_func("/wifi/open_ap/wpa_psk_connection/4", test_open_ap_wpa_psk_connection_4); + g_test_add_func("/wifi/open_ap/wpa_psk_connection/5", test_open_ap_wpa_psk_connection_5); + + g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/1", + (gconstpointer) IDX_OPEN, + test_ap_wpa_eap_connection_1); + g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/2", + (gconstpointer) IDX_OPEN, + test_ap_wpa_eap_connection_2); + g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/3", + (gconstpointer) IDX_OPEN, + test_ap_wpa_eap_connection_3); + g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/4", + (gconstpointer) IDX_OPEN, + test_ap_wpa_eap_connection_4); + g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/5", + (gconstpointer) IDX_OPEN, + test_ap_wpa_eap_connection_5); + + /* WEP AP tests */ + g_test_add_func("/wifi/priv_ap/empty_connection", test_priv_ap_empty_connection); + g_test_add_data_func("/wifi/priv_ap/leap_connection/1", + (gconstpointer) FALSE, + test_priv_ap_leap_connection_1); + g_test_add_func("/wifi/priv_ap/leap_connection/2", test_priv_ap_leap_connection_2); + + g_test_add_func("/wifi/priv_ap/dynamic_wep/1", test_priv_ap_dynamic_wep_1); + g_test_add_func("/wifi/priv_ap/dynamic_wep/2", test_priv_ap_dynamic_wep_2); + g_test_add_func("/wifi/priv_ap/dynamic_wep/3", test_priv_ap_dynamic_wep_3); + + g_test_add_func("/wifi/priv_ap/wpa_psk_connection/1", test_priv_ap_wpa_psk_connection_1); + g_test_add_func("/wifi/priv_ap/wpa_psk_connection/2", test_priv_ap_wpa_psk_connection_2); + g_test_add_func("/wifi/priv_ap/wpa_psk_connection/3", test_priv_ap_wpa_psk_connection_3); + g_test_add_func("/wifi/priv_ap/wpa_psk_connection/4", test_priv_ap_wpa_psk_connection_4); + g_test_add_func("/wifi/priv_ap/wpa_psk_connection/5", test_priv_ap_wpa_psk_connection_5); + + g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/1", + (gconstpointer) IDX_PRIV, + test_ap_wpa_eap_connection_1); + g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/2", + (gconstpointer) IDX_PRIV, + test_ap_wpa_eap_connection_2); + g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/3", + (gconstpointer) IDX_PRIV, + test_ap_wpa_eap_connection_3); + g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/4", + (gconstpointer) IDX_PRIV, + test_ap_wpa_eap_connection_4); + g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/5", + (gconstpointer) IDX_PRIV, + test_ap_wpa_eap_connection_5); + +#define ADD_FUNC(func) \ + do { \ + char *name_idx = g_strdup_printf("/wifi/wpa_psk/" G_STRINGIFY(func) "/%zd", i); \ + g_test_add_data_func(name_idx, (gconstpointer) i, func); \ + g_free(name_idx); \ + } while (0) + + /* WPA-PSK tests */ + for (i = IDX_WPA_PSK_PTKIP_GTKIP; i <= IDX_WPA_RSN_PSK_PCCMP_GCCMP; i++) { + ADD_FUNC(test_wpa_ap_empty_connection); + ADD_FUNC(test_wpa_ap_leap_connection_1); + ADD_FUNC(test_wpa_ap_leap_connection_2); + ADD_FUNC(test_wpa_ap_dynamic_wep_connection); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_1); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_2); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_3); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_4); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_5); + ADD_FUNC(test_ap_wpa_eap_connection_1); + ADD_FUNC(test_ap_wpa_eap_connection_2); + ADD_FUNC(test_ap_wpa_eap_connection_3); + ADD_FUNC(test_ap_wpa_eap_connection_4); + ADD_FUNC(test_ap_wpa_eap_connection_5); + } + +#undef ADD_FUNC +#define ADD_FUNC(func) \ + do { \ + char *name_idx = g_strdup_printf("/wifi/rsn_psk/" G_STRINGIFY(func) "/%zd", i); \ + g_test_add_data_func(name_idx, (gconstpointer) i, func); \ + g_free(name_idx); \ + } while (0) + + /* RSN-PSK tests */ + for (i = IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP; i <= IDX_RSN_PSK_PTKIP_PCCMP_GTKIP; i++) { + ADD_FUNC(test_wpa_ap_empty_connection); + ADD_FUNC(test_wpa_ap_leap_connection_1); + ADD_FUNC(test_wpa_ap_leap_connection_2); + ADD_FUNC(test_wpa_ap_dynamic_wep_connection); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_1); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_2); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_3); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_4); + ADD_FUNC(test_wpa_ap_wpa_psk_connection_5); + ADD_FUNC(test_ap_wpa_eap_connection_1); + ADD_FUNC(test_ap_wpa_eap_connection_2); + ADD_FUNC(test_ap_wpa_eap_connection_3); + ADD_FUNC(test_ap_wpa_eap_connection_4); + ADD_FUNC(test_ap_wpa_eap_connection_5); + } + +#undef ADD_FUNC + + /* Scanned signal strength conversion tests */ + g_test_add_func("/wifi/strength/dbm", test_strength_dbm); + g_test_add_func("/wifi/strength/percent", test_strength_percent); + g_test_add_func("/wifi/strength/wext", test_strength_wext); + g_test_add_func("/wifi/strength/all", test_strength_all); + + g_test_add_func("/wifi/ssids_options_to_ptrarray", test_ssids_options_to_ptrarray); + + return g_test_run(); +} diff --git a/src/core/devices/wwan/libnm-wwan.ver b/src/core/devices/wwan/libnm-wwan.ver new file mode 100644 index 00000000..c368a590 --- /dev/null +++ b/src/core/devices/wwan/libnm-wwan.ver @@ -0,0 +1,41 @@ +{ +global: + nm_modem_act_stage1_prepare; + nm_modem_act_stage2_config; + nm_modem_check_connection_compatible; + nm_modem_claim; + nm_modem_complete_connection; + nm_modem_deactivate; + nm_modem_deactivate_async; + nm_modem_device_state_changed; + nm_modem_get_apn; + nm_modem_get_capabilities; + nm_modem_get_configured_mtu; + nm_modem_get_control_port; + nm_modem_get_device_id; + nm_modem_get_driver; + nm_modem_get_iid; + nm_modem_get_ip_ifindex; + nm_modem_get_operator_code; + nm_modem_get_path; + nm_modem_get_secrets; + nm_modem_get_state; + nm_modem_get_type; + nm_modem_get_uid; + nm_modem_ip4_pre_commit; + nm_modem_is_claimed; + nm_modem_manager_get; + nm_modem_manager_get_modems; + nm_modem_manager_get_type; + nm_modem_manager_name_owner_get; + nm_modem_manager_name_owner_ref; + nm_modem_manager_name_owner_unref; + nm_modem_owns_port; + nm_modem_set_mm_enabled; + nm_modem_stage3_ip4_config_start; + nm_modem_stage3_ip6_config_start; + nm_modem_state_to_string; + nm_modem_unclaim; +local: + *; +}; diff --git a/src/core/devices/wwan/meson.build b/src/core/devices/wwan/meson.build new file mode 100644 index 00000000..87af0429 --- /dev/null +++ b/src/core/devices/wwan/meson.build @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +wwan_inc = include_directories('.') + +linker_script = join_paths(meson.current_source_dir(), 'libnm-wwan.ver') + +libnm_wwan = shared_module( + 'nm-wwan', + sources: files( + 'nm-service-providers.c', + 'nm-modem-broadband.c', + 'nm-modem.c', + 'nm-modem-manager.c', + ) + (enable_ofono ? files('nm-modem-ofono.c') : files()), + dependencies: [ + core_plugin_dep, + libsystemd_dep, + mm_glib_dep, + ], + c_args: daemon_c_flags, + link_args: '-Wl,--version-script,@0@'.format(linker_script), + link_depends: linker_script, + install: true, + install_dir: nm_plugindir, +) + +libnm_wwan_dep = declare_dependency( + include_directories: wwan_inc, + link_with: libnm_wwan, +) + +core_plugins += libnm_wwan + +test( + 'check-wwan', + check_exports, + args: [ + libnm_wwan.full_path(), + linker_script, + ], +) + +libnm_device_plugin_wwan = shared_module( + 'nm-device-plugin-wwan', + sources: files( + 'nm-device-modem.c', + 'nm-wwan-factory.c', + ), + dependencies: [ + core_plugin_dep, + libsystemd_dep, + mm_glib_dep, + ], + c_args: daemon_c_flags, + link_with: libnm_wwan, + link_args: ldflags_linker_script_devices, + link_depends: linker_script_devices, + install: true, + install_dir: nm_plugindir, + install_rpath: nm_plugindir, +) + +core_plugins += libnm_device_plugin_wwan + +run_target( + 'check-local-devices-wwan', + command: [check_exports, libnm_device_plugin_wwan.full_path(), linker_script_devices], + depends: libnm_device_plugin_wwan, +) + +if enable_tests + exe = executable( + 'test-service-providers', + files( + 'tests/test-service-providers.c', + 'nm-service-providers.c', + ), + include_directories: wwan_inc, + dependencies: libNetworkManagerTest_dep, + c_args: test_c_flags, + ) + test( + 'wwan/test-service-providers', + test_script, + timeout: default_test_timeout, + args: test_args + [exe.full_path()], + ) +endif diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c new file mode 100644 index 00000000..3ea89d2c --- /dev/null +++ b/src/core/devices/wwan/nm-device-modem.c @@ -0,0 +1,957 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2019 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-device-modem.h" + +#include "nm-modem.h" +#include "nm-ip4-config.h" +#include "devices/nm-device-private.h" +#include "nm-rfkill-manager.h" +#include "settings/nm-settings-connection.h" +#include "nm-modem-broadband.h" +#include "NetworkManagerUtils.h" +#include "nm-core-internal.h" + +#define _NMLOG_DEVICE_TYPE NMDeviceModem +#include "devices/nm-device-logging.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceModem, + PROP_MODEM, + PROP_CAPABILITIES, + PROP_CURRENT_CAPABILITIES, + PROP_DEVICE_ID, + PROP_OPERATOR_CODE, + PROP_APN, ); + +typedef struct { + NMModem * modem; + NMDeviceModemCapabilities caps; + NMDeviceModemCapabilities current_caps; + char * device_id; + char * operator_code; + char * apn; + bool rf_enabled : 1; + NMDeviceStageState stage1_state : 3; +} NMDeviceModemPrivate; + +struct _NMDeviceModem { + NMDevice parent; + NMDeviceModemPrivate _priv; +}; + +struct _NMDeviceModemClass { + NMDeviceClass parent; +}; + +G_DEFINE_TYPE(NMDeviceModem, nm_device_modem, NM_TYPE_DEVICE) + +#define NM_DEVICE_MODEM_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDeviceModem, NM_IS_DEVICE_MODEM, NMDevice) + +/*****************************************************************************/ + +static void +ppp_failed(NMModem *modem, guint i_reason, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceModem * self = NM_DEVICE_MODEM(user_data); + NMDeviceStateReason reason = i_reason; + + switch (nm_device_get_state(device)) { + case NM_DEVICE_STATE_PREPARE: + case NM_DEVICE_STATE_CONFIG: + case NM_DEVICE_STATE_NEED_AUTH: + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason); + break; + case NM_DEVICE_STATE_IP_CONFIG: + case NM_DEVICE_STATE_IP_CHECK: + case NM_DEVICE_STATE_SECONDARIES: + case NM_DEVICE_STATE_ACTIVATED: + if (nm_device_activate_ip4_state_in_conf(device)) + nm_device_activate_schedule_ip_config_timeout(device, AF_INET); + else if (nm_device_activate_ip6_state_in_conf(device)) + nm_device_activate_schedule_ip_config_timeout(device, AF_INET6); + else if (nm_device_activate_ip4_state_done(device)) { + nm_device_ip_method_failed(device, + AF_INET, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else if (nm_device_activate_ip6_state_done(device)) { + nm_device_ip_method_failed(device, + AF_INET6, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else { + _LOGW(LOGD_MB, + "PPP failure in unexpected state %u", + (guint) nm_device_get_state(device)); + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } + break; + default: + break; + } +} + +static void +modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(user_data); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + NMDevice * device = NM_DEVICE(self); + NMDeviceStateReason reason = i_reason; + + if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE + || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING) { + nm_assert_not_reached(); + success = FALSE; + } + + if (!success) { + /* There are several reasons to block autoconnection at device level: + * + * - Wrong SIM-PIN: The device won't autoconnect because it doesn't make sense + * to retry the connection with the same PIN. This error also makes autoconnection + * blocked at settings level, so not even a modem unplug and replug will allow + * autoconnection again. It is somewhat redundant to block autoconnection at + * both device and setting level really. + * + * - SIM wrong or not inserted: If the modem is reporting a SIM not inserted error, + * we can block autoconnection at device level, so that if the same device is + * unplugged and replugged with a SIM (or if a SIM hotplug event happens in MM, + * recreating the device completely), we can try the autoconnection again. + * + * - Modem initialization failed: For some reason unknown to NM, the modem wasn't + * initialized correctly, which leads to an unusable device. A device unplug and + * replug may solve the issue, so make it a device-level autoconnection blocking + * reason. + */ + switch (nm_device_state_reason_check(reason)) { + case NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED: + case NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED: + case NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT: + nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN); + break; + case NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED: + case NM_DEVICE_STATE_REASON_GSM_SIM_WRONG: + nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_SIM_MISSING); + break; + case NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED: + nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_INIT_FAILED); + break; + default: + break; + } + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason); + return; + } + + priv->stage1_state = NM_DEVICE_STAGE_STATE_COMPLETED; + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +modem_auth_requested(NMModem *modem, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled + * during activation. + */ + if (!nm_device_is_activating(device)) + return; + + nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE); +} + +static void +modem_auth_result(NMModem *modem, GError *error, gpointer user_data) +{ + NMDevice * device = NM_DEVICE(user_data); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH); + + if (error) { + nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS); + return; + } + + priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT; + nm_device_activate_schedule_stage1_device_prepare(device, FALSE); +} + +static void +modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data) +{ + NMDeviceModem *self = NM_DEVICE_MODEM(user_data); + NMDevice * device = NM_DEVICE(self); + + g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE); + + if (error) { + _LOGW(LOGD_MB | LOGD_IP4, "retrieving IPv4 configuration failed: %s", error->message); + nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else { + nm_device_set_dev2_ip_config(device, AF_INET, NM_IP_CONFIG_CAST(config)); + nm_device_activate_schedule_ip_config_result(device, AF_INET, NULL); + } +} + +static void +modem_ip6_config_result(NMModem * modem, + NMIP6Config *config, + gboolean do_slaac, + GError * error, + gpointer user_data) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(user_data); + NMDevice * device = NM_DEVICE(self); + NMActStageReturn ret; + NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE; + gs_unref_object NMIP6Config *ignored = NULL; + gboolean got_config = !!config; + + g_return_if_fail(nm_device_activate_ip6_state_in_conf(device) == TRUE); + + if (error) { + _LOGW(LOGD_MB | LOGD_IP6, "retrieving IPv6 configuration failed: %s", error->message); + nm_device_ip_method_failed(device, AF_INET6, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return; + } + + /* Re-enable IPv6 on the interface */ + nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "0"); + + if (config) + nm_device_set_dev2_ip_config(device, AF_INET6, NM_IP_CONFIG_CAST(config)); + + if (do_slaac == FALSE) { + if (got_config) + nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL); + else { + _LOGW(LOGD_MB | LOGD_IP6, + "retrieving IPv6 configuration failed: SLAAC not requested and no addresses"); + nm_device_ip_method_failed(device, + AF_INET6, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } + return; + } + + /* Start SLAAC now that we have a link-local address from the modem */ + ret = + NM_DEVICE_CLASS(nm_device_modem_parent_class) + ->act_stage3_ip_config_start(device, AF_INET6, (gpointer *) &ignored, &failure_reason); + + nm_assert(ignored == NULL); + + switch (ret) { + case NM_ACT_STAGE_RETURN_FAILURE: + nm_device_ip_method_failed(device, AF_INET6, failure_reason); + break; + case NM_ACT_STAGE_RETURN_IP_FAIL: + /* all done */ + nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL); + break; + case NM_ACT_STAGE_RETURN_POSTPONE: + /* let SLAAC run */ + break; + default: + /* Should never get here since we've assured that the IPv6 method + * will either be "auto" or "ignored" when starting IPv6 configuration. + */ + nm_assert_not_reached(); + } +} + +static void +ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(user_data); + + if (!nm_device_is_activating(device)) + return; + + if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return; + } + + /* Disable IPv6 immediately on the interface since NM handles IPv6 + * internally, and leaving it enabled could allow the kernel's IPv6 + * RA handling code to run before NM is ready. + */ + nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "1"); +} + +static void +operator_code_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(user_data); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + const char * operator_code = nm_modem_get_operator_code(modem); + + if (g_strcmp0(priv->operator_code, operator_code) != 0) { + g_free(priv->operator_code); + priv->operator_code = g_strdup(operator_code); + _notify(self, PROP_OPERATOR_CODE); + } +} + +static void +apn_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(user_data); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + const char * apn = nm_modem_get_apn(modem); + + if (g_strcmp0(priv->apn, apn) != 0) { + g_free(priv->apn); + priv->apn = g_strdup(apn); + _notify(self, PROP_APN); + } +} + +static void +ids_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + nm_device_recheck_available_connections(NM_DEVICE(user_data)); +} + +static void +modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data) +{ + NMModemState new_state = new_state_i; + NMModemState old_state = old_state_i; + NMDevice * device = NM_DEVICE(user_data); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + NMDeviceState dev_state = nm_device_get_state(device); + + if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING + && priv->rf_enabled) { + /* Called when the ModemManager modem enabled state is changed externally + * to NetworkManager (eg something using MM's D-Bus API directly). + */ + if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) { + /* user-initiated action, hence DISCONNECTED not FAILED */ + nm_device_state_changed(device, + NM_DEVICE_STATE_DISCONNECTED, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + return; + } + } + + if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING + && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) { + /* Fail the device if the modem disconnects unexpectedly while the + * device is activating/activated. */ + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER); + return; + } + + if (new_state > NM_MODEM_STATE_LOCKED && old_state == NM_MODEM_STATE_LOCKED) { + /* If the modem is now unlocked, enable/disable it according to the + * device's enabled/disabled state. + */ + nm_modem_set_mm_enabled(priv->modem, priv->rf_enabled); + + if (dev_state == NM_DEVICE_STATE_NEED_AUTH) { + /* The modem was unlocked externally to NetworkManager, + * deactivate so the default connection can be + * automatically activated again */ + nm_device_state_changed(device, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_MODEM_AVAILABLE); + } + + /* Now allow connections without a PIN to be available */ + nm_device_recheck_available_connections(device); + } + + nm_device_queue_recheck_available(device, + NM_DEVICE_STATE_REASON_MODEM_AVAILABLE, + NM_DEVICE_STATE_REASON_MODEM_FAILED); +} + +static void +modem_removed_cb(NMModem *modem, gpointer user_data) +{ + g_signal_emit_by_name(NM_DEVICE(user_data), NM_DEVICE_REMOVED); +} + +/*****************************************************************************/ + +static gboolean +owns_iface(NMDevice *device, const char *iface) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + g_return_val_if_fail(priv->modem, FALSE); + + return nm_modem_owns_port(priv->modem, iface); +} + +/*****************************************************************************/ + +static void +device_state_changed(NMDevice * device, + NMDeviceState new_state, + NMDeviceState old_state, + NMDeviceStateReason reason) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(device); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + + g_return_if_fail(priv->modem); + + if (new_state == NM_DEVICE_STATE_UNAVAILABLE && old_state < NM_DEVICE_STATE_UNAVAILABLE) { + /* Log initial modem state */ + _LOGI(LOGD_MB, + "modem state '%s'", + nm_modem_state_to_string(nm_modem_get_state(priv->modem))); + } + nm_modem_device_state_changed(priv->modem, new_state, old_state); +} + +static NMDeviceCapabilities +get_generic_capabilities(NMDevice *device) +{ + return NM_DEVICE_CAP_IS_NON_KERNEL; +} + +static const char * +get_type_description(NMDevice *device) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS)) + return "gsm"; + if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO)) + return "cdma"; + return NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_type_description(device); +} + +static gboolean +check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error) +{ + GError *local = NULL; + + if (!NM_DEVICE_CLASS(nm_device_modem_parent_class) + ->check_connection_compatible(device, connection, error)) + return FALSE; + + if (!nm_modem_check_connection_compatible(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem, + connection, + error ? &local : NULL)) { + if (error) { + g_set_error(error, + NM_UTILS_ERROR, + g_error_matches(local, + NM_UTILS_ERROR, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE) + ? NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE + : NM_UTILS_ERROR_UNKNOWN, + "modem is incompatible with connection: %s", + local->message); + g_error_free(local); + } + return FALSE; + } + return TRUE; +} + +static gboolean +check_connection_available(NMDevice * device, + NMConnection * connection, + NMDeviceCheckConAvailableFlags flags, + const char * specific_object, + GError ** error) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(device); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + NMModemState state; + + if (!priv->rf_enabled) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "RFKILL for modem enabled"); + return FALSE; + } + + if (!priv->modem) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "modem not available"); + return FALSE; + } + + state = nm_modem_get_state(priv->modem); + if (state <= NM_MODEM_STATE_INITIALIZING) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "modem not initialized"); + return FALSE; + } + + if (state == NM_MODEM_STATE_LOCKED) { + if (!nm_connection_get_setting_gsm(connection)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "modem is locked without pin available"); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +complete_connection(NMDevice * device, + NMConnection * connection, + const char * specific_object, + NMConnection *const *existing_connections, + GError ** error) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + return nm_modem_complete_connection(priv->modem, + nm_device_get_iface(device), + connection, + existing_connections, + error); +} + +static void +deactivate(NMDevice *device) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + nm_modem_deactivate(priv->modem, device); + priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT; +} + +/*****************************************************************************/ + +static void +modem_deactivate_async_cb(NMModem *modem, GError *error, gpointer user_data) +{ + gs_unref_object NMDevice * self = NULL; + NMDeviceDeactivateCallback callback; + gpointer callback_user_data; + + nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data); + callback(self, error, callback_user_data); +} + +static void +deactivate_async(NMDevice * self, + GCancellable * cancellable, + NMDeviceDeactivateCallback callback, + gpointer user_data) +{ + nm_assert(G_IS_CANCELLABLE(cancellable)); + nm_assert(callback); + + nm_modem_deactivate_async(NM_DEVICE_MODEM_GET_PRIVATE(self)->modem, + self, + cancellable, + modem_deactivate_async_cb, + nm_utils_user_data_pack(g_object_ref(self), callback, user_data)); +} + +/*****************************************************************************/ + +static NMActStageReturn +act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + NMActRequest * req; + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + if (priv->stage1_state == NM_DEVICE_STAGE_STATE_INIT) { + priv->stage1_state = NM_DEVICE_STAGE_STATE_PENDING; + return nm_modem_act_stage1_prepare(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem, + req, + out_failure_reason); + } + + if (priv->stage1_state == NM_DEVICE_STAGE_STATE_PENDING) + return NM_ACT_STAGE_RETURN_POSTPONE; + + nm_assert(priv->stage1_state == NM_DEVICE_STAGE_STATE_COMPLETED); + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason) +{ + nm_modem_act_stage2_config(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem); + return NM_ACT_STAGE_RETURN_SUCCESS; +} + +static NMActStageReturn +act_stage3_ip_config_start(NMDevice * device, + int addr_family, + gpointer * out_config, + NMDeviceStateReason *out_failure_reason) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + + nm_assert_addr_family(addr_family); + + if (addr_family == AF_INET) { + return nm_modem_stage3_ip4_config_start(priv->modem, + device, + NM_DEVICE_CLASS(nm_device_modem_parent_class), + out_failure_reason); + } else { + return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason); + } +} + +static void +ip4_config_pre_commit(NMDevice *device, NMIP4Config *config) +{ + nm_modem_ip4_pre_commit(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem, device, config); +} + +static gboolean +get_ip_iface_identifier(NMDevice *device, NMUtilsIPv6IfaceId *out_iid) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(device); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + gboolean success; + + g_return_val_if_fail(priv->modem, FALSE); + success = nm_modem_get_iid(priv->modem, out_iid); + if (!success) + success = + NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_ip_iface_identifier(device, out_iid); + return success; +} + +/*****************************************************************************/ + +static gboolean +get_enabled(NMDevice *device) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device); + NMModemState modem_state = nm_modem_get_state(priv->modem); + + return priv->rf_enabled && (modem_state >= NM_MODEM_STATE_LOCKED); +} + +static void +set_enabled(NMDevice *device, gboolean enabled) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(device); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + + /* Called only by the Manager in response to rfkill switch changes or + * global user WWAN enable/disable preference changes. + */ + priv->rf_enabled = enabled; + + if (priv->modem) { + /* Sync the ModemManager modem enabled/disabled with rfkill/user preference */ + nm_modem_set_mm_enabled(priv->modem, enabled); + } + + if (enabled == FALSE) { + nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE); + } +} + +static gboolean +is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags) +{ + NMDeviceModem * self = NM_DEVICE_MODEM(device); + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + NMModemState modem_state; + + if (!priv->rf_enabled) + return FALSE; + + g_assert(priv->modem); + modem_state = nm_modem_get_state(priv->modem); + if (modem_state <= NM_MODEM_STATE_INITIALIZING) + return FALSE; + + return TRUE; +} + +/*****************************************************************************/ + +static void +set_modem(NMDeviceModem *self, NMModem *modem) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self); + + g_return_if_fail(modem != NULL); + + priv->modem = nm_modem_claim(modem); + + g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self); + g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self); + g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self); + g_signal_connect(modem, NM_MODEM_IP6_CONFIG_RESULT, G_CALLBACK(modem_ip6_config_result), self); + g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self); + g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self); + g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self); + g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self); + + g_signal_connect(modem, + "notify::" NM_MODEM_IP_IFINDEX, + G_CALLBACK(ip_ifindex_changed_cb), + self); + g_signal_connect(modem, "notify::" NM_MODEM_DEVICE_ID, G_CALLBACK(ids_changed_cb), self); + g_signal_connect(modem, "notify::" NM_MODEM_SIM_ID, G_CALLBACK(ids_changed_cb), self); + g_signal_connect(modem, "notify::" NM_MODEM_SIM_OPERATOR_ID, G_CALLBACK(ids_changed_cb), self); + g_signal_connect(modem, + "notify::" NM_MODEM_OPERATOR_CODE, + G_CALLBACK(operator_code_changed_cb), + self); + g_signal_connect(modem, "notify::" NM_MODEM_APN, G_CALLBACK(apn_changed_cb), self); +} + +static guint32 +get_dhcp_timeout_for_device(NMDevice *device, int addr_family) +{ + /* DHCP is always done by the modem firmware, not by the network, and + * by the time we get around to DHCP the firmware should already know + * the IP addressing details. So the DHCP timeout can be much shorter. + */ + return 15; +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_MODEM: + g_value_set_object(value, priv->modem); + break; + case PROP_CAPABILITIES: + g_value_set_uint(value, priv->caps); + break; + case PROP_CURRENT_CAPABILITIES: + g_value_set_uint(value, priv->current_caps); + break; + case PROP_DEVICE_ID: + g_value_set_string(value, priv->device_id); + break; + case PROP_OPERATOR_CODE: + g_value_set_string(value, priv->operator_code); + break; + case PROP_APN: + g_value_set_string(value, priv->apn); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_MODEM: + /* construct-only */ + set_modem(NM_DEVICE_MODEM(object), g_value_get_object(value)); + break; + case PROP_CAPABILITIES: + priv->caps = g_value_get_uint(value); + break; + case PROP_CURRENT_CAPABILITIES: + priv->current_caps = g_value_get_uint(value); + break; + case PROP_DEVICE_ID: + /* construct-only */ + priv->device_id = g_value_dup_string(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_device_modem_init(NMDeviceModem *self) +{} + +NMDevice * +nm_device_modem_new(NMModem *modem) +{ + NMDeviceModemCapabilities caps = NM_DEVICE_MODEM_CAPABILITY_NONE; + NMDeviceModemCapabilities current_caps = NM_DEVICE_MODEM_CAPABILITY_NONE; + + g_return_val_if_fail(NM_IS_MODEM(modem), NULL); + + /* Load capabilities */ + nm_modem_get_capabilities(modem, &caps, ¤t_caps); + + return g_object_new(NM_TYPE_DEVICE_MODEM, + NM_DEVICE_UDI, + nm_modem_get_path(modem), + NM_DEVICE_IFACE, + nm_modem_get_uid(modem), + NM_DEVICE_DRIVER, + nm_modem_get_driver(modem), + NM_DEVICE_TYPE_DESC, + "Broadband", + NM_DEVICE_DEVICE_TYPE, + NM_DEVICE_TYPE_MODEM, + NM_DEVICE_RFKILL_TYPE, + RFKILL_TYPE_WWAN, + NM_DEVICE_MODEM_MODEM, + modem, + NM_DEVICE_MODEM_CAPABILITIES, + caps, + NM_DEVICE_MODEM_CURRENT_CAPABILITIES, + current_caps, + NM_DEVICE_MODEM_DEVICE_ID, + nm_modem_get_device_id(modem), + NULL); +} + +static void +dispose(GObject *object) +{ + NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object); + + if (priv->modem) { + g_signal_handlers_disconnect_by_data(priv->modem, NM_DEVICE_MODEM(object)); + nm_clear_pointer(&priv->modem, nm_modem_unclaim); + } + + nm_clear_g_free(&priv->device_id); + nm_clear_g_free(&priv->operator_code); + nm_clear_g_free(&priv->apn); + + G_OBJECT_CLASS(nm_device_modem_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_device_modem = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_DEVICE_MODEM, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ModemCapabilities", + "u", + NM_DEVICE_MODEM_CAPABILITIES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("CurrentCapabilities", + "u", + NM_DEVICE_MODEM_CURRENT_CAPABILITIES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DeviceId", + "s", + NM_DEVICE_MODEM_DEVICE_ID), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("OperatorCode", + "s", + NM_DEVICE_MODEM_OPERATOR_CODE), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Apn", "s", NM_DEVICE_MODEM_APN), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_device_modem_class_init(NMDeviceModemClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass); + NMDeviceClass * device_class = NM_DEVICE_CLASS(klass); + + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_modem); + + device_class->get_generic_capabilities = get_generic_capabilities; + device_class->get_type_description = get_type_description; + device_class->check_connection_compatible = check_connection_compatible; + device_class->check_connection_available = check_connection_available; + device_class->complete_connection = complete_connection; + device_class->deactivate_async = deactivate_async; + device_class->deactivate = deactivate; + device_class->act_stage1_prepare = act_stage1_prepare; + device_class->act_stage2_config = act_stage2_config; + device_class->act_stage3_ip_config_start = act_stage3_ip_config_start; + device_class->ip4_config_pre_commit = ip4_config_pre_commit; + device_class->get_enabled = get_enabled; + device_class->set_enabled = set_enabled; + device_class->owns_iface = owns_iface; + device_class->is_available = is_available; + device_class->get_ip_iface_identifier = get_ip_iface_identifier; + device_class->get_configured_mtu = nm_modem_get_configured_mtu; + device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device; + + device_class->state_changed = device_state_changed; + + obj_properties[PROP_MODEM] = + g_param_spec_object(NM_DEVICE_MODEM_MODEM, + "", + "", + NM_TYPE_MODEM, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_MODEM_CAPABILITIES, + "", + "", + 0, + G_MAXUINT32, + NM_DEVICE_MODEM_CAPABILITY_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CURRENT_CAPABILITIES] = + g_param_spec_uint(NM_DEVICE_MODEM_CURRENT_CAPABILITIES, + "", + "", + 0, + G_MAXUINT32, + NM_DEVICE_MODEM_CAPABILITY_NONE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DEVICE_ID] = + g_param_spec_string(NM_DEVICE_MODEM_DEVICE_ID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_OPERATOR_CODE] = + g_param_spec_string(NM_DEVICE_MODEM_OPERATOR_CODE, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_APN] = g_param_spec_string(NM_DEVICE_MODEM_APN, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wwan/nm-device-modem.h b/src/core/devices/wwan/nm-device-modem.h new file mode 100644 index 00000000..f171d76f --- /dev/null +++ b/src/core/devices/wwan/nm-device-modem.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DEVICE_MODEM_H__ +#define __NETWORKMANAGER_DEVICE_MODEM_H__ + +#include "devices/nm-device.h" +#include "nm-modem.h" + +#define NM_TYPE_DEVICE_MODEM (nm_device_modem_get_type()) +#define NM_DEVICE_MODEM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModem)) +#define NM_DEVICE_MODEM_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass)) +#define NM_IS_DEVICE_MODEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MODEM)) +#define NM_IS_DEVICE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MODEM)) +#define NM_DEVICE_MODEM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass)) + +#define NM_DEVICE_MODEM_MODEM "modem" +#define NM_DEVICE_MODEM_CAPABILITIES "modem-capabilities" +#define NM_DEVICE_MODEM_CURRENT_CAPABILITIES "current-capabilities" +#define NM_DEVICE_MODEM_DEVICE_ID "device-id" +#define NM_DEVICE_MODEM_OPERATOR_CODE "operator-code" +#define NM_DEVICE_MODEM_APN "apn" + +typedef struct _NMDeviceModem NMDeviceModem; +typedef struct _NMDeviceModemClass NMDeviceModemClass; + +GType nm_device_modem_get_type(void); + +NMDevice *nm_device_modem_new(NMModem *modem); + +#endif /* __NETWORKMANAGER_DEVICE_MODEM_H__ */ diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c new file mode 100644 index 00000000..ca028804 --- /dev/null +++ b/src/core/devices/wwan/nm-modem-broadband.c @@ -0,0 +1,1625 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2012 Aleksander Morgado <aleksander@gnu.org> + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-modem-broadband.h" +#include "nm-service-providers.h" + +#include <arpa/inet.h> +#include <libmm-glib.h> + +#include "nm-core-internal.h" +#include "NetworkManagerUtils.h" +#include "devices/nm-device-private.h" +#include "platform/nm-platform.h" +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" + +#define NM_MODEM_BROADBAND_MODEM "modem" + +static gboolean +MODEM_CAPS_3GPP(MMModemCapability caps) +{ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + /* MM_MODEM_CAPABILITY_LTE_ADVANCED is marked as deprecated since ModemManager 1.14.0. + * + * The flag probably was never used, it certainly isn't used since 1.14.0. + * + * Still, just to be sure, there is no harm in checking it here. Suppress the + * warning, it should have no bad effect. + */ + return NM_FLAGS_ANY(caps, + (MM_MODEM_CAPABILITY_GSM_UMTS | MM_MODEM_CAPABILITY_LTE + | MM_MODEM_CAPABILITY_LTE_ADVANCED)); + G_GNUC_END_IGNORE_DEPRECATIONS +} + +#define MODEM_CAPS_3GPP2(caps) (caps & (MM_MODEM_CAPABILITY_CDMA_EVDO)) + +/* Maximum time to keep the DBus call waiting for a connection result. + * This value is greater than the default timeout in ModemManager (180s since + * 1.16), so that whenever possible the timeout happens first there instead of + * in NetworkManager. */ +#define MODEM_CONNECT_TIMEOUT_SECS 200 + +/*****************************************************************************/ + +typedef enum { + CONNECT_STEP_FIRST, + CONNECT_STEP_WAIT_FOR_SIM, + CONNECT_STEP_UNLOCK, + CONNECT_STEP_WAIT_FOR_READY, + CONNECT_STEP_CONNECT, + CONNECT_STEP_LAST, +} ConnectStep; + +typedef struct { + NMModemBroadband *self; + ConnectStep step; + + MMModemCapability caps; + NMConnection * connection; + GCancellable * cancellable; + MMSimpleConnectProperties *connect_properties; + GArray * ip_types; + guint ip_types_i; + guint ip_type_tries; + GError * first_error; +} ConnectContext; + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_MODEM, ); + +typedef struct { + /* The modem object from dbus */ + MMObject *modem_object; + /* Per-interface objects */ + MMModem * modem_iface; + MMModem3gpp * modem_3gpp_iface; + MMModemSimple *simple_iface; + MMSim * sim_iface; + + /* Connection setup */ + ConnectContext *ctx; + + MMBearer * bearer; + MMBearerIpConfig *ipv4_config; + MMBearerIpConfig *ipv6_config; + + guint idle_id_ip4; + guint idle_id_ip6; + + guint32 pin_tries; +} NMModemBroadbandPrivate; + +struct _NMModemBroadband { + NMModem parent; + NMModemBroadbandPrivate _priv; +}; + +struct _NMModemBroadbandClass { + NMModemClass parent; +}; + +G_DEFINE_TYPE(NMModemBroadband, nm_modem_broadband, NM_TYPE_MODEM) + +#define NM_MODEM_BROADBAND_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMModemBroadband, NM_IS_MODEM_BROADBAND) + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_MB +#define _NMLOG_PREFIX_NAME "modem-broadband" +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) { \ + NMModemBroadband *const __self = (self); \ + char __prefix_name[128]; \ + const char * __uid; \ + \ + _nm_log(_level, \ + (_NMLOG_DOMAIN), \ + 0, \ + NULL, \ + ((__self && __self->_priv.ctx) \ + ? nm_connection_get_uuid(__self->_priv.ctx->connection) \ + : NULL), \ + "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + (__self ? ({ \ + ((__uid = nm_modem_get_uid((NMModem *) __self)) \ + ? nm_sprintf_buf(__prefix_name, "[%s]", __uid) \ + : "(null)"); \ + }) \ + : "") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static NMDeviceStateReason +translate_mm_error(NMModemBroadband *self, GError *error) +{ + NMDeviceStateReason reason; + + g_return_val_if_fail(error != NULL, NM_DEVICE_STATE_REASON_UNKNOWN); + + if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_CARRIER)) + reason = NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER; + else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_DIALTONE)) + reason = NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE; + else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_BUSY)) + reason = NM_DEVICE_STATE_REASON_MODEM_BUSY; + else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_ANSWER)) + reason = NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT; + else if (g_error_matches(error, + MM_MOBILE_EQUIPMENT_ERROR, + MM_MOBILE_EQUIPMENT_ERROR_NETWORK_NOT_ALLOWED)) + reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED; + else if (g_error_matches(error, + MM_MOBILE_EQUIPMENT_ERROR, + MM_MOBILE_EQUIPMENT_ERROR_NETWORK_TIMEOUT)) + reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT; + else if (g_error_matches(error, + MM_MOBILE_EQUIPMENT_ERROR, + MM_MOBILE_EQUIPMENT_ERROR_NO_NETWORK)) + reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING; + else if (g_error_matches(error, + MM_MOBILE_EQUIPMENT_ERROR, + MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED)) + reason = NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED; + else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN)) + reason = NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED; + else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK)) + reason = NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED; + else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_WRONG)) + reason = NM_DEVICE_STATE_REASON_GSM_SIM_WRONG; + else if (g_error_matches(error, + MM_MOBILE_EQUIPMENT_ERROR, + MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD)) + reason = NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT; + else { + /* unable to map the ModemManager error to a NM_DEVICE_STATE_REASON */ + _LOGD("unmapped error detected: '%s'", error->message); + reason = NM_DEVICE_STATE_REASON_UNKNOWN; + } + + return reason; +} + +/*****************************************************************************/ + +static void +get_capabilities(NMModem * _self, + NMDeviceModemCapabilities *modem_caps, + NMDeviceModemCapabilities *current_caps) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(_self); + MMModemCapability all_supported = MM_MODEM_CAPABILITY_NONE; + MMModemCapability *supported; + guint n_supported; + + /* For now, we don't care about the capability combinations, just merge all + * combinations in a single mask */ + if (mm_modem_get_supported_capabilities(self->_priv.modem_iface, &supported, &n_supported)) { + guint i; + + for (i = 0; i < n_supported; i++) + all_supported |= supported[i]; + + g_free(supported); + } + + *modem_caps = (NMDeviceModemCapabilities) all_supported; + *current_caps = + (NMDeviceModemCapabilities) mm_modem_get_current_capabilities(self->_priv.modem_iface); +} + +static gboolean +owns_port(NMModem *_self, const char *iface) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(_self); + const MMModemPortInfo *ports = NULL; + guint n_ports = 0, i; + + mm_modem_peek_ports(self->_priv.modem_iface, &ports, &n_ports); + for (i = 0; i < n_ports; i++) { + if (nm_streq0(iface, ports[i].name)) + return TRUE; + } + return FALSE; +} + +/*****************************************************************************/ + +static void +ask_for_pin(NMModemBroadband *self) +{ + guint32 tries; + + tries = self->_priv.pin_tries++; + nm_modem_get_secrets(NM_MODEM(self), + NM_SETTING_GSM_SETTING_NAME, + tries ? TRUE : FALSE, + NM_SETTING_GSM_PIN); +} + +static NMModemIPMethod +get_bearer_ip_method(MMBearerIpConfig *config) +{ + MMBearerIpMethod mm_method; + + mm_method = mm_bearer_ip_config_get_method(config); + if (mm_method == MM_BEARER_IP_METHOD_PPP) + return NM_MODEM_IP_METHOD_PPP; + else if (mm_method == MM_BEARER_IP_METHOD_STATIC) + return NM_MODEM_IP_METHOD_STATIC; + else if (mm_method == MM_BEARER_IP_METHOD_DHCP) + return NM_MODEM_IP_METHOD_AUTO; + return NM_MODEM_IP_METHOD_UNKNOWN; +} + +static MMSimpleConnectProperties * +create_cdma_connect_properties(NMConnection *connection) +{ + MMSimpleConnectProperties *properties; + + properties = mm_simple_connect_properties_new(); + +#if !MM_CHECK_VERSION(1, 9, 1) + { + NMSettingCdma *setting; + const char * str; + + setting = nm_connection_get_setting_cdma(connection); + str = nm_setting_cdma_get_number(setting); + if (str) + mm_simple_connect_properties_set_number(properties, str); + } +#endif + + return properties; +} + +static MMSimpleConnectProperties * +create_gsm_connect_properties(NMConnection *connection, + const char * apn, + const char * username, + const char * password) +{ + NMSettingGsm * setting; + NMSettingPpp * s_ppp; + MMSimpleConnectProperties *properties; + const char * str; + + setting = nm_connection_get_setting_gsm(connection); + + properties = mm_simple_connect_properties_new(); + + mm_simple_connect_properties_set_apn(properties, apn ?: ""); + if (username) + mm_simple_connect_properties_set_user(properties, username); + if (password) + mm_simple_connect_properties_set_password(properties, password); + + str = nm_setting_gsm_get_network_id(setting); + if (str) + mm_simple_connect_properties_set_operator_id(properties, str); + + str = nm_setting_gsm_get_pin(setting); + if (str) + mm_simple_connect_properties_set_pin(properties, str); + + /* Roaming */ + if (nm_setting_gsm_get_home_only(setting)) + mm_simple_connect_properties_set_allow_roaming(properties, FALSE); + + /* For IpMethod == STATIC or DHCP */ + s_ppp = nm_connection_get_setting_ppp(connection); + if (s_ppp) { + MMBearerAllowedAuth allowed_auth = MM_BEARER_ALLOWED_AUTH_UNKNOWN; + + if (nm_setting_ppp_get_noauth(s_ppp)) + allowed_auth = MM_BEARER_ALLOWED_AUTH_NONE; + if (!nm_setting_ppp_get_refuse_pap(s_ppp)) + allowed_auth |= MM_BEARER_ALLOWED_AUTH_PAP; + if (!nm_setting_ppp_get_refuse_chap(s_ppp)) + allowed_auth |= MM_BEARER_ALLOWED_AUTH_CHAP; + if (!nm_setting_ppp_get_refuse_mschap(s_ppp)) + allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAP; + if (!nm_setting_ppp_get_refuse_mschapv2(s_ppp)) + allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAPV2; + if (!nm_setting_ppp_get_refuse_eap(s_ppp)) + allowed_auth |= MM_BEARER_ALLOWED_AUTH_EAP; + + mm_simple_connect_properties_set_allowed_auth(properties, allowed_auth); + } + + return properties; +} + +static void +connect_context_clear(NMModemBroadband *self) +{ + if (self->_priv.ctx) { + ConnectContext *ctx = self->_priv.ctx; + + g_clear_error(&ctx->first_error); + nm_clear_pointer(&ctx->ip_types, g_array_unref); + nm_clear_g_cancellable(&ctx->cancellable); + g_clear_object(&ctx->connection); + g_clear_object(&ctx->connect_properties); + g_clear_object(&ctx->self); + g_slice_free(ConnectContext, ctx); + self->_priv.ctx = NULL; + } +} + +static void connect_context_step(NMModemBroadband *self); + +static void +connect_ready(MMModemSimple *simple_iface, GAsyncResult *res, NMModemBroadband *self) +{ + ConnectContext *ctx; + GError * error = NULL; + NMModemIPMethod ip4_method = NM_MODEM_IP_METHOD_UNKNOWN; + NMModemIPMethod ip6_method = NM_MODEM_IP_METHOD_UNKNOWN; + MMBearer * bearer; + + bearer = mm_modem_simple_connect_finish(simple_iface, res, &error); + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free(error); + return; + } + + ctx = self->_priv.ctx; + + if (!ctx) + return; + + self->_priv.bearer = bearer; + + if (!self->_priv.bearer) { + if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN) + || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED) + && mm_modem_get_unlock_required(self->_priv.modem_iface) + == MM_MODEM_LOCK_SIM_PIN)) { + g_error_free(error); + + /* Request PIN */ + ask_for_pin(self); + connect_context_clear(self); + return; + } + + /* Save the error, if it's the first one */ + if (!ctx->first_error) { + /* Strip remote error info before saving it */ + if (g_dbus_error_is_remote_error(error)) + g_dbus_error_strip_remote_error(error); + ctx->first_error = error; + } else + g_clear_error(&error); + + if (ctx->ip_type_tries == 0 && g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_RETRY)) { + /* Try one more time */ + ctx->ip_type_tries++; + } else { + /* If the modem/provider lies and the IP type we tried isn't supported, + * retry with the next one, if any. + */ + ctx->ip_types_i++; + ctx->ip_type_tries = 0; + } + connect_context_step(self); + return; + } + + /* Grab IP configurations */ + self->_priv.ipv4_config = mm_bearer_get_ipv4_config(self->_priv.bearer); + if (self->_priv.ipv4_config) + ip4_method = get_bearer_ip_method(self->_priv.ipv4_config); + + self->_priv.ipv6_config = mm_bearer_get_ipv6_config(self->_priv.bearer); + if (self->_priv.ipv6_config) + ip6_method = get_bearer_ip_method(self->_priv.ipv6_config); + + if (!nm_modem_set_data_port(NM_MODEM(self), + NM_PLATFORM_GET, + mm_bearer_get_interface(self->_priv.bearer), + ip4_method, + ip6_method, + mm_bearer_get_ip_timeout(self->_priv.bearer), + &error)) { + _LOGW("failed to connect modem: %s", error->message); + g_error_free(error); + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_CONFIG_FAILED); + connect_context_clear(self); + return; + } + + ctx->step++; + connect_context_step(self); +} + +static void +send_pin_ready(MMSim *sim, GAsyncResult *result, NMModemBroadband *self) +{ + gs_free_error GError *error = NULL; + + mm_sim_send_pin_finish(sim, result, &error); + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + if (!self->_priv.ctx || self->_priv.ctx->step != CONNECT_STEP_UNLOCK) + g_return_if_reached(); + + if (error) { + if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN) + || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED) + && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN)) + ask_for_pin(self); + else + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, translate_mm_error(self, error)); + return; + } + + self->_priv.ctx->step++; + connect_context_step(self); +} + +static void +find_gsm_apn_cb(const char * apn, + const char * username, + const char * password, + const char * gateway, + const char * auth_method, + const GSList *dns, + GError * error, + gpointer user_data) +{ + NMModemBroadband * self = user_data; + NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self); + ConnectContext * ctx = priv->ctx; + + if (error) { + _LOGW("failed to connect '%s': APN not found: %s", + nm_connection_get_id(ctx->connection), + error->message); + + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_GSM_APN_FAILED); + connect_context_clear(self); + return; + } + + /* Blank APN ("") means the default subscription APN */ + ctx->connect_properties = + create_gsm_connect_properties(ctx->connection, apn, username, password); + g_return_if_fail(ctx->connect_properties); + connect_context_step(self); +} + +static gboolean +try_create_connect_properties(NMModemBroadband *self) +{ + NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self); + ConnectContext * ctx = priv->ctx; + + if (MODEM_CAPS_3GPP(ctx->caps)) { + NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection); + + if (!s_gsm || nm_setting_gsm_get_auto_config(s_gsm)) { + gs_unref_object MMModem3gpp *modem_3gpp = NULL; + const char * network_id = NULL; + + s_gsm = nm_connection_get_setting_gsm(ctx->connection); + if (s_gsm) + network_id = nm_setting_gsm_get_network_id(s_gsm); + if (!network_id) { + if (mm_modem_get_state(self->_priv.modem_iface) < MM_MODEM_STATE_REGISTERED) + return FALSE; + modem_3gpp = mm_object_get_modem_3gpp(priv->modem_object); + network_id = mm_modem_3gpp_get_operator_code(modem_3gpp); + } + if (!network_id) { + _LOGW("failed to connect '%s': unable to determine the network id", + nm_connection_get_id(ctx->connection)); + goto out; + } + + nm_service_providers_find_gsm_apn(MOBILE_BROADBAND_PROVIDER_INFO_DATABASE, + network_id, + ctx->cancellable, + find_gsm_apn_cb, + self); + } else { + ctx->connect_properties = + create_gsm_connect_properties(ctx->connection, + nm_setting_gsm_get_apn(s_gsm), + nm_setting_gsm_get_username(s_gsm), + nm_setting_gsm_get_password(s_gsm)); + g_return_val_if_fail(ctx->connect_properties, TRUE); + } + + return TRUE; + } else if (MODEM_CAPS_3GPP2(ctx->caps)) { + ctx->connect_properties = create_cdma_connect_properties(ctx->connection); + g_return_val_if_fail(ctx->connect_properties, FALSE); + return TRUE; + } else { + _LOGW("failed to connect '%s': not a mobile broadband modem", + nm_connection_get_id(ctx->connection)); + } + +out: + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED); + connect_context_clear(self); + return TRUE; +} + +static void +connect_context_step(NMModemBroadband *self) +{ + ConnectContext *ctx = self->_priv.ctx; + + switch (ctx->step) { + case CONNECT_STEP_FIRST: + ctx->step++; + /* fall-through */ + + case CONNECT_STEP_WAIT_FOR_SIM: + if (MODEM_CAPS_3GPP(ctx->caps) && !self->_priv.sim_iface) { + /* Have to wait for the SIM to show up */ + break; + } + ctx->step++; + /* fall-through */ + + case CONNECT_STEP_UNLOCK: + if (MODEM_CAPS_3GPP(ctx->caps) + && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN) { + NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection); + const char * pin = nm_setting_gsm_get_pin(s_gsm); + + /* If we have a PIN already, send it. If we don't, get it. */ + if (pin) { + mm_sim_send_pin(self->_priv.sim_iface, + pin, + ctx->cancellable, + (GAsyncReadyCallback) send_pin_ready, + self); + } else { + ask_for_pin(self); + } + break; + } + ctx->step++; + /* fall-through */ + case CONNECT_STEP_WAIT_FOR_READY: + { + GError *error = NULL; + + if (mm_modem_get_state(self->_priv.modem_iface) <= MM_MODEM_STATE_LOCKED) + break; + + if (!try_create_connect_properties(self)) + break; + + if (!self->_priv.ctx) + break; + + /* Build up list of IP types that we need to use in the retries */ + ctx->ip_types = nm_modem_get_connection_ip_type(NM_MODEM(self), ctx->connection, &error); + if (!ctx->ip_types) { + _LOGW("failed to connect '%s': %s", + nm_connection_get_id(ctx->connection), + error->message); + g_clear_error(&error); + + nm_modem_emit_prepare_result(NM_MODEM(self), + FALSE, + NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED); + connect_context_clear(self); + break; + } + + ctx->step++; + } + /* fall-through */ + case CONNECT_STEP_CONNECT: + if (!ctx->connect_properties) + break; + + if (ctx->ip_types_i < ctx->ip_types->len) { + NMModemIPType current; + + current = g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i); + + if (current == NM_MODEM_IP_TYPE_IPV4) + mm_simple_connect_properties_set_ip_type(ctx->connect_properties, + MM_BEARER_IP_FAMILY_IPV4); + else if (current == NM_MODEM_IP_TYPE_IPV6) + mm_simple_connect_properties_set_ip_type(ctx->connect_properties, + MM_BEARER_IP_FAMILY_IPV6); + else if (current == NM_MODEM_IP_TYPE_IPV4V6) + mm_simple_connect_properties_set_ip_type(ctx->connect_properties, + MM_BEARER_IP_FAMILY_IPV4V6); + else + g_return_if_reached(); + + _nm_modem_set_apn(NM_MODEM(self), + mm_simple_connect_properties_get_apn(ctx->connect_properties)); + + _LOGD("launching connection with ip type '%s' (try %d)", + nm_modem_ip_type_to_string(current), + ctx->ip_type_tries + 1); + + mm_modem_simple_connect(self->_priv.simple_iface, + ctx->connect_properties, + ctx->cancellable, + (GAsyncReadyCallback) connect_ready, + self); + break; + } + + ctx->step++; + /* fall-through */ + + case CONNECT_STEP_LAST: + if (self->_priv.ipv4_config || self->_priv.ipv6_config) + nm_modem_emit_prepare_result(NM_MODEM(self), TRUE, NM_DEVICE_STATE_REASON_NONE); + else { + /* If we have a saved error from a previous attempt, use it */ + if (!ctx->first_error) + ctx->first_error = g_error_new_literal(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "invalid bearer IP configuration"); + + _LOGW("failed to connect modem: %s", ctx->first_error->message); + nm_modem_emit_prepare_result(NM_MODEM(self), + FALSE, + translate_mm_error(self, ctx->first_error)); + } + + connect_context_clear(self); + break; + } +} + +static NMActStageReturn +modem_act_stage1_prepare(NMModem * _self, + NMConnection * connection, + NMDeviceStateReason *out_failure_reason) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(_self); + + /* Make sure we can get the Simple interface from the modem */ + if (!self->_priv.simple_iface) { + self->_priv.simple_iface = mm_object_get_modem_simple(self->_priv.modem_object); + if (!self->_priv.simple_iface) { + _LOGW("cannot access the Simple mobile broadband modem interface"); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + } + + connect_context_clear(self); + + /* Allocate new context for this connect stage attempt */ + self->_priv.ctx = g_slice_new0(ConnectContext); + self->_priv.ctx->caps = mm_modem_get_current_capabilities(self->_priv.modem_iface); + self->_priv.ctx->cancellable = g_cancellable_new(); + self->_priv.ctx->connection = g_object_ref(connection); + + g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(self->_priv.simple_iface), + MODEM_CONNECT_TIMEOUT_SECS * 1000); + connect_context_step(self); + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +static gboolean +check_connection_compatible_with_modem(NMModem *_self, NMConnection *connection, GError **error) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(_self); + MMModemCapability modem_caps; + + modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface); + + if (MODEM_CAPS_3GPP(modem_caps)) { + if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error)) + return FALSE; + + return TRUE; + } + + if (MODEM_CAPS_3GPP2(modem_caps)) { + if (!_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, error)) + return FALSE; + + return TRUE; + } + + if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL) + && !_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, NULL)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "connection type %s is not supported by modem", + nm_connection_get_connection_type(connection)); + return FALSE; + } + + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "modem lacks capabilities for %s profile", + nm_connection_get_connection_type(connection)); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +complete_connection(NMModem * modem, + const char * iface, + NMConnection * connection, + NMConnection *const *existing_connections, + GError ** error) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(modem); + MMModemCapability modem_caps; + NMSettingPpp * s_ppp; + + modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface); + + /* PPP settings common to 3GPP and 3GPP2 */ + s_ppp = nm_connection_get_setting_ppp(connection); + if (!s_ppp) { + s_ppp = (NMSettingPpp *) nm_setting_ppp_new(); + g_object_set(G_OBJECT(s_ppp), + NM_SETTING_PPP_LCP_ECHO_FAILURE, + 5, + NM_SETTING_PPP_LCP_ECHO_INTERVAL, + 30, + NULL); + nm_connection_add_setting(connection, NM_SETTING(s_ppp)); + } + + if (MODEM_CAPS_3GPP(modem_caps)) { + NMSettingGsm *s_gsm; + + s_gsm = nm_connection_get_setting_gsm(connection); + if (!s_gsm) { + s_gsm = (NMSettingGsm *) nm_setting_gsm_new(); + nm_connection_add_setting(connection, NM_SETTING(s_gsm)); + g_object_set(G_OBJECT(s_gsm), NM_SETTING_GSM_AUTO_CONFIG, TRUE, NULL); + } + + if (!nm_setting_gsm_get_device_id(s_gsm)) { + g_object_set(G_OBJECT(s_gsm), + NM_SETTING_GSM_DEVICE_ID, + nm_modem_get_device_id(modem), + NULL); + } + + nm_utils_complete_generic(NM_PLATFORM_GET, + connection, + NM_SETTING_GSM_SETTING_NAME, + existing_connections, + NULL, + _("GSM connection"), + NULL, + NULL, + FALSE); /* No IPv6 yet by default */ + + return TRUE; + } + + if (MODEM_CAPS_3GPP2(modem_caps)) { + NMSettingCdma *s_cdma; + + s_cdma = nm_connection_get_setting_cdma(connection); + if (!s_cdma) { + s_cdma = (NMSettingCdma *) nm_setting_cdma_new(); + nm_connection_add_setting(connection, NM_SETTING(s_cdma)); + } + + if (!nm_setting_cdma_get_number(s_cdma)) + g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL); + + nm_utils_complete_generic(NM_PLATFORM_GET, + connection, + NM_SETTING_CDMA_SETTING_NAME, + existing_connections, + NULL, + _("CDMA connection"), + NULL, + iface, + FALSE); /* No IPv6 yet by default */ + + return TRUE; + } + + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Device is not a mobile broadband modem"); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +get_user_pass(NMModem *modem, NMConnection *connection, const char **user, const char **pass) +{ + NMSettingGsm * s_gsm; + NMSettingCdma *s_cdma; + + s_gsm = nm_connection_get_setting_gsm(connection); + s_cdma = nm_connection_get_setting_cdma(connection); + if (!s_gsm && !s_cdma) + return FALSE; + + if (user) { + if (s_gsm) + *user = nm_setting_gsm_get_username(s_gsm); + else if (s_cdma) + *user = nm_setting_cdma_get_username(s_cdma); + } + if (pass) { + if (s_gsm) + *pass = nm_setting_gsm_get_password(s_gsm); + else if (s_cdma) + *pass = nm_setting_cdma_get_password(s_cdma); + } + + return TRUE; +} + +/*****************************************************************************/ +/* Query/Update enabled state */ + +static void +set_power_state_low_ready(MMModem *modem, GAsyncResult *result, NMModemBroadband *self) +{ + GError *error = NULL; + + if (!mm_modem_set_power_state_finish(modem, result, &error)) { + /* Log but ignore errors; not all modems support low power state */ + _LOGD("failed to set modem low power state: %s", NM_G_ERROR_MSG(error)); + g_clear_error(&error); + } + + /* Balance refcount */ + g_object_unref(self); +} + +static void +modem_disable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self) +{ + GError *error = NULL; + + if (mm_modem_disable_finish(modem_iface, res, &error)) { + /* Once disabled, move to low-power mode */ + mm_modem_set_power_state(modem_iface, + MM_MODEM_POWER_STATE_LOW, + NULL, + (GAsyncReadyCallback) set_power_state_low_ready, + g_object_ref(self)); + } else { + _LOGW("failed to disable modem: %s", NM_G_ERROR_MSG(error)); + nm_modem_set_prev_state(NM_MODEM(self), "disable failed"); + g_clear_error(&error); + } + + /* Balance refcount */ + g_object_unref(self); +} + +static void +modem_enable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self) +{ + GError *error = NULL; + + if (!mm_modem_enable_finish(modem_iface, res, &error)) { + _LOGW("failed to enable modem: %s", NM_G_ERROR_MSG(error)); + nm_modem_set_prev_state(NM_MODEM(self), "enable failed"); + g_clear_error(&error); + } + + /* Balance refcount */ + g_object_unref(self); +} + +static void +set_mm_enabled(NMModem *_self, gboolean enabled) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(_self); + + if (enabled) { + mm_modem_enable(self->_priv.modem_iface, + NULL, /* cancellable */ + (GAsyncReadyCallback) modem_enable_ready, + g_object_ref(self)); + } else { + mm_modem_disable(self->_priv.modem_iface, + NULL, /* cancellable */ + (GAsyncReadyCallback) modem_disable_ready, + g_object_ref(self)); + } +} + +/*****************************************************************************/ +/* IPv4 method static */ + +static gboolean +static_stage3_ip4_done(NMModemBroadband *self) +{ + GError * error = NULL; + gs_unref_object NMIP4Config *config = NULL; + const char * data_port; + const char * address_string; + const char * gw_string; + guint32 address_network; + guint32 gw = 0; + NMPlatformIP4Address address; + const char ** dns; + guint i; + guint32 ip4_route_table, ip4_route_metric; + NMPlatformIP4Route * r; + guint32 mtu_n; + + g_return_val_if_fail(self->_priv.ipv4_config, FALSE); + g_return_val_if_fail(self->_priv.bearer, FALSE); + + self->_priv.idle_id_ip4 = 0; + + _LOGI("IPv4 static configuration:"); + + /* Fully fail if invalid IP address retrieved */ + address_string = mm_bearer_ip_config_get_address(self->_priv.ipv4_config); + if (!address_string + || !nm_utils_parse_inaddr_bin(AF_INET, address_string, NULL, &address_network)) { + error = + g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IP4 configuration failed: invalid address given %s%s%s", + nm_modem_get_uid(NM_MODEM(self)), + NM_PRINT_FMT_QUOTE_STRING(address_string)); + goto out; + } + + /* Missing gateway not a hard failure */ + gw_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv4_config); + if (gw_string && !nm_utils_parse_inaddr_bin(AF_INET, gw_string, NULL, &gw)) { + error = + g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IP4 configuration failed: invalid gateway address \"%s\"", + nm_modem_get_uid(NM_MODEM(self)), + gw_string); + goto out; + } + + data_port = mm_bearer_get_interface(self->_priv.bearer); + g_return_val_if_fail(data_port, FALSE); + config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), + nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port)); + + memset(&address, 0, sizeof(address)); + address.address = address_network; + address.peer_address = address_network; + address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv4_config); + address.addr_source = NM_IP_CONFIG_SOURCE_WWAN; + if (address.plen <= 32) + nm_ip4_config_add_address(config, &address); + + _LOGI(" address %s/%d", address_string, address.plen); + + nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL); + r = &(NMPlatformIP4Route){ + .rt_source = NM_IP_CONFIG_SOURCE_WWAN, + .gateway = gw, + .table_coerced = nm_platform_route_table_coerce(ip4_route_table), + .metric = ip4_route_metric, + }; + nm_ip4_config_add_route(config, r, NULL); + _LOGI(" gateway %s", gw_string); + + /* DNS servers */ + dns = mm_bearer_ip_config_get_dns(self->_priv.ipv4_config); + for (i = 0; dns && dns[i]; i++) { + if (nm_utils_parse_inaddr_bin(AF_INET, dns[i], NULL, &address_network) + && address_network > 0) { + nm_ip4_config_add_nameserver(config, address_network); + _LOGI(" DNS %s", dns[i]); + } + } + +#if MM_CHECK_VERSION(1, 4, 0) + mtu_n = mm_bearer_ip_config_get_mtu(self->_priv.ipv4_config); + if (mtu_n) { + nm_ip4_config_set_mtu(config, mtu_n, NM_IP_CONFIG_SOURCE_WWAN); + _LOGI(" MTU %u", mtu_n); + } +#endif + +out: + g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, config, error); + g_clear_error(&error); + return FALSE; +} + +static NMActStageReturn +static_stage3_ip4_config_start(NMModem * modem, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(modem); + NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self); + + /* We schedule it in an idle just to follow the same logic as in the + * generic modem implementation. */ + nm_clear_g_source(&priv->idle_id_ip4); + priv->idle_id_ip4 = g_idle_add((GSourceFunc) static_stage3_ip4_done, self); + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ +/* IPv6 method static */ + +static gboolean +stage3_ip6_done(NMModemBroadband *self) +{ + GError * error = NULL; + NMIP6Config * config = NULL; + const char * data_port; + const char * address_string; + NMPlatformIP6Address address; + NMModemIPMethod ip_method; + const char ** dns; + guint i; + + g_return_val_if_fail(self->_priv.ipv6_config, FALSE); + + self->_priv.idle_id_ip6 = 0; + memset(&address, 0, sizeof(address)); + + ip_method = get_bearer_ip_method(self->_priv.ipv6_config); + + address_string = mm_bearer_ip_config_get_address(self->_priv.ipv6_config); + if (!address_string) { + /* DHCP/SLAAC is allowed to skip addresses; other methods require it */ + if (ip_method != NM_MODEM_IP_METHOD_AUTO) { + error = g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IPv6 configuration failed: no address given", + nm_modem_get_uid(NM_MODEM(self))); + } + goto out; + } + + /* Fail if invalid IP address retrieved */ + if (!inet_pton(AF_INET6, address_string, (void *) &(address.address))) { + error = g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IPv6 configuration failed: invalid address given '%s'", + nm_modem_get_uid(NM_MODEM(self)), + address_string); + goto out; + } + + _LOGI("IPv6 base configuration:"); + + data_port = mm_bearer_get_interface(self->_priv.bearer); + g_return_val_if_fail(data_port, FALSE); + + config = nm_ip6_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), + nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port)); + + address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config); + if (address.plen <= 128) + nm_ip6_config_add_address(config, &address); + + _LOGI(" address %s/%d", address_string, address.plen); + + address_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv6_config); + if (address_string) { + guint32 ip6_route_table, ip6_route_metric; + + if (inet_pton(AF_INET6, address_string, &address.address) != 1) { + error = + g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IPv6 configuration failed: invalid gateway given '%s'", + nm_modem_get_uid(NM_MODEM(self)), + address_string); + goto out; + } + + nm_modem_get_route_parameters(NM_MODEM(self), + NULL, + NULL, + &ip6_route_table, + &ip6_route_metric); + { + const NMPlatformIP6Route r = { + .rt_source = NM_IP_CONFIG_SOURCE_WWAN, + .gateway = address.address, + .table_coerced = nm_platform_route_table_coerce(ip6_route_table), + .metric = ip6_route_metric, + }; + + _LOGI(" gateway %s", address_string); + nm_ip6_config_add_route(config, &r, NULL); + } + } else if (ip_method == NM_MODEM_IP_METHOD_STATIC) { + /* Gateway required for the 'static' method */ + error = g_error_new(NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "(%s) retrieving IPv6 configuration failed: missing gateway", + nm_modem_get_uid(NM_MODEM(self))); + goto out; + } + + /* DNS servers */ + dns = mm_bearer_ip_config_get_dns(self->_priv.ipv6_config); + for (i = 0; dns && dns[i]; i++) { + struct in6_addr addr; + + if (inet_pton(AF_INET6, dns[i], &addr)) { + nm_ip6_config_add_nameserver(config, &addr); + _LOGI(" DNS %s", dns[i]); + } + } + +out: + nm_modem_emit_ip6_config_result(NM_MODEM(self), config, error); + g_clear_object(&config); + g_clear_error(&error); + return FALSE; +} + +static NMActStageReturn +stage3_ip6_config_request(NMModem *modem, NMDeviceStateReason *out_failure_reason) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(modem); + NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self); + + /* We schedule it in an idle just to follow the same logic as in the + * generic modem implementation. */ + nm_clear_g_source(&priv->idle_id_ip6); + priv->idle_id_ip6 = g_idle_add((GSourceFunc) stage3_ip6_done, self); + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ +/* Disconnect */ + +typedef struct { + NMModemBroadband * self; + _NMModemDisconnectCallback callback; + gpointer callback_user_data; + GCancellable * cancellable; + gboolean warn; +} DisconnectContext; + +static void +disconnect_context_complete(DisconnectContext *ctx, GError *error) +{ + if (ctx->callback) + ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data); + nm_g_object_unref(ctx->cancellable); + g_object_unref(ctx->self); + g_slice_free(DisconnectContext, ctx); +} + +static void +disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable) +{ + DisconnectContext *ctx = user_data; + gs_free_error GError *cancelled_error = NULL; + + g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error); + disconnect_context_complete(ctx, cancelled_error); +} + +static void +simple_disconnect_ready(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + MMModemSimple * modem_iface = MM_MODEM_SIMPLE(source_object); + DisconnectContext *ctx = user_data; + gs_free_error GError *error = NULL; + + if (!mm_modem_simple_disconnect_finish(modem_iface, res, &error)) { + if (ctx->warn && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) { + NMModemBroadband *self = ctx->self; + + _LOGW("failed to disconnect modem: %s", error->message); + } + } + + disconnect_context_complete(ctx, error); +} + +static void +disconnect(NMModem * modem, + gboolean warn, + GCancellable * cancellable, + _NMModemDisconnectCallback callback, + gpointer user_data) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(modem); + DisconnectContext *ctx; + + connect_context_clear(self); + _nm_modem_set_apn(NM_MODEM(self), NULL); + + ctx = g_slice_new0(DisconnectContext); + ctx->self = g_object_ref(self); + ctx->cancellable = nm_g_object_ref(cancellable); + ctx->callback = callback; + ctx->callback_user_data = user_data; + + /* Don't bother warning on FAILED since the modem is already gone */ + ctx->warn = warn; + + /* Already cancelled or no simple-iface? We are done. */ + if (!ctx->self->_priv.simple_iface || g_cancellable_is_cancelled(cancellable)) { + nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx); + return; + } + + _LOGD("notifying ModemManager about the modem disconnection"); + mm_modem_simple_disconnect(self->_priv.simple_iface, + NULL, /* bearer path; if NULL given ALL get disconnected */ + cancellable, + simple_disconnect_ready, + ctx); +} + +/*****************************************************************************/ + +static void +deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(modem); + + /* TODO: cancel SimpleConnect() if any */ + + /* Cleanup IPv4 addresses and routes */ + g_clear_object(&self->_priv.ipv4_config); + g_clear_object(&self->_priv.ipv6_config); + g_clear_object(&self->_priv.bearer); + + self->_priv.pin_tries = 0; + + NM_MODEM_CLASS(nm_modem_broadband_parent_class) + ->deactivate_cleanup(modem, device, stop_ppp_manager); +} + +/*****************************************************************************/ + +#define MAP_STATE(name) \ + case MM_MODEM_STATE_##name: \ + return NM_MODEM_STATE_##name; + +static NMModemState +mm_state_to_nm(MMModemState mm_state) +{ + switch (mm_state) { + MAP_STATE(UNKNOWN) + MAP_STATE(FAILED) + MAP_STATE(INITIALIZING) + MAP_STATE(LOCKED) + MAP_STATE(DISABLED) + MAP_STATE(DISABLING) + MAP_STATE(ENABLING) + MAP_STATE(ENABLED) + MAP_STATE(SEARCHING) + MAP_STATE(REGISTERED) + MAP_STATE(DISCONNECTING) + MAP_STATE(CONNECTING) + MAP_STATE(CONNECTED) + } + return NM_MODEM_STATE_UNKNOWN; +} + +static void +modem_state_changed(MMModem * modem, + MMModemState old_state, + MMModemState new_state, + MMModemStateChangeReason reason, + NMModemBroadband * self) +{ + /* After the SIM is unlocked MM1 will move the device to INITIALIZING which + * is an unavailable state. That makes state handling confusing here, so + * suppress this state change and let the modem move from LOCKED to DISABLED. + */ + if (new_state == MM_MODEM_STATE_INITIALIZING && old_state == MM_MODEM_STATE_LOCKED) + return; + + nm_modem_set_state(NM_MODEM(self), + mm_state_to_nm(new_state), + mm_modem_state_change_reason_get_string(reason)); + + if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_READY) + connect_context_step(self); +} + +/*****************************************************************************/ + +static NMModemIPType +mm_ip_family_to_nm(MMBearerIpFamily family) +{ + NMModemIPType nm_type = NM_MODEM_IP_TYPE_UNKNOWN; + + if (family & MM_BEARER_IP_FAMILY_IPV4) + nm_type |= NM_MODEM_IP_TYPE_IPV4; + if (family & MM_BEARER_IP_FAMILY_IPV6) + nm_type |= NM_MODEM_IP_TYPE_IPV6; + if (family & MM_BEARER_IP_FAMILY_IPV4V6) + nm_type |= MM_BEARER_IP_FAMILY_IPV4V6; + + return nm_type; +} + +static void +get_sim_ready(MMModem *modem, GAsyncResult *res, NMModemBroadband *self) +{ + GError *error = NULL; + MMSim * new_sim; + + new_sim = mm_modem_get_sim_finish(modem, res, &error); + if (new_sim != self->_priv.sim_iface) { + g_clear_object(&self->_priv.sim_iface); + self->_priv.sim_iface = new_sim; + } else + g_clear_object(&new_sim); + + if (self->_priv.sim_iface) { + g_object_set(G_OBJECT(self), + NM_MODEM_SIM_ID, + mm_sim_get_identifier(self->_priv.sim_iface), + NM_MODEM_SIM_OPERATOR_ID, + mm_sim_get_operator_identifier(self->_priv.sim_iface), + NULL); + + /* If we're waiting for the SIM during a connect, proceed with the connect */ + if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_SIM) + connect_context_step(self); + } else { + _NMLOG(g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND) ? LOGL_INFO + : LOGL_WARN, + "failed to retrieve SIM object: %s", + NM_G_ERROR_MSG(error)); + } + g_clear_error(&error); + g_object_unref(self); +} + +static void +sim_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(user_data); + + g_return_if_fail(modem == self->_priv.modem_iface); + + if (mm_modem_get_sim_path(self->_priv.modem_iface)) { + mm_modem_get_sim(self->_priv.modem_iface, + NULL, /* cancellable */ + (GAsyncReadyCallback) get_sim_ready, + g_object_ref(self)); + } else + g_object_set(G_OBJECT(self), NM_MODEM_SIM_ID, NULL, NM_MODEM_SIM_OPERATOR_ID, NULL, NULL); +} + +static void +supported_ip_families_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(user_data); + + g_return_if_fail(modem == self->_priv.modem_iface); + + g_object_set(G_OBJECT(self), + NM_MODEM_IP_TYPES, + mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem)), + NULL); +} + +static void +operator_code_changed(MMModem3gpp *modem_3gpp, GParamSpec *pspec, gpointer user_data) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(user_data); + + g_return_if_fail(modem_3gpp == self->_priv.modem_3gpp_iface); + _nm_modem_set_operator_code(NM_MODEM(self), mm_modem_3gpp_get_operator_code(modem_3gpp)); +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(object); + + switch (prop_id) { + case PROP_MODEM: + g_value_set_object(value, self->_priv.modem_object); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMModemBroadband *self = NM_MODEM_BROADBAND(object); + + switch (prop_id) { + case PROP_MODEM: + /* construct-only */ + self->_priv.modem_object = g_value_dup_object(value); + self->_priv.modem_iface = mm_object_get_modem(self->_priv.modem_object); + g_return_if_fail(self->_priv.modem_iface); + self->_priv.modem_3gpp_iface = mm_object_get_modem_3gpp(self->_priv.modem_object); + + g_signal_connect(self->_priv.modem_iface, + "state-changed", + G_CALLBACK(modem_state_changed), + self); + g_signal_connect(self->_priv.modem_iface, "notify::sim", G_CALLBACK(sim_changed), self); + sim_changed(self->_priv.modem_iface, NULL, self); + g_signal_connect(self->_priv.modem_iface, + "notify::supported-ip-families", + G_CALLBACK(supported_ip_families_changed), + self); + + if (self->_priv.modem_3gpp_iface) { + g_signal_connect(self->_priv.modem_3gpp_iface, + "notify::operator-code", + G_CALLBACK(operator_code_changed), + self); + } + + /* Note: don't grab the Simple iface here; the Modem interface is the + * only one assumed to be always valid and available */ + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_modem_broadband_init(NMModemBroadband *self) +{} + +NMModem * +nm_modem_broadband_new(GObject *object, GError **error) +{ + MMObject * modem_object; + MMModem * modem_iface; + MMModem3gpp * modem_3gpp_iface; + const char *const *drivers; + const char * operator_code = NULL; + gs_free char * driver = NULL; + + g_return_val_if_fail(MM_IS_OBJECT(object), NULL); + modem_object = MM_OBJECT(object); + + /* Ensure we have the 'Modem' interface and the primary port at least */ + modem_iface = mm_object_peek_modem(modem_object); + g_return_val_if_fail(modem_iface, NULL); + g_return_val_if_fail(mm_modem_get_primary_port(modem_iface), NULL); + + /* Build a single string with all drivers listed */ + drivers = mm_modem_get_drivers(modem_iface); + if (drivers) + driver = g_strjoinv(", ", (char **) drivers); + + modem_3gpp_iface = mm_object_peek_modem_3gpp(modem_object); + if (modem_3gpp_iface) + operator_code = mm_modem_3gpp_get_operator_code(modem_3gpp_iface); + + return g_object_new(NM_TYPE_MODEM_BROADBAND, + NM_MODEM_PATH, + mm_object_get_path(modem_object), + NM_MODEM_UID, + mm_modem_get_primary_port(modem_iface), + NM_MODEM_CONTROL_PORT, + mm_modem_get_primary_port(modem_iface), + NM_MODEM_IP_TYPES, + mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem_iface)), + NM_MODEM_STATE, + (int) mm_state_to_nm(mm_modem_get_state(modem_iface)), + NM_MODEM_DEVICE_ID, + mm_modem_get_device_identifier(modem_iface), + NM_MODEM_BROADBAND_MODEM, + modem_object, + NM_MODEM_DRIVER, + driver, + NM_MODEM_OPERATOR_CODE, + operator_code, + NULL); +} + +static void +dispose(GObject *object) +{ + NMModemBroadband * self = NM_MODEM_BROADBAND(object); + NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self); + + nm_clear_g_source(&priv->idle_id_ip4); + nm_clear_g_source(&priv->idle_id_ip6); + + connect_context_clear(self); + g_clear_object(&self->_priv.ipv4_config); + g_clear_object(&self->_priv.ipv6_config); + g_clear_object(&self->_priv.bearer); + + if (self->_priv.modem_iface) { + g_signal_handlers_disconnect_by_data(self->_priv.modem_iface, self); + g_clear_object(&self->_priv.modem_iface); + } + + if (self->_priv.modem_3gpp_iface) { + g_signal_handlers_disconnect_by_data(self->_priv.modem_3gpp_iface, self); + g_clear_object(&self->_priv.modem_3gpp_iface); + } + + g_clear_object(&self->_priv.simple_iface); + g_clear_object(&self->_priv.sim_iface); + g_clear_object(&self->_priv.modem_object); + + G_OBJECT_CLASS(nm_modem_broadband_parent_class)->dispose(object); +} + +static void +nm_modem_broadband_class_init(NMModemBroadbandClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + NMModemClass *modem_class = NM_MODEM_CLASS(klass); + + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + modem_class->get_capabilities = get_capabilities; + modem_class->static_stage3_ip4_config_start = static_stage3_ip4_config_start; + modem_class->stage3_ip6_config_request = stage3_ip6_config_request; + modem_class->disconnect = disconnect; + modem_class->deactivate_cleanup = deactivate_cleanup; + modem_class->set_mm_enabled = set_mm_enabled; + modem_class->get_user_pass = get_user_pass; + modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem; + modem_class->complete_connection = complete_connection; + modem_class->modem_act_stage1_prepare = modem_act_stage1_prepare; + modem_class->owns_port = owns_port; + + obj_properties[PROP_MODEM] = + g_param_spec_object(NM_MODEM_BROADBAND_MODEM, + "", + "", + MM_GDBUS_TYPE_OBJECT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} diff --git a/src/core/devices/wwan/nm-modem-broadband.h b/src/core/devices/wwan/nm-modem-broadband.h new file mode 100644 index 00000000..627fe25a --- /dev/null +++ b/src/core/devices/wwan/nm-modem-broadband.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2012 - Aleksander Morgado <aleksander@gnu.org> + */ + +#ifndef __NETWORKMANAGER_MODEM_BROADBAND_H__ +#define __NETWORKMANAGER_MODEM_BROADBAND_H__ + +#include "nm-modem.h" + +#define NM_TYPE_MODEM_BROADBAND (nm_modem_broadband_get_type()) +#define NM_MODEM_BROADBAND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadband)) +#define NM_MODEM_BROADBAND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass)) +#define NM_IS_MODEM_BROADBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_BROADBAND)) +#define NM_IS_MODEM_BROADBAND_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_BROADBAND)) +#define NM_MODEM_BROADBAND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass)) + +typedef struct _NMModemBroadband NMModemBroadband; +typedef struct _NMModemBroadbandClass NMModemBroadbandClass; + +GType nm_modem_broadband_get_type(void); + +NMModem *nm_modem_broadband_new(GObject *object, GError **error); + +#endif /* __NETWORKMANAGER_MODEM_BROADBAND_H__ */ diff --git a/src/core/devices/wwan/nm-modem-manager.c b/src/core/devices/wwan/nm-modem-manager.c new file mode 100644 index 00000000..598c6898 --- /dev/null +++ b/src/core/devices/wwan/nm-modem-manager.c @@ -0,0 +1,876 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2014 Red Hat, Inc. + * Copyright (C) 2009 Novell, Inc. + * Copyright (C) 2009 - 2013 Canonical Ltd. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-modem-manager.h" + +#include <libmm-glib.h> + +#if HAVE_LIBSYSTEMD + #include <systemd/sd-daemon.h> +#else + #define sd_booted() FALSE +#endif + +#include "nm-std-aux/nm-dbus-compat.h" +#include "nm-modem.h" +#include "nm-modem-broadband.h" + +#if WITH_OFONO + #include "nm-modem-ofono.h" +#endif + +#define MODEM_POKE_INTERVAL 120 + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMModemManager, PROP_NAME_OWNER, ); + +enum { + MODEM_ADDED, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct { + GDBusConnection *dbus_connection; + + /* used during g_bus_get() and later during mm_manager_new(). */ + GCancellable *main_cancellable; + + struct { + MMManager * manager; + GCancellable *poke_cancellable; + gulong handle_name_owner_changed_id; + gulong handle_object_added_id; + gulong handle_object_removed_id; + guint relaunch_id; + + /* this only has one use: that the <info> logging line about + * ModemManager available distinguishes between first-time + * and later name-owner-changed. */ + enum { + LOG_AVAILABLE_NOT_INITIALIZED = 0, + LOG_AVAILABLE_YES, + LOG_AVAILABLE_NO, + } log_available : 3; + + GDBusProxy * proxy; + GCancellable *proxy_cancellable; + guint proxy_ref_count; + char * proxy_name_owner; + } modm; + +#if WITH_OFONO + struct { + GDBusProxy * proxy; + GCancellable *cancellable; + } ofono; +#endif + + GHashTable *modems; +} NMModemManagerPrivate; + +struct _NMModemManager { + GObject parent; + NMModemManagerPrivate _priv; +}; + +struct _NMModemManagerClass { + GObjectClass parent; +}; + +G_DEFINE_TYPE(NMModemManager, nm_modem_manager, G_TYPE_OBJECT) + +#define NM_MODEM_MANAGER_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMModemManager, NM_IS_MODEM_MANAGER) + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_MB +#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "modem-manager", __VA_ARGS__) + +/*****************************************************************************/ + +NM_DEFINE_SINGLETON_GETTER(NMModemManager, nm_modem_manager_get, NM_TYPE_MODEM_MANAGER); + +/*****************************************************************************/ + +static void modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds); +static void modm_ensure_manager(NMModemManager *self); + +/*****************************************************************************/ + +static void +handle_new_modem(NMModemManager *self, NMModem *modem) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + const char * path; + + path = nm_modem_get_path(modem); + if (g_hash_table_lookup(priv->modems, path)) { + g_warn_if_reached(); + return; + } + + /* Track the new modem */ + g_hash_table_insert(priv->modems, g_strdup(path), modem); + g_signal_emit(self, signals[MODEM_ADDED], 0, modem); +} + +static gboolean +remove_one_modem(gpointer key, gpointer value, gpointer user_data) +{ + nm_modem_emit_removed(NM_MODEM(value)); + return TRUE; +} + +/*****************************************************************************/ + +NMModem ** +nm_modem_manager_get_modems(NMModemManager *self, guint *out_len) +{ + g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL); + + return (NMModem **) nm_utils_hash_values_to_array(NM_MODEM_MANAGER_GET_PRIVATE(self)->modems, + NULL, + NULL, + out_len); +} + +/*****************************************************************************/ + +static void +modm_clear_manager(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + if (!priv->modm.manager) + return; + nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_name_owner_changed_id); + nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_added_id); + nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_removed_id); + g_clear_object(&priv->modm.manager); +} + +static void +modm_handle_object_added(MMManager *modem_manager, MMObject *modem_object, NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + const char * path; + MMModem * modem_iface; + NMModem * modem; + GError * error = NULL; + + /* Ensure we don't have the same modem already */ + path = mm_object_get_path(modem_object); + if (g_hash_table_lookup(priv->modems, path)) { + _LOGW("modem with path %s already exists, ignoring", path); + return; + } + + /* Ensure we have the 'Modem' interface at least */ + modem_iface = mm_object_peek_modem(modem_object); + if (!modem_iface) { + _LOGW("modem with path %s doesn't have the Modem interface, ignoring", path); + return; + } + + /* Ensure we have a primary port reported */ + if (!mm_modem_get_primary_port(modem_iface)) { + _LOGW("modem with path %s has unknown primary port, ignoring", path); + return; + } + + /* Create a new modem object */ + modem = nm_modem_broadband_new(G_OBJECT(modem_object), &error); + if (modem) + handle_new_modem(self, modem); + else + _LOGW("failed to create modem: %s", error->message); + g_clear_error(&error); +} + +static void +modm_handle_object_removed(MMManager *manager, MMObject *modem_object, NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + NMModem * modem; + const char * path; + + path = mm_object_get_path(modem_object); + modem = (NMModem *) g_hash_table_lookup(priv->modems, path); + if (!modem) + return; + + nm_modem_emit_removed(modem); + g_hash_table_remove(priv->modems, path); +} + +static void +modm_manager_available(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + GList * modems, *l; + + if (priv->modm.log_available != LOG_AVAILABLE_YES) { + _LOGI("ModemManager %savailable", priv->modm.log_available ? "now " : ""); + priv->modm.log_available = LOG_AVAILABLE_YES; + } + + /* Update initial modems list */ + modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(priv->modm.manager)); + for (l = modems; l; l = g_list_next(l)) + modm_handle_object_added(priv->modm.manager, MM_OBJECT(l->data), self); + g_list_free_full(modems, (GDestroyNotify) g_object_unref); +} + +static void +modm_handle_name_owner_changed(MMManager *modem_manager, GParamSpec *pspec, NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + char * name_owner; + + /* Quit poking, if any */ + nm_clear_g_source(&priv->modm.relaunch_id); + + name_owner = + g_dbus_object_manager_client_get_name_owner(G_DBUS_OBJECT_MANAGER_CLIENT(modem_manager)); + if (!name_owner) { + if (priv->modm.log_available != LOG_AVAILABLE_NO) { + _LOGI("ModemManager %savailable", priv->modm.log_available ? "no longer " : "not "); + priv->modm.log_available = LOG_AVAILABLE_NO; + } + + /* If not managed by systemd, schedule relaunch */ + if (!sd_booted()) + modm_schedule_manager_relaunch(self, 0); + + return; + } + + /* Available! */ + g_free(name_owner); + + /* Hack alert: GDBusObjectManagerClient won't signal neither 'object-added' + * nor 'object-removed' if it was created while there was no ModemManager in + * the bus. This hack avoids this issue until we get a GIO with the fix + * included... */ + modm_clear_manager(self); + modm_ensure_manager(self); + + /* Whenever GDBusObjectManagerClient is fixed, we can just do the following: + * modm_manager_available (self); + */ +} + +static void +modm_manager_poke_cb(GObject *connection, GAsyncResult *res, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *result = NULL; + + result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(connection), res, &error); + + if (!result && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = user_data; + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_clear_object(&priv->modm.poke_cancellable); + + if (error) { + _LOGW("error poking ModemManager: %s", error->message); + + /* Don't reschedule poke is MM service doesn't exist. */ + if (!g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) + && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_SERVICE_NOT_FOUND)) { + /* Setup timeout to relaunch */ + modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL); + } + } +} + +static void +modm_manager_poke(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + nm_clear_g_cancellable(&priv->modm.poke_cancellable); + priv->modm.poke_cancellable = g_cancellable_new(); + + /* If there is no current owner right away, ensure we poke to get one */ + g_dbus_connection_call(priv->dbus_connection, + NM_MODEM_MANAGER_MM_DBUS_SERVICE, + NM_MODEM_MANAGER_MM_DBUS_PATH, + DBUS_INTERFACE_PEER, + "Ping", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->modm.poke_cancellable, + modm_manager_poke_cb, + self); +} + +static void +modm_manager_check_name_owner(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + gs_free char * name_owner = NULL; + + name_owner = g_dbus_object_manager_client_get_name_owner( + G_DBUS_OBJECT_MANAGER_CLIENT(priv->modm.manager)); + if (name_owner) { + modm_manager_available(self); + return; + } + + /* If the lifecycle is not managed by systemd, poke */ + if (!sd_booted()) + modm_manager_poke(self); +} + +static void +modm_manager_new_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + gs_free_error GError *error = NULL; + MMManager * modem_manager; + + modem_manager = mm_manager_new_finish(res, &error); + if (!modem_manager && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = user_data; + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + nm_assert(!priv->modm.manager); + + g_clear_object(&priv->main_cancellable); + + if (!modem_manager) { + /* We're not really supposed to get any error here. If we do get one, + * though, just re-schedule the MMManager creation after some time. + * During this period, name-owner changes won't be followed. */ + _LOGW("error creating ModemManager client: %s", error->message); + /* Setup timeout to relaunch */ + modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL); + return; + } + + priv->modm.manager = modem_manager; + + /* Setup signals in the GDBusObjectManagerClient */ + priv->modm.handle_name_owner_changed_id = + g_signal_connect(priv->modm.manager, + "notify::name-owner", + G_CALLBACK(modm_handle_name_owner_changed), + self); + priv->modm.handle_object_added_id = g_signal_connect(priv->modm.manager, + "object-added", + G_CALLBACK(modm_handle_object_added), + self); + priv->modm.handle_object_removed_id = g_signal_connect(priv->modm.manager, + "object-removed", + G_CALLBACK(modm_handle_object_removed), + self); + + modm_manager_check_name_owner(self); +} + +static void +modm_ensure_manager(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_assert(priv->dbus_connection); + + /* Create the GDBusObjectManagerClient. We do not request to autostart, as + * we don't really want the MMManager creation to fail. We can always poke + * later on if we want to request the autostart */ + if (!priv->modm.manager) { + if (!priv->main_cancellable) + priv->main_cancellable = g_cancellable_new(); + mm_manager_new(priv->dbus_connection, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + priv->main_cancellable, + modm_manager_new_cb, + self); + return; + } + + /* If already available, recheck name owner! */ + modm_manager_check_name_owner(self); +} + +static gboolean +modm_schedule_manager_relaunch_cb(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + priv->modm.relaunch_id = 0; + modm_ensure_manager(self); + return G_SOURCE_REMOVE; +} + +static void +modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + /* No need to pass an extra reference to self; timeout/idle will be + * cancelled if the object gets disposed. */ + if (n_seconds) + priv->modm.relaunch_id = + g_timeout_add_seconds(n_seconds, (GSourceFunc) modm_schedule_manager_relaunch_cb, self); + else + priv->modm.relaunch_id = g_idle_add((GSourceFunc) modm_schedule_manager_relaunch_cb, self); +} + +/*****************************************************************************/ + +static void +modm_proxy_name_owner_reset(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + char * name = NULL; + + if (priv->modm.proxy) + name = g_dbus_proxy_get_name_owner(priv->modm.proxy); + + if (nm_streq0(priv->modm.proxy_name_owner, name)) { + g_free(name); + return; + } + g_free(priv->modm.proxy_name_owner); + priv->modm.proxy_name_owner = name; + + _notify(self, PROP_NAME_OWNER); +} + +static void +modm_proxy_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data) +{ + modm_proxy_name_owner_reset(user_data); +} + +static void +modm_proxy_new_cb(GObject *source_object, GAsyncResult *result, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + GDBusProxy * proxy; + gs_free_error GError *error = NULL; + + proxy = g_dbus_proxy_new_for_bus_finish(result, &error); + if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = user_data; + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_clear_object(&priv->modm.proxy_cancellable); + + if (!proxy) { + _LOGW("could not obtain D-Bus proxy for ModemManager: %s", error->message); + return; + } + + priv->modm.proxy = proxy; + g_signal_connect(priv->modm.proxy, + "notify::g-name-owner", + G_CALLBACK(modm_proxy_name_owner_changed_cb), + self); + + modm_proxy_name_owner_reset(self); +} + +void +nm_modem_manager_name_owner_ref(NMModemManager *self) +{ + NMModemManagerPrivate *priv; + + g_return_if_fail(NM_IS_MODEM_MANAGER(self)); + + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + if (priv->modm.proxy_ref_count++ > 0) { + /* only try once to create the proxy. If proxy creation + * for the first "ref" failed, it's unclear what to do. + * The proxy is hosed. */ + return; + } + + nm_assert(!priv->modm.proxy && !priv->modm.proxy_cancellable); + + priv->modm.proxy_cancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES + | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS + | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + NM_MODEM_MANAGER_MM_DBUS_SERVICE, + NM_MODEM_MANAGER_MM_DBUS_PATH, + NM_MODEM_MANAGER_MM_DBUS_INTERFACE, + priv->modm.proxy_cancellable, + modm_proxy_new_cb, + self); +} + +void +nm_modem_manager_name_owner_unref(NMModemManager *self) +{ + NMModemManagerPrivate *priv; + + g_return_if_fail(NM_IS_MODEM_MANAGER(self)); + + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_return_if_fail(priv->modm.proxy_ref_count > 0); + + if (--priv->modm.proxy_ref_count > 0) + return; + + nm_clear_g_cancellable(&priv->modm.proxy_cancellable); + g_clear_object(&priv->modm.proxy); + + modm_proxy_name_owner_reset(self); +} + +const char * +nm_modem_manager_name_owner_get(NMModemManager *self) +{ + g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL); + nm_assert(NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_ref_count > 0); + + return NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_name_owner; +} + +/*****************************************************************************/ + +#if WITH_OFONO + +static void +ofono_create_modem(NMModemManager *self, const char *path) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + NMModem * modem = NULL; + + /* Ensure duplicate modems aren't created. Because we're not using the + * ObjectManager interface there's a race during oFono startup where we + * receive ModemAdded signals before GetModems() returns, so some of the + * modems returned from GetModems() may already have been created. + */ + if (!g_hash_table_lookup(priv->modems, path)) { + modem = nm_modem_ofono_new(path); + if (modem) + handle_new_modem(self, modem); + else + _LOGW("Failed to create oFono modem for %s", path); + } +} + +static void +ofono_signal_cb(GDBusProxy *proxy, + char * sender_name, + char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + NMModemManager * self = NM_MODEM_MANAGER(user_data); + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + char * object_path; + NMModem * modem; + + if (g_strcmp0(signal_name, "ModemAdded") == 0) { + g_variant_get(parameters, "(oa{sv})", &object_path, NULL); + _LOGI("oFono modem appeared: %s", object_path); + + ofono_create_modem(NM_MODEM_MANAGER(user_data), object_path); + g_free(object_path); + } else if (g_strcmp0(signal_name, "ModemRemoved") == 0) { + g_variant_get(parameters, "(o)", &object_path); + _LOGI("oFono modem removed: %s", object_path); + + modem = (NMModem *) g_hash_table_lookup(priv->modems, object_path); + if (modem) { + nm_modem_emit_removed(modem); + g_hash_table_remove(priv->modems, object_path); + } else { + _LOGW("could not remove modem %s, not found in table", object_path); + } + g_free(object_path); + } +} + +static void +ofono_enumerate_devices_done(GObject *proxy, GAsyncResult *res, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + gs_free_error GError *error = NULL; + GVariant * results; + GVariantIter * iter; + const char * path; + + results = g_dbus_proxy_call_finish(G_DBUS_PROXY(proxy), res, &error); + if (!results && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_MANAGER(user_data); + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_clear_object(&priv->ofono.cancellable); + + if (!results) { + _LOGW("failed to enumerate oFono devices: %s", error->message); + return; + } + + g_variant_get(results, "(a(oa{sv}))", &iter); + while (g_variant_iter_loop(iter, "(&oa{sv})", &path, NULL)) + ofono_create_modem(self, path); + g_variant_iter_free(iter); + g_variant_unref(results); +} + +static void +ofono_check_name_owner(NMModemManager *self, gboolean first_invocation) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + gs_free char * name_owner = NULL; + + name_owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(priv->ofono.proxy)); + if (name_owner) { + _LOGI("oFono is %savailable", first_invocation ? "" : "now "); + + nm_clear_g_cancellable(&priv->ofono.cancellable); + priv->ofono.cancellable = g_cancellable_new(); + + g_dbus_proxy_call(priv->ofono.proxy, + "GetModems", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + priv->ofono.cancellable, + ofono_enumerate_devices_done, + self); + } else { + GHashTableIter iter; + NMModem * modem; + + _LOGI("oFono is %savailable", first_invocation ? "not " : "no longer "); + + /* Remove any oFono modems that might be left around */ + g_hash_table_iter_init(&iter, priv->modems); + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &modem)) { + if (NM_IS_MODEM_OFONO(modem)) { + nm_modem_emit_removed(modem); + g_hash_table_iter_remove(&iter); + } + } + } +} + +static void +ofono_name_owner_changed(GDBusProxy *ofono_proxy, GParamSpec *pspec, NMModemManager *self) +{ + ofono_check_name_owner(self, FALSE); +} + +static void +ofono_proxy_new_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + gs_free_error GError *error = NULL; + GDBusProxy * proxy; + + proxy = g_dbus_proxy_new_finish(res, &error); + if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_MANAGER(user_data); + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + g_clear_object(&priv->ofono.cancellable); + + if (!proxy) { + _LOGW("error getting oFono bus proxy: %s", error->message); + return; + } + + priv->ofono.proxy = proxy; + + g_signal_connect(priv->ofono.proxy, + "notify::g-name-owner", + G_CALLBACK(ofono_name_owner_changed), + self); + + g_signal_connect(priv->ofono.proxy, "g-signal", G_CALLBACK(ofono_signal_cb), self); + + ofono_check_name_owner(self, TRUE); +} + +static void +ofono_init_proxy(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + nm_assert(priv->dbus_connection); + nm_assert(!priv->ofono.cancellable); + + priv->ofono.cancellable = g_cancellable_new(); + + g_dbus_proxy_new(priv->dbus_connection, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + OFONO_DBUS_SERVICE, + OFONO_DBUS_PATH, + OFONO_DBUS_INTERFACE, + priv->ofono.cancellable, + ofono_proxy_new_cb, + self); +} +#endif + +/*****************************************************************************/ + +static void +bus_get_ready(GObject *source, GAsyncResult *res, gpointer user_data) +{ + NMModemManager * self; + NMModemManagerPrivate *priv; + gs_free_error GError *error = NULL; + GDBusConnection * connection; + + connection = g_bus_get_finish(res, &error); + if (!connection && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_MANAGER(user_data); + priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + if (!connection) { + _LOGW("error getting bus connection: %s", error->message); + return; + } + + priv->dbus_connection = connection; + + modm_ensure_manager(self); +#if WITH_OFONO + ofono_init_proxy(self); +#endif +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMModemManager * self = NM_MODEM_MANAGER(object); + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_NAME_OWNER: + g_value_set_string(value, priv->modm.proxy_name_owner); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_modem_manager_init(NMModemManager *self) +{ + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + priv->modems = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_object_unref); + + priv->main_cancellable = g_cancellable_new(); + + g_bus_get(G_BUS_TYPE_SYSTEM, priv->main_cancellable, bus_get_ready, self); +} + +static void +dispose(GObject *object) +{ + NMModemManager * self = NM_MODEM_MANAGER(object); + NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self); + + nm_clear_g_cancellable(&priv->main_cancellable); + nm_clear_g_cancellable(&priv->modm.poke_cancellable); + + nm_clear_g_source(&priv->modm.relaunch_id); + + nm_clear_g_cancellable(&priv->modm.proxy_cancellable); + g_clear_object(&priv->modm.proxy); + nm_clear_g_free(&priv->modm.proxy_name_owner); + + modm_clear_manager(self); + +#if WITH_OFONO + if (priv->ofono.proxy) { + g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_name_owner_changed, self); + g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_signal_cb, self); + g_clear_object(&priv->ofono.proxy); + } + nm_clear_g_cancellable(&priv->ofono.cancellable); +#endif + + g_clear_object(&priv->dbus_connection); + + if (priv->modems) { + g_hash_table_foreach_remove(priv->modems, remove_one_modem, object); + g_hash_table_destroy(priv->modems); + priv->modems = NULL; + } + + G_OBJECT_CLASS(nm_modem_manager_parent_class)->dispose(object); +} + +static void +nm_modem_manager_class_init(NMModemManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->dispose = dispose; + object_class->get_property = get_property; + + obj_properties[PROP_NAME_OWNER] = + g_param_spec_string(NM_MODEM_MANAGER_NAME_OWNER, + "", + "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[MODEM_ADDED] = g_signal_new(NM_MODEM_MANAGER_MODEM_ADDED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + NM_TYPE_MODEM); +} diff --git a/src/core/devices/wwan/nm-modem-manager.h b/src/core/devices/wwan/nm-modem-manager.h new file mode 100644 index 00000000..e89a032b --- /dev/null +++ b/src/core/devices/wwan/nm-modem-manager.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2014 Red Hat, Inc. + * Copyright (C) 2009 Novell, Inc. + * Copyright (C) 2009 Canonical Ltd. + */ + +#ifndef __NETWORKMANAGER_MODEM_MANAGER_H__ +#define __NETWORKMANAGER_MODEM_MANAGER_H__ + +#include "nm-modem.h" + +#define NM_TYPE_MODEM_MANAGER (nm_modem_manager_get_type()) +#define NM_MODEM_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_MANAGER, NMModemManager)) +#define NM_MODEM_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_MANAGER, NMModemManagerClass)) +#define NM_IS_MODEM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_MANAGER)) +#define NM_IS_MODEM_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_MANAGER)) +#define NM_MODEM_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_MANAGER, NMModemManagerClass)) + +#define NM_MODEM_MANAGER_MODEM_ADDED "modem-added" + +#define NM_MODEM_MANAGER_NAME_OWNER "name-owner" + +#define NM_MODEM_MANAGER_MM_DBUS_SERVICE "org.freedesktop.ModemManager1" +#define NM_MODEM_MANAGER_MM_DBUS_PATH "/org/freedesktop/ModemManager1" +#define NM_MODEM_MANAGER_MM_DBUS_INTERFACE "org.freedesktop.ModemManager1" + +typedef struct _NMModemManager NMModemManager; +typedef struct _NMModemManagerClass NMModemManagerClass; + +GType nm_modem_manager_get_type(void); + +NMModemManager *nm_modem_manager_get(void); + +void nm_modem_manager_name_owner_ref(NMModemManager *self); +void nm_modem_manager_name_owner_unref(NMModemManager *self); + +const char *nm_modem_manager_name_owner_get(NMModemManager *self); + +NMModem **nm_modem_manager_get_modems(NMModemManager *self, guint *out_len); + +#endif /* __NETWORKMANAGER_MODEM_MANAGER_H__ */ diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c new file mode 100644 index 00000000..21734cee --- /dev/null +++ b/src/core/devices/wwan/nm-modem-ofono.c @@ -0,0 +1,1239 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - 2016 Canonical Ltd. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-modem-ofono.h" + +#include "nm-core-internal.h" +#include "devices/nm-device-private.h" +#include "nm-modem.h" +#include "platform/nm-platform.h" +#include "nm-ip4-config.h" + +#define VARIANT_IS_OF_TYPE_BOOLEAN(v) \ + ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_BOOLEAN))) +#define VARIANT_IS_OF_TYPE_STRING(v) \ + ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING))) +#define VARIANT_IS_OF_TYPE_OBJECT_PATH(v) \ + ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_OBJECT_PATH))) +#define VARIANT_IS_OF_TYPE_STRING_ARRAY(v) \ + ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING_ARRAY))) +#define VARIANT_IS_OF_TYPE_DICTIONARY(v) \ + ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_DICTIONARY))) + +/*****************************************************************************/ + +typedef struct { + GHashTable *connect_properties; + + GDBusProxy *modem_proxy; + GDBusProxy *connman_proxy; + GDBusProxy *context_proxy; + GDBusProxy *sim_proxy; + + GCancellable *modem_proxy_cancellable; + GCancellable *connman_proxy_cancellable; + GCancellable *context_proxy_cancellable; + GCancellable *sim_proxy_cancellable; + + GError *property_error; + + char *context_path; + char *imsi; + + gboolean modem_online; + gboolean gprs_attached; + + NMIP4Config *ip4_config; +} NMModemOfonoPrivate; + +struct _NMModemOfono { + NMModem parent; + NMModemOfonoPrivate _priv; +}; + +struct _NMModemOfonoClass { + NMModemClass parent; +}; + +G_DEFINE_TYPE(NMModemOfono, nm_modem_ofono, NM_TYPE_MODEM) + +#define NM_MODEM_OFONO_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMModemOfono, NM_IS_MODEM_OFONO) + +/*****************************************************************************/ + +#define _NMLOG_DOMAIN LOGD_MB +#define _NMLOG_PREFIX_NAME "modem-ofono" +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) { \ + NMModemOfono *const __self = (self); \ + char __prefix_name[128]; \ + const char * __uid; \ + \ + _nm_log(_level, \ + (_NMLOG_DOMAIN), \ + 0, \ + NULL, \ + NULL, \ + "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + (__self ? ({ \ + ((__uid = nm_modem_get_uid((NMModem *) __self)) \ + ? nm_sprintf_buf(__prefix_name, "[%s]", __uid) \ + : "(null)"); \ + }) \ + : "") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void +get_capabilities(NMModem * _self, + NMDeviceModemCapabilities *modem_caps, + NMDeviceModemCapabilities *current_caps) +{ + /* FIXME: auto-detect capabilities to allow LTE */ + *modem_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS; + *current_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS; +} + +static void +update_modem_state(NMModemOfono *self) +{ + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + NMModemState state = nm_modem_get_state(NM_MODEM(self)); + NMModemState new_state = NM_MODEM_STATE_DISABLED; + const char * reason = NULL; + + _LOGI("'Attached': %s 'Online': %s 'IMSI': %s", + priv->gprs_attached ? "true" : "false", + priv->modem_online ? "true" : "false", + priv->imsi); + + if (priv->modem_online == FALSE) { + reason = "modem 'Online=false'"; + } else if (priv->imsi == NULL && state != NM_MODEM_STATE_ENABLING) { + reason = "modem not ready"; + } else if (priv->gprs_attached == FALSE) { + new_state = NM_MODEM_STATE_SEARCHING; + reason = "modem searching"; + } else { + new_state = NM_MODEM_STATE_REGISTERED; + reason = "modem ready"; + } + + if (state != new_state) + nm_modem_set_state(NM_MODEM(self), new_state, reason); +} + +/* Disconnect */ +typedef struct { + NMModemOfono * self; + _NMModemDisconnectCallback callback; + gpointer callback_user_data; + GCancellable * cancellable; + gboolean warn; +} DisconnectContext; + +static void +disconnect_context_complete(DisconnectContext *ctx, GError *error) +{ + if (ctx->callback) + ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data); + nm_g_object_unref(ctx->cancellable); + g_object_unref(ctx->self); + g_slice_free(DisconnectContext, ctx); +} + +static void +disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable) +{ + DisconnectContext *ctx = user_data; + gs_free_error GError *error = NULL; + + if (!g_cancellable_set_error_if_cancelled(cancellable, &error)) { + g_set_error_literal(&error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + ("modem is currently not connected")); + } + disconnect_context_complete(ctx, error); +} + +static void +disconnect_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + DisconnectContext *ctx = user_data; + NMModemOfono * self = ctx->self; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *v = NULL; + + v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error); + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + disconnect_context_complete(ctx, error); + return; + } + + if (error && ctx->warn) + _LOGW("failed to disconnect modem: %s", error->message); + + _LOGD("modem disconnected"); + + update_modem_state(self); + disconnect_context_complete(ctx, error); +} + +static void +disconnect(NMModem * modem, + gboolean warn, + GCancellable * cancellable, + _NMModemDisconnectCallback callback, + gpointer user_data) +{ + NMModemOfono * self = NM_MODEM_OFONO(modem); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + DisconnectContext * ctx; + NMModemState state = nm_modem_get_state(NM_MODEM(self)); + + _LOGD("warn: %s modem_state: %s", warn ? "TRUE" : "FALSE", nm_modem_state_to_string(state)); + + ctx = g_slice_new0(DisconnectContext); + ctx->self = g_object_ref(self); + ctx->cancellable = nm_g_object_ref(cancellable); + ctx->warn = warn; + ctx->callback = callback; + ctx->callback_user_data = user_data; + + if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)) { + nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx); + return; + } + + nm_modem_set_state(NM_MODEM(self), + NM_MODEM_STATE_DISCONNECTING, + nm_modem_state_to_string(NM_MODEM_STATE_DISCONNECTING)); + + g_dbus_proxy_call(priv->context_proxy, + "SetProperty", + g_variant_new("(sv)", "Active", g_variant_new("b", warn)), + G_DBUS_CALL_FLAGS_NONE, + 20000, + ctx->cancellable, + disconnect_done, + ctx); +} + +static void +deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager) +{ + NMModemOfono * self = NM_MODEM_OFONO(modem); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + /* TODO: cancel SimpleConnect() if any */ + + g_clear_object(&priv->ip4_config); + + NM_MODEM_CLASS(nm_modem_ofono_parent_class) + ->deactivate_cleanup(modem, device, stop_ppp_manager); +} + +static gboolean +check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection, GError **error) +{ + NMModemOfono * self = NM_MODEM_OFONO(modem); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + const char * id; + + if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE, + "connection type %s is not supported by ofono modem", + nm_connection_get_connection_type(connection)); + return FALSE; + } + + if (!priv->imsi) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "modem has no IMSI"); + return FALSE; + } + + id = nm_connection_get_id(connection); + + if (!strstr(id, "/context")) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "the connection ID has no context"); + return FALSE; + } + + if (!strstr(id, priv->imsi)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "the connection ID does not contain the IMSI"); + return FALSE; + } + + return TRUE; +} + +static void +handle_sim_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + NMModemOfono * self = NM_MODEM_OFONO(user_data); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (g_strcmp0(property, "SubscriberIdentity") == 0 && VARIANT_IS_OF_TYPE_STRING(v)) { + gsize length; + const char *value_str = g_variant_get_string(v, &length); + + _LOGD("SubscriberIdentify found"); + + /* Check for empty DBus string value */ + if (length && g_strcmp0(value_str, "(null)") != 0 + && g_strcmp0(value_str, priv->imsi) != 0) { + if (priv->imsi != NULL) { + _LOGW("SimManager:'SubscriberIdentity' changed: %s", priv->imsi); + g_free(priv->imsi); + } + + priv->imsi = g_strdup(value_str); + update_modem_state(self); + } + } +} + +static void +sim_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + GVariant *v_child = g_variant_get_child_value(v, 0); + + handle_sim_property(proxy, property, v_child, user_data); + g_variant_unref(v_child); +} + +static void +sim_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *v_properties = NULL; + gs_unref_variant GVariant *v_dict = NULL; + GVariant * v; + GVariantIter i; + const char * property; + + v_properties = + _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error); + if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + g_clear_object(&priv->sim_proxy_cancellable); + + if (!v_properties) { + g_dbus_error_strip_remote_error(error); + _LOGW("error getting sim properties: %s", error->message); + return; + } + + _LOGD("sim v_properties is type: %s", g_variant_get_type_string(v_properties)); + + v_dict = g_variant_get_child_value(v_properties, 0); + if (!v_dict) { + _LOGW("error getting sim properties: no v_dict"); + return; + } + + _LOGD("sim v_dict is type: %s", g_variant_get_type_string(v_dict)); + + /* + * TODO: + * 1) optimize by looking up properties ( Online, Interfaces ), instead + * of iterating + * + * 2) reduce code duplication between all of the get_properties_done + * functions in this class. + */ + + g_variant_iter_init(&i, v_dict); + while (g_variant_iter_next(&i, "{&sv}", &property, &v)) { + handle_sim_property(NULL, property, v, self); + g_variant_unref(v); + } +} + +static void +_sim_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + GDBusProxy * proxy; + + proxy = g_dbus_proxy_new_for_bus_finish(result, &error); + if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = user_data; + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (!proxy) { + _LOGW("failed to create SimManager proxy: %s", error->message); + g_clear_object(&priv->sim_proxy_cancellable); + return; + } + + priv->sim_proxy = proxy; + + /* Watch for custom ofono PropertyChanged signals */ + _nm_dbus_signal_connect(priv->sim_proxy, + "PropertyChanged", + G_VARIANT_TYPE("(sv)"), + G_CALLBACK(sim_property_changed), + self); + + g_dbus_proxy_call(priv->sim_proxy, + "GetProperties", + NULL, + G_DBUS_CALL_FLAGS_NONE, + 20000, + priv->sim_proxy_cancellable, + sim_get_properties_done, + self); +} + +static void +handle_sim_iface(NMModemOfono *self, gboolean found) +{ + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + _LOGD("SimManager interface %sfound", found ? "" : "not "); + + if (!found && (priv->sim_proxy || priv->sim_proxy_cancellable)) { + _LOGI("SimManager interface disappeared"); + nm_clear_g_cancellable(&priv->sim_proxy_cancellable); + if (priv->sim_proxy) { + g_signal_handlers_disconnect_by_data(priv->sim_proxy, self); + g_clear_object(&priv->sim_proxy); + } + nm_clear_g_free(&priv->imsi); + update_modem_state(self); + } else if (found && (!priv->sim_proxy && !priv->sim_proxy_cancellable)) { + _LOGI("found new SimManager interface"); + + priv->sim_proxy_cancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES + | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, /* GDBusInterfaceInfo */ + OFONO_DBUS_SERVICE, + nm_modem_get_path(NM_MODEM(self)), + OFONO_DBUS_INTERFACE_SIM_MANAGER, + priv->sim_proxy_cancellable, /* GCancellable */ + _sim_proxy_new_cb, + self); + } +} + +static void +handle_connman_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + NMModemOfono * self = NM_MODEM_OFONO(user_data); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (g_strcmp0(property, "Attached") == 0 && VARIANT_IS_OF_TYPE_BOOLEAN(v)) { + gboolean attached = g_variant_get_boolean(v); + gboolean old_attached = priv->gprs_attached; + + _LOGD("Attached: %s", attached ? "True" : "False"); + + if (priv->gprs_attached != attached) { + priv->gprs_attached = attached; + + _LOGI("Attached %s -> %s", + old_attached ? "true" : "false", + attached ? "true" : "false"); + + update_modem_state(self); + } + } +} + +static void +connman_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + GVariant *v_child = g_variant_get_child_value(v, 0); + + handle_connman_property(proxy, property, v_child, user_data); + g_variant_unref(v_child); +} + +static void +connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *v_properties = NULL; + gs_unref_variant GVariant *v_dict = NULL; + GVariant * v; + GVariantIter i; + const char * property; + + v_properties = + _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error); + if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + g_clear_object(&priv->connman_proxy_cancellable); + + if (!v_properties) { + g_dbus_error_strip_remote_error(error); + _LOGW("error getting connman properties: %s", error->message); + return; + } + + v_dict = g_variant_get_child_value(v_properties, 0); + + /* + * TODO: + * 1) optimize by looking up properties ( Online, Interfaces ), instead + * of iterating + * + * 2) reduce code duplication between all of the get_properties_done + * functions in this class. + */ + + g_variant_iter_init(&i, v_dict); + while (g_variant_iter_next(&i, "{&sv}", &property, &v)) { + handle_connman_property(NULL, property, v, self); + g_variant_unref(v); + } +} + +static void +_connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + GDBusProxy * proxy; + + proxy = g_dbus_proxy_new_for_bus_finish(result, &error); + if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = user_data; + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (!proxy) { + _LOGW("failed to create ConnectionManager proxy: %s", error->message); + g_clear_object(&priv->connman_proxy_cancellable); + return; + } + + priv->connman_proxy = proxy; + + _nm_dbus_signal_connect(priv->connman_proxy, + "PropertyChanged", + G_VARIANT_TYPE("(sv)"), + G_CALLBACK(connman_property_changed), + self); + + g_dbus_proxy_call(priv->connman_proxy, + "GetProperties", + NULL, + G_DBUS_CALL_FLAGS_NONE, + 20000, + priv->connman_proxy_cancellable, + connman_get_properties_done, + self); +} + +static void +handle_connman_iface(NMModemOfono *self, gboolean found) +{ + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + _LOGD("ConnectionManager interface %sfound", found ? "" : "not "); + + if (!found && (priv->connman_proxy || priv->connman_proxy_cancellable)) { + _LOGI("ConnectionManager interface disappeared"); + nm_clear_g_cancellable(&priv->connman_proxy_cancellable); + if (priv->connman_proxy) { + g_signal_handlers_disconnect_by_data(priv->connman_proxy, self); + g_clear_object(&priv->connman_proxy); + } + + /* The connection manager proxy disappeared, we should + * consider the modem disabled. + */ + priv->gprs_attached = FALSE; + + update_modem_state(self); + } else if (found && (!priv->connman_proxy && !priv->connman_proxy_cancellable)) { + _LOGI("found new ConnectionManager interface"); + + priv->connman_proxy_cancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES + | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, /* GDBusInterfaceInfo */ + OFONO_DBUS_SERVICE, + nm_modem_get_path(NM_MODEM(self)), + OFONO_DBUS_INTERFACE_CONNECTION_MANAGER, + priv->connman_proxy_cancellable, + _connman_proxy_new_cb, + self); + } +} + +static void +handle_modem_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + NMModemOfono * self = NM_MODEM_OFONO(user_data); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if ((g_strcmp0(property, "Online") == 0) && VARIANT_IS_OF_TYPE_BOOLEAN(v)) { + gboolean online = g_variant_get_boolean(v); + + _LOGD("Online: %s", online ? "True" : "False"); + + if (online != priv->modem_online) { + priv->modem_online = online; + _LOGI("modem is now %s", online ? "Online" : "Offline"); + update_modem_state(self); + } + + } else if ((g_strcmp0(property, "Interfaces") == 0) && VARIANT_IS_OF_TYPE_STRING_ARRAY(v)) { + const char **array, **iter; + gboolean found_connman = FALSE; + gboolean found_sim = FALSE; + + _LOGD("Interfaces found"); + + array = g_variant_get_strv(v, NULL); + if (array) { + for (iter = array; *iter; iter++) { + if (g_strcmp0(OFONO_DBUS_INTERFACE_SIM_MANAGER, *iter) == 0) + found_sim = TRUE; + else if (g_strcmp0(OFONO_DBUS_INTERFACE_CONNECTION_MANAGER, *iter) == 0) + found_connman = TRUE; + } + g_free(array); + } + + handle_sim_iface(self, found_sim); + handle_connman_iface(self, found_connman); + } +} + +static void +modem_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + GVariant *v_child = g_variant_get_child_value(v, 0); + + handle_modem_property(proxy, property, v_child, user_data); + g_variant_unref(v_child); +} + +static void +modem_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *v_properties = NULL; + gs_unref_variant GVariant *v_dict = NULL; + GVariant * v; + GVariantIter i; + const char * property; + + v_properties = + _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error); + if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + g_clear_object(&priv->modem_proxy_cancellable); + + if (!v_properties) { + g_dbus_error_strip_remote_error(error); + _LOGW("error getting modem properties: %s", error->message); + return; + } + + v_dict = g_variant_get_child_value(v_properties, 0); + if (!v_dict) { + _LOGW("error getting modem properties: no v_dict"); + return; + } + + /* + * TODO: + * 1) optimize by looking up properties ( Online, Interfaces ), instead + * of iterating + * + * 2) reduce code duplication between all of the get_properties_done + * functions in this class. + */ + + g_variant_iter_init(&i, v_dict); + while (g_variant_iter_next(&i, "{&sv}", &property, &v)) { + handle_modem_property(NULL, property, v, self); + g_variant_unref(v); + } +} + +static void +stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + gs_unref_variant GVariant *v = NULL; + + v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error); + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + g_clear_object(&priv->context_proxy_cancellable); + + nm_clear_pointer(&priv->connect_properties, g_hash_table_destroy); + + if (error) { + _LOGW("connection failed: %s", error->message); + + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY); + /* + * FIXME: add code to check for InProgress so that the + * connection doesn't continue to try and activate, + * leading to the connection being disabled, and a 5m + * timeout... + */ + } +} + +static void +context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data) +{ + NMModemOfono * self = NM_MODEM_OFONO(user_data); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + NMPlatformIP4Address addr; + gboolean ret = FALSE; + gs_unref_variant GVariant *v_dict = NULL; + const char * interface; + const char * s; + const char ** array, **iter; + guint32 address_network, gateway_network; + guint32 ip4_route_table, ip4_route_metric; + int ifindex; + GError * error = NULL; + + _LOGD("PropertyChanged: %s", property); + + /* + * TODO: might be a good idea and re-factor this to mimic bluez-device, + * ie. have this function just check the key, and call a sub-func to + * handle the action. + */ + + if (g_strcmp0(property, "Settings") != 0) + return; + + v_dict = g_variant_get_child_value(v, 0); + if (!v_dict) { + _LOGW("error getting IPv4 Settings: no v_dict"); + goto out; + } + + _LOGI("IPv4 static Settings:"); + + if (!g_variant_lookup(v_dict, "Interface", "&s", &interface)) { + _LOGW("Settings 'Interface' missing"); + goto out; + } + + _LOGD("Interface: %s", interface); + if (!nm_modem_set_data_port(NM_MODEM(self), + NM_PLATFORM_GET, + interface, + NM_MODEM_IP_METHOD_STATIC, + NM_MODEM_IP_METHOD_UNKNOWN, + 0, + &error)) { + _LOGW("failed to connect to modem: %s", error->message); + g_clear_error(&error); + goto out; + } + + ifindex = nm_modem_get_ip_ifindex(NM_MODEM(self)); + nm_assert(ifindex > 0); + + /* TODO: verify handling of ip4_config; check other places it's used... */ + g_clear_object(&priv->ip4_config); + + priv->ip4_config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), ifindex); + + if (!g_variant_lookup(v_dict, "Address", "&s", &s)) { + _LOGW("Settings 'Address' missing"); + goto out; + } + if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) { + _LOGW("can't convert 'Address' %s to addr", s ?: ""); + goto out; + } + memset(&addr, 0, sizeof(addr)); + addr.ifindex = ifindex; + addr.address = address_network; + addr.addr_source = NM_IP_CONFIG_SOURCE_WWAN; + + if (!g_variant_lookup(v_dict, "Netmask", "&s", &s)) { + _LOGW("Settings 'Netmask' missing"); + goto out; + } + if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) { + _LOGW("invalid 'Netmask': %s", s ?: ""); + goto out; + } + addr.plen = nm_utils_ip4_netmask_to_prefix(address_network); + + _LOGI("Address: %s", nm_platform_ip4_address_to_string(&addr, NULL, 0)); + nm_ip4_config_add_address(priv->ip4_config, &addr); + + if (!g_variant_lookup(v_dict, "Gateway", "&s", &s) || !s) { + _LOGW("Settings 'Gateway' missing"); + goto out; + } + if (!nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &gateway_network)) { + _LOGW("invalid 'Gateway': %s", s); + goto out; + } + nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL); + { + const NMPlatformIP4Route r = { + .rt_source = NM_IP_CONFIG_SOURCE_WWAN, + .gateway = gateway_network, + .table_coerced = nm_platform_route_table_coerce(ip4_route_table), + .metric = ip4_route_metric, + }; + + _LOGI("Gateway: %s", s); + nm_ip4_config_add_route(priv->ip4_config, &r, NULL); + } + + if (!g_variant_lookup(v_dict, "DomainNameServers", "^a&s", &array)) { + _LOGW("Settings 'DomainNameServers' missing"); + goto out; + } + if (array) { + for (iter = array; *iter; iter++) { + if (nm_utils_parse_inaddr_bin(AF_INET, *iter, NULL, &address_network) + && address_network) { + _LOGI("DNS: %s", *iter); + nm_ip4_config_add_nameserver(priv->ip4_config, address_network); + } else { + _LOGW("invalid NameServer: %s", *iter); + } + } + + if (iter == array) { + _LOGW("Settings: 'DomainNameServers': none specified"); + g_free(array); + goto out; + } + g_free(array); + } + + if (g_variant_lookup(v_dict, "MessageProxy", "&s", &s)) { + _LOGI("MessageProxy: %s", s); + if (s && nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) { + nm_modem_get_route_parameters(NM_MODEM(self), + &ip4_route_table, + &ip4_route_metric, + NULL, + NULL); + + { + const NMPlatformIP4Route mms_route = { + .network = address_network, + .plen = 32, + .gateway = gateway_network, + .table_coerced = nm_platform_route_table_coerce(ip4_route_table), + .metric = ip4_route_metric, + }; + + nm_ip4_config_add_route(priv->ip4_config, &mms_route, NULL); + } + } else { + _LOGW("invalid MessageProxy: %s", s); + } + } + + ret = TRUE; + +out: + if (nm_modem_get_state(NM_MODEM(self)) != NM_MODEM_STATE_CONNECTED) { + _LOGI("emitting PREPARE_RESULT: %s", ret ? "TRUE" : "FALSE"); + nm_modem_emit_prepare_result(NM_MODEM(self), + ret, + ret ? NM_DEVICE_STATE_REASON_NONE + : NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + } else { + _LOGW("MODEM_PPP_FAILED"); + nm_modem_emit_ppp_failed(NM_MODEM(self), NM_DEVICE_STATE_REASON_PPP_FAILED); + } +} + +static NMActStageReturn +static_stage3_ip4_config_start(NMModem * modem, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason) +{ + NMModemOfono * self = NM_MODEM_OFONO(modem); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + GError * error = NULL; + + if (!priv->ip4_config) { + _LOGD("IP4 config not ready(?)"); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + _LOGD("IP4 config is done; setting modem_state -> CONNECTED"); + g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, priv->ip4_config, error); + + /* Signal listener takes ownership of the IP4Config */ + priv->ip4_config = NULL; + + nm_modem_set_state(NM_MODEM(self), + NM_MODEM_STATE_CONNECTED, + nm_modem_state_to_string(NM_MODEM_STATE_CONNECTED)); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + GDBusProxy * proxy; + + proxy = g_dbus_proxy_new_for_bus_finish(result, &error); + if (!proxy || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (!proxy) { + _LOGE("failed to create ofono ConnectionContext DBus proxy: %s", error->message); + g_clear_object(&priv->context_proxy_cancellable); + nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY); + return; + } + + priv->context_proxy = proxy; + + if (!priv->gprs_attached) { + g_clear_object(&priv->context_proxy_cancellable); + nm_modem_emit_prepare_result(NM_MODEM(self), + FALSE, + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER); + return; + } + + /* We have an old copy of the settings from a previous activation, + * clear it so that we can gate getting the IP config from oFono + * on whether or not we have already received them + */ + g_clear_object(&priv->ip4_config); + + _nm_dbus_signal_connect(priv->context_proxy, + "PropertyChanged", + G_VARIANT_TYPE("(sv)"), + G_CALLBACK(context_property_changed), + self); + + g_dbus_proxy_call(priv->context_proxy, + "SetProperty", + g_variant_new("(sv)", "Active", g_variant_new("b", TRUE)), + G_DBUS_CALL_FLAGS_NONE, + 20000, + priv->context_proxy_cancellable, + stage1_prepare_done, + self); +} + +static void +do_context_activate(NMModemOfono *self) +{ + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + g_return_if_fail(NM_IS_MODEM_OFONO(self)); + + nm_clear_g_cancellable(&priv->context_proxy_cancellable); + g_clear_object(&priv->context_proxy); + + priv->context_proxy_cancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + OFONO_DBUS_SERVICE, + priv->context_path, + OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT, + priv->context_proxy_cancellable, + context_proxy_new_cb, + self); +} + +static GHashTable * +create_connect_properties(NMConnection *connection) +{ + NMSettingGsm *setting; + GHashTable * properties; + const char * str; + + setting = nm_connection_get_setting_gsm(connection); + properties = g_hash_table_new(nm_str_hash, g_str_equal); + + str = nm_setting_gsm_get_apn(setting); + if (str) + g_hash_table_insert(properties, "AccessPointName", g_strdup(str)); + + str = nm_setting_gsm_get_username(setting); + if (str) + g_hash_table_insert(properties, "Username", g_strdup(str)); + + str = nm_setting_gsm_get_password(setting); + if (str) + g_hash_table_insert(properties, "Password", g_strdup(str)); + + return properties; +} + +static NMActStageReturn +modem_act_stage1_prepare(NMModem * modem, + NMConnection * connection, + NMDeviceStateReason *out_failure_reason) +{ + NMModemOfono * self = NM_MODEM_OFONO(modem); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + const char * context_id; + char ** id = NULL; + + context_id = nm_connection_get_id(connection); + id = g_strsplit(context_id, "/", 0); + g_return_val_if_fail(id[2], NM_ACT_STAGE_RETURN_FAILURE); + + _LOGD("trying %s %s", id[1], id[2]); + + g_free(priv->context_path); + priv->context_path = g_strdup_printf("%s/%s", nm_modem_get_path(modem), id[2]); + g_strfreev(id); + + if (!priv->context_path) { + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_GSM_APN_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (priv->connect_properties) + g_hash_table_destroy(priv->connect_properties); + + priv->connect_properties = create_connect_properties(connection); + + _LOGI("activating context %s", priv->context_path); + + if (nm_modem_get_state(modem) == NM_MODEM_STATE_REGISTERED) { + do_context_activate(self); + } else { + _LOGW("could not activate context: modem is not registered."); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +static void +modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMModemOfono * self; + NMModemOfonoPrivate *priv; + gs_free_error GError *error = NULL; + GDBusProxy * proxy; + + proxy = g_dbus_proxy_new_for_bus_finish(result, &error); + if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + self = NM_MODEM_OFONO(user_data); + priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + if (!proxy) { + _LOGE("failed to create ofono modem DBus proxy: %s", error->message); + g_clear_object(&priv->modem_proxy_cancellable); + return; + } + + priv->modem_proxy = proxy; + + _nm_dbus_signal_connect(priv->modem_proxy, + "PropertyChanged", + G_VARIANT_TYPE("(sv)"), + G_CALLBACK(modem_property_changed), + self); + + g_dbus_proxy_call(priv->modem_proxy, + "GetProperties", + NULL, + G_DBUS_CALL_FLAGS_NONE, + 20000, + priv->modem_proxy_cancellable, + modem_get_properties_done, + self); +} + +/*****************************************************************************/ + +static void +nm_modem_ofono_init(NMModemOfono *self) +{} + +static void +constructed(GObject *object) +{ + NMModemOfono * self = NM_MODEM_OFONO(object); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + priv->modem_proxy_cancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + OFONO_DBUS_SERVICE, + nm_modem_get_path(NM_MODEM(self)), + OFONO_DBUS_INTERFACE_MODEM, + priv->modem_proxy_cancellable, + modem_proxy_new_cb, + self); + + G_OBJECT_CLASS(nm_modem_ofono_parent_class)->constructed(object); +} + +NMModem * +nm_modem_ofono_new(const char *path) +{ + gs_free char *basename = NULL; + + g_return_val_if_fail(path != NULL, NULL); + + nm_log_info(LOGD_MB, "ofono: creating new Ofono modem path %s", path); + + /* Use short modem name (not its object path) as the NM device name (which + * comes from NM_MODEM_UID)and the device ID. + */ + basename = g_path_get_basename(path); + + return g_object_new(NM_TYPE_MODEM_OFONO, + NM_MODEM_PATH, + path, + NM_MODEM_UID, + basename, + NM_MODEM_DEVICE_ID, + basename, + NM_MODEM_CONTROL_PORT, + "ofono", /* mandatory */ + NM_MODEM_DRIVER, + "ofono", + NM_MODEM_STATE, + (int) NM_MODEM_STATE_INITIALIZING, + NULL); +} + +static void +dispose(GObject *object) +{ + NMModemOfono * self = NM_MODEM_OFONO(object); + NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self); + + nm_clear_g_cancellable(&priv->modem_proxy_cancellable); + nm_clear_g_cancellable(&priv->connman_proxy_cancellable); + nm_clear_g_cancellable(&priv->context_proxy_cancellable); + nm_clear_g_cancellable(&priv->sim_proxy_cancellable); + + if (priv->connect_properties) { + g_hash_table_destroy(priv->connect_properties); + priv->connect_properties = NULL; + } + + g_clear_object(&priv->ip4_config); + + if (priv->modem_proxy) { + g_signal_handlers_disconnect_by_data(priv->modem_proxy, self); + g_clear_object(&priv->modem_proxy); + } + + if (priv->connman_proxy) { + g_signal_handlers_disconnect_by_data(priv->connman_proxy, self); + g_clear_object(&priv->connman_proxy); + } + + if (priv->context_proxy) { + g_signal_handlers_disconnect_by_data(priv->context_proxy, self); + g_clear_object(&priv->context_proxy); + } + + if (priv->sim_proxy) { + g_signal_handlers_disconnect_by_data(priv->sim_proxy, self); + g_clear_object(&priv->sim_proxy); + } + + g_free(priv->imsi); + priv->imsi = NULL; + + G_OBJECT_CLASS(nm_modem_ofono_parent_class)->dispose(object); +} + +static void +nm_modem_ofono_class_init(NMModemOfonoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + NMModemClass *modem_class = NM_MODEM_CLASS(klass); + + object_class->constructed = constructed; + object_class->dispose = dispose; + + modem_class->get_capabilities = get_capabilities; + modem_class->disconnect = disconnect; + modem_class->deactivate_cleanup = deactivate_cleanup; + modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem; + + modem_class->modem_act_stage1_prepare = modem_act_stage1_prepare; + modem_class->static_stage3_ip4_config_start = static_stage3_ip4_config_start; +} diff --git a/src/core/devices/wwan/nm-modem-ofono.h b/src/core/devices/wwan/nm-modem-ofono.h new file mode 100644 index 00000000..260e3954 --- /dev/null +++ b/src/core/devices/wwan/nm-modem-ofono.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2013 - Canonical Ltd. + */ + +#ifndef NM_MODEM_OFONO_H +#define NM_MODEM_OFONO_H + +#include "nm-modem.h" + +#define NM_TYPE_MODEM_OFONO (nm_modem_ofono_get_type()) +#define NM_MODEM_OFONO(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_OFONO, NMModemOfono)) +#define NM_IS_MODEM_OFONO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_OFONO)) +#define NM_MODEM_OFONO_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_OFONO, NMModemOfonoClass)) +#define NM_IS_MODEM_OFONO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_OFONO)) +#define NM_MODEM_OFONO_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_OFONO, NMModemOfonoClass)) + +#define OFONO_DBUS_SERVICE "org.ofono" +#define OFONO_DBUS_PATH "/" +#define OFONO_DBUS_INTERFACE "org.ofono.Manager" +#define OFONO_DBUS_INTERFACE_MODEM "org.ofono.Modem" +#define OFONO_DBUS_INTERFACE_CONNECTION_MANAGER "org.ofono.ConnectionManager" +#define OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT "org.ofono.ConnectionContext" +#define OFONO_DBUS_INTERFACE_SIM_MANAGER "org.ofono.SimManager" + +typedef struct _NMModemOfono NMModemOfono; +typedef struct _NMModemOfonoClass NMModemOfonoClass; + +GType nm_modem_ofono_get_type(void); + +NMModem *nm_modem_ofono_new(const char *path); + +#endif /* NM_MODEM_OFONO_H */ diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c new file mode 100644 index 00000000..0d334fa4 --- /dev/null +++ b/src/core/devices/wwan/nm-modem.c @@ -0,0 +1,2037 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2014 Red Hat, Inc. + * Copyright (C) 2009 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-modem.h" + +#include <fcntl.h> +#include <termios.h> +#include <linux/rtnetlink.h> + +#include "nm-core-internal.h" +#include "platform/nm-platform.h" +#include "nm-setting-connection.h" +#include "NetworkManagerUtils.h" +#include "devices/nm-device-private.h" +#include "nm-netns.h" +#include "nm-act-request.h" +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" +#include "ppp/nm-ppp-manager-call.h" +#include "ppp/nm-ppp-status.h" + +/*****************************************************************************/ + +NM_GOBJECT_PROPERTIES_DEFINE(NMModem, + PROP_CONTROL_PORT, + PROP_IP_IFINDEX, + PROP_PATH, + PROP_UID, + PROP_DRIVER, + PROP_STATE, + PROP_DEVICE_ID, + PROP_SIM_ID, + PROP_IP_TYPES, + PROP_SIM_OPERATOR_ID, + PROP_OPERATOR_CODE, + PROP_APN, ); + +enum { + PPP_STATS, + PPP_FAILED, + PREPARE_RESULT, + IP4_CONFIG_RESULT, + IP6_CONFIG_RESULT, + AUTH_REQUESTED, + AUTH_RESULT, + REMOVED, + STATE_CHANGED, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = {0}; + +typedef struct _NMModemPrivate { + char *uid; + char *path; + char *driver; + char *control_port; + char *data_port; + + /* TODO: ip_iface is solely used for nm_modem_owns_port(). + * We should rework the code that it's not necessary */ + char *ip_iface; + + int ip_ifindex; + NMModemIPMethod ip4_method; + NMModemIPMethod ip6_method; + NMUtilsIPv6IfaceId iid; + NMModemState state; + NMModemState prev_state; /* revert to this state if enable/disable fails */ + char * device_id; + char * sim_id; + NMModemIPType ip_types; + char * sim_operator_id; + char * operator_code; + char * apn; + + NMPPPManager *ppp_manager; + + NMActRequest * act_request; + guint32 secrets_tries; + NMActRequestGetSecretsCallId *secrets_id; + + guint mm_ip_timeout; + + guint32 ip4_route_table; + guint32 ip4_route_metric; + guint32 ip6_route_table; + guint32 ip6_route_metric; + + /* PPP stats */ + guint32 in_bytes; + guint32 out_bytes; + + bool claimed : 1; +} NMModemPrivate; + +G_DEFINE_TYPE(NMModem, nm_modem, G_TYPE_OBJECT) + +#define NM_MODEM_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMModem, NM_IS_MODEM) + +/*****************************************************************************/ + +#define _NMLOG_PREFIX_BUFLEN 64 +#define _NMLOG_PREFIX_NAME "modem" +#define _NMLOG_DOMAIN LOGD_MB + +static const char * +_nmlog_prefix(char *prefix, NMModem *self) +{ + const char *uuid; + int c; + + if (!self) + return ""; + + uuid = nm_modem_get_uid(self); + + if (uuid) { + char pp[_NMLOG_PREFIX_BUFLEN - 5]; + + c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%s]", nm_strquote(pp, sizeof(pp), uuid)); + } else + c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%p]", self); + nm_assert(c < _NMLOG_PREFIX_BUFLEN); + + return prefix; +} + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + char _prefix[_NMLOG_PREFIX_BUFLEN]; \ + \ + nm_log((level), \ + _NMLOG_DOMAIN, \ + NULL, \ + NULL, \ + "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + _nmlog_prefix(_prefix, (self)) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static void _set_ip_ifindex(NMModem *self, int ifindex, const char *ifname); + +/*****************************************************************************/ +/* State/enabled/connected */ + +static const char *state_table[] = { + [NM_MODEM_STATE_UNKNOWN] = "unknown", + [NM_MODEM_STATE_FAILED] = "failed", + [NM_MODEM_STATE_INITIALIZING] = "initializing", + [NM_MODEM_STATE_LOCKED] = "locked", + [NM_MODEM_STATE_DISABLED] = "disabled", + [NM_MODEM_STATE_DISABLING] = "disabling", + [NM_MODEM_STATE_ENABLING] = "enabling", + [NM_MODEM_STATE_ENABLED] = "enabled", + [NM_MODEM_STATE_SEARCHING] = "searching", + [NM_MODEM_STATE_REGISTERED] = "registered", + [NM_MODEM_STATE_DISCONNECTING] = "disconnecting", + [NM_MODEM_STATE_CONNECTING] = "connecting", + [NM_MODEM_STATE_CONNECTED] = "connected", +}; + +const char * +nm_modem_state_to_string(NMModemState state) +{ + if ((gsize) state < G_N_ELEMENTS(state_table)) + return state_table[state]; + return NULL; +} + +/*****************************************************************************/ + +gboolean +nm_modem_is_claimed(NMModem *self) +{ + g_return_val_if_fail(NM_IS_MODEM(self), FALSE); + + return NM_MODEM_GET_PRIVATE(self)->claimed; +} + +NMModem * +nm_modem_claim(NMModem *self) +{ + NMModemPrivate *priv; + + g_return_val_if_fail(NM_IS_MODEM(self), NULL); + + priv = NM_MODEM_GET_PRIVATE(self); + + g_return_val_if_fail(!priv->claimed, NULL); + + priv->claimed = TRUE; + return g_object_ref(self); +} + +void +nm_modem_unclaim(NMModem *self) +{ + NMModemPrivate *priv; + + g_return_if_fail(NM_IS_MODEM(self)); + + priv = NM_MODEM_GET_PRIVATE(self); + + g_return_if_fail(priv->claimed); + + /* we don't actually unclaim the instance. This instance should not be re-used + * by another owner, that is because we only claim modems as we receive them. + * There is no mechanism that somebody else would later re-use them again. + * + * // priv->claimed = FALSE; */ + + g_object_unref(self); +} + +/*****************************************************************************/ + +NMModemState +nm_modem_get_state(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->state; +} + +void +nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + NMModemState old_state = priv->state; + + priv->prev_state = NM_MODEM_STATE_UNKNOWN; + + if (new_state != old_state) { + _LOGI("modem state changed, '%s' --> '%s' (reason: %s)", + nm_modem_state_to_string(old_state), + nm_modem_state_to_string(new_state), + reason ?: "none"); + + priv->state = new_state; + _notify(self, PROP_STATE); + g_signal_emit(self, signals[STATE_CHANGED], 0, (int) new_state, (int) old_state); + } +} + +void +nm_modem_set_prev_state(NMModem *self, const char *reason) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + /* Reset modem to previous state if the state hasn't already changed */ + if (priv->prev_state != NM_MODEM_STATE_UNKNOWN) + nm_modem_set_state(self, priv->prev_state, reason); +} + +void +nm_modem_set_mm_enabled(NMModem *self, gboolean enabled) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + NMModemState prev_state = priv->state; + + if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) { + _LOGD("cannot enable modem: already enabled"); + return; + } + if (!enabled && priv->state <= NM_MODEM_STATE_DISABLING) { + _LOGD("cannot disable modem: already disabled"); + return; + } + + if (priv->state <= NM_MODEM_STATE_INITIALIZING) { + _LOGD("cannot enable/disable modem: initializing or failed"); + return; + } else if (priv->state == NM_MODEM_STATE_LOCKED) { + /* Don't try to enable if the modem is locked since that will fail */ + _LOGW("cannot enable/disable modem: locked"); + + /* Try to unlock the modem if it's being enabled */ + if (enabled) + g_signal_emit(self, signals[AUTH_REQUESTED], 0); + return; + } + + /* Not all modem classes support set_mm_enabled */ + if (NM_MODEM_GET_CLASS(self)->set_mm_enabled) + NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled); + + /* Pre-empt the state change signal */ + nm_modem_set_state(self, + enabled ? NM_MODEM_STATE_ENABLING : NM_MODEM_STATE_DISABLING, + "user preference"); + priv->prev_state = prev_state; +} + +void +nm_modem_emit_removed(NMModem *self) +{ + g_signal_emit(self, signals[REMOVED], 0); +} + +void +nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason) +{ + nm_assert(NM_IS_MODEM(self)); + + g_signal_emit(self, signals[PREPARE_RESULT], 0, success, (guint) reason); +} + +void +nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason) +{ + nm_assert(NM_IS_MODEM(self)); + + g_signal_emit(self, signals[PPP_FAILED], 0, (guint) reason); +} + +NMModemIPType +nm_modem_get_supported_ip_types(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->ip_types; +} + +const char * +nm_modem_ip_type_to_string(NMModemIPType ip_type) +{ + switch (ip_type) { + case NM_MODEM_IP_TYPE_IPV4: + return "ipv4"; + case NM_MODEM_IP_TYPE_IPV6: + return "ipv6"; + case NM_MODEM_IP_TYPE_IPV4V6: + return "ipv4v6"; + default: + g_return_val_if_reached("unknown"); + } +} + +static GArray * +build_single_ip_type_array(NMModemIPType type) +{ + return g_array_append_val(g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 1), type); +} + +/** + * nm_modem_get_connection_ip_type: + * @self: the #NMModem + * @connection: the #NMConnection to determine IP type to use + * + * Given a modem and a connection, determine which #NMModemIPTypes to use + * when connecting. + * + * Returns: an array of #NMModemIpType values, in the order in which they + * should be tried. + */ +GArray * +nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error) +{ + NMModemPrivate * priv = NM_MODEM_GET_PRIVATE(self); + NMSettingIPConfig *s_ip4, *s_ip6; + const char * method; + gboolean ip4 = TRUE, ip6 = TRUE; + gboolean ip4_may_fail = TRUE, ip6_may_fail = TRUE; + + s_ip4 = nm_connection_get_setting_ip4_config(connection); + if (s_ip4) { + method = nm_setting_ip_config_get_method(s_ip4); + if (g_strcmp0(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) == 0) + ip4 = FALSE; + ip4_may_fail = nm_setting_ip_config_get_may_fail(s_ip4); + } + + s_ip6 = nm_connection_get_setting_ip6_config(connection); + if (s_ip6) { + method = nm_setting_ip_config_get_method(s_ip6); + if (NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) + ip6 = FALSE; + ip6_may_fail = nm_setting_ip_config_get_may_fail(s_ip6); + } + + if (ip4 && !ip6) { + if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV4)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Connection requested IPv4 but IPv4 is " + "unsupported by the modem."); + return NULL; + } + return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV4); + } + + if (ip6 && !ip4) { + if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV6)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Connection requested IPv6 but IPv6 is " + "unsupported by the modem."); + return NULL; + } + return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV6); + } + + if (ip4 && ip6) { + NMModemIPType type; + GArray * out; + + out = g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 3); + + /* Modem supports dual-stack? */ + if (priv->ip_types & NM_MODEM_IP_TYPE_IPV4V6) { + type = NM_MODEM_IP_TYPE_IPV4V6; + g_array_append_val(out, type); + } + + /* If IPv6 may-fail=false, we should NOT try IPv4 as fallback */ + if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV4) && ip6_may_fail) { + type = NM_MODEM_IP_TYPE_IPV4; + g_array_append_val(out, type); + } + + /* If IPv4 may-fail=false, we should NOT try IPv6 as fallback */ + if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV6) && ip4_may_fail) { + type = NM_MODEM_IP_TYPE_IPV6; + g_array_append_val(out, type); + } + + if (out->len > 0) + return out; + + /* Error... */ + g_array_unref(out); + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Connection requested both IPv4 and IPv6 " + "but dual-stack addressing is unsupported " + "by the modem."); + return NULL; + } + + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION, + "Connection specified no IP configuration!"); + return NULL; +} + +const char * +nm_modem_get_device_id(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->device_id; +} + +const char * +nm_modem_get_sim_id(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->sim_id; +} + +const char * +nm_modem_get_sim_operator_id(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->sim_operator_id; +} + +const char * +nm_modem_get_operator_code(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->operator_code; +} + +const char * +nm_modem_get_apn(NMModem *self) +{ + return NM_MODEM_GET_PRIVATE(self)->apn; +} + +/*****************************************************************************/ +/* IP method PPP */ + +static void +ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data) +{ + switch (status) { + case NM_PPP_STATUS_DISCONNECT: + nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_DISCONNECT); + break; + case NM_PPP_STATUS_DEAD: + nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_FAILED); + break; + default: + break; + } +} + +static void +ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data) +{ + NMModem *self = NM_MODEM(user_data); + + nm_assert(ifindex >= 0); + nm_assert(NM_MODEM_GET_PRIVATE(self)->ppp_manager == ppp_manager); + + if (ifindex <= 0 && iface) { + /* this might happen, if the ifname was already deleted + * and we failed to resolve ifindex. + * + * Forget about the name. */ + iface = NULL; + } + _set_ip_ifindex(self, ifindex, iface); +} + +static void +ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data) +{ + NMModem *self = NM_MODEM(user_data); + guint32 i, num; + guint32 bad_dns1 = htonl(0x0A0B0C0D); + guint32 good_dns1 = htonl(0x04020201); /* GTE nameserver */ + guint32 bad_dns2 = htonl(0x0A0B0C0E); + guint32 good_dns2 = htonl(0x04020202); /* GTE nameserver */ + gboolean dns_workaround = FALSE; + + /* Work around a PPP bug (#1732) which causes many mobile broadband + * providers to return 10.11.12.13 and 10.11.12.14 for the DNS servers. + * Apparently fixed in ppp-2.4.5 but we've had some reports that this is + * not the case. + * + * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=2e09ef6886bbf00bc5a9a641110f801e372ffde6 + * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=f8191bf07df374f119a07910a79217c7618f113e + */ + + num = nm_ip4_config_get_num_nameservers(config); + if (num == 2) { + gboolean found1 = FALSE, found2 = FALSE; + + for (i = 0; i < num; i++) { + guint32 ns = nm_ip4_config_get_nameserver(config, i); + + if (ns == bad_dns1) + found1 = TRUE; + else if (ns == bad_dns2) + found2 = TRUE; + } + + /* Be somewhat conservative about substitutions; the "bad" nameservers + * could actually be valid in some cases, so only substitute if ppp + * returns *only* the two bad nameservers. + */ + dns_workaround = (found1 && found2); + } + + if (!num || dns_workaround) { + _LOGW("compensating for invalid PPP-provided nameservers"); + nm_ip4_config_reset_nameservers(config); + nm_ip4_config_add_nameserver(config, good_dns1); + nm_ip4_config_add_nameserver(config, good_dns2); + } + + g_signal_emit(self, signals[IP4_CONFIG_RESULT], 0, config, NULL); +} + +static void +ppp_ip6_config(NMPPPManager * ppp_manager, + const NMUtilsIPv6IfaceId *iid, + NMIP6Config * config, + gpointer user_data) +{ + NMModem *self = NM_MODEM(user_data); + + NM_MODEM_GET_PRIVATE(self)->iid = *iid; + + nm_modem_emit_ip6_config_result(self, config, NULL); +} + +static void +ppp_stats(NMPPPManager *ppp_manager, guint i_in_bytes, guint i_out_bytes, gpointer user_data) +{ + NMModem * self = NM_MODEM(user_data); + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + guint32 in_bytes = i_in_bytes; + guint32 out_bytes = i_out_bytes; + + if (priv->in_bytes != in_bytes || priv->out_bytes != out_bytes) { + priv->in_bytes = in_bytes; + priv->out_bytes = out_bytes; + g_signal_emit(self, signals[PPP_STATS], 0, (guint) in_bytes, (guint) out_bytes); + } +} + +static gboolean +port_speed_is_zero(const char *port) +{ + struct termios options; + nm_auto_close int fd = -1; + gs_free char * path = NULL; + + nm_assert(port); + + if (port[0] != '/') { + if (!port[0] || strchr(port, '/') || NM_IN_STRSET(port, ".", "..")) + return FALSE; + path = g_build_path("/sys/class/tty", port, NULL); + port = path; + } + + fd = open(port, O_RDWR | O_NONBLOCK | O_NOCTTY | O_CLOEXEC); + if (fd < 0) + return FALSE; + + memset(&options, 0, sizeof(struct termios)); + if (tcgetattr(fd, &options) != 0) + return FALSE; + + return cfgetospeed(&options) == B0; +} + +static NMActStageReturn +ppp_stage3_ip_config_start(NMModem * self, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + const char * ppp_name = NULL; + GError * error = NULL; + guint ip_timeout = 30; + guint baud_override = 0; + + g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE); + g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE); + + /* If we're already running PPP don't restart it; for example, if both + * IPv4 and IPv6 are requested, IPv4 gets started first, but we use the + * same pppd for both v4 and v6. + */ + if (priv->ppp_manager) + return NM_ACT_STAGE_RETURN_POSTPONE; + + if (NM_MODEM_GET_CLASS(self)->get_user_pass) { + NMConnection *connection = nm_act_request_get_applied_connection(req); + + g_assert(connection); + if (!NM_MODEM_GET_CLASS(self)->get_user_pass(self, connection, &ppp_name, NULL)) + return NM_ACT_STAGE_RETURN_FAILURE; + } + + if (!priv->data_port) { + _LOGE("error starting PPP (no data port)"); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + /* Check if ModemManager requested a specific IP timeout to be used. If 0 reported, + * use the default one (30s) */ + if (priv->mm_ip_timeout > 0) { + _LOGI("using modem-specified IP timeout: %u seconds", priv->mm_ip_timeout); + ip_timeout = priv->mm_ip_timeout; + } + + /* Some tty drivers and modems ignore port speed, but pppd requires the + * port speed to be > 0 or it exits. If the port speed is 0 pass an + * explicit speed to pppd to prevent the exit. + * https://bugzilla.redhat.com/show_bug.cgi?id=1281731 + */ + if (port_speed_is_zero(priv->data_port)) + baud_override = 57600; + + priv->ppp_manager = nm_ppp_manager_create(priv->data_port, &error); + + if (priv->ppp_manager) { + nm_ppp_manager_set_route_parameters(priv->ppp_manager, + priv->ip4_route_table, + priv->ip4_route_metric, + priv->ip6_route_table, + priv->ip6_route_metric); + } + + if (!priv->ppp_manager + || !nm_ppp_manager_start(priv->ppp_manager, + req, + ppp_name, + ip_timeout, + baud_override, + &error)) { + _LOGE("error starting PPP: %s", error->message); + g_error_free(error); + g_clear_object(&priv->ppp_manager); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_STATE_CHANGED, + G_CALLBACK(ppp_state_changed), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IFINDEX_SET, + G_CALLBACK(ppp_ifindex_set), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IP4_CONFIG, + G_CALLBACK(ppp_ip4_config), + self); + g_signal_connect(priv->ppp_manager, + NM_PPP_MANAGER_SIGNAL_IP6_CONFIG, + G_CALLBACK(ppp_ip6_config), + self); + g_signal_connect(priv->ppp_manager, NM_PPP_MANAGER_SIGNAL_STATS, G_CALLBACK(ppp_stats), self); + + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +NMActStageReturn +nm_modem_stage3_ip4_config_start(NMModem * self, + NMDevice * device, + NMDeviceClass * device_class, + NMDeviceStateReason *out_failure_reason) +{ + NMModemPrivate * priv; + NMActRequest * req; + NMConnection * connection; + const char * method; + NMActStageReturn ret; + + _LOGD("ip4_config_start"); + + g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE); + g_return_val_if_fail(NM_IS_DEVICE(device), NM_ACT_STAGE_RETURN_FAILURE); + g_return_val_if_fail(NM_IS_DEVICE_CLASS(device_class), NM_ACT_STAGE_RETURN_FAILURE); + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + connection = nm_act_request_get_applied_connection(req); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + nm_modem_set_route_parameters_from_device(self, device); + + method = nm_utils_get_ip_config_method(connection, AF_INET); + + /* Only Disabled and Auto methods make sense for WWAN */ + if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) + return NM_ACT_STAGE_RETURN_SUCCESS; + + if (!nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) { + _LOGE("unhandled WWAN IPv4 method '%s'; will fail", method); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + priv = NM_MODEM_GET_PRIVATE(self); + switch (priv->ip4_method) { + case NM_MODEM_IP_METHOD_PPP: + ret = ppp_stage3_ip_config_start(self, req, out_failure_reason); + break; + case NM_MODEM_IP_METHOD_STATIC: + _LOGD("MODEM_IP_METHOD_STATIC"); + ret = + NM_MODEM_GET_CLASS(self)->static_stage3_ip4_config_start(self, req, out_failure_reason); + break; + case NM_MODEM_IP_METHOD_AUTO: + _LOGD("MODEM_IP_METHOD_AUTO"); + ret = device_class->act_stage3_ip_config_start(device, AF_INET, NULL, out_failure_reason); + break; + default: + _LOGI("IPv4 configuration disabled"); + ret = NM_ACT_STAGE_RETURN_IP_FAIL; + break; + } + + return ret; +} + +void +nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(modem); + + /* If the modem has an ethernet-type data interface (ie, not PPP and thus + * not point-to-point) and IP config has a /32 prefix, then we assume that + * ARP will be pointless and we turn it off. + */ + if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC + || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO) { + const NMPlatformIP4Address *address = nm_ip4_config_get_first_address(config); + + g_assert(address); + if (address->plen == 32) + nm_platform_link_set_noarp(nm_device_get_platform(device), + nm_device_get_ip_ifindex(device)); + } +} + +/*****************************************************************************/ + +void +nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error) +{ + NMModemPrivate * priv = NM_MODEM_GET_PRIVATE(self); + NMDedupMultiIter ipconf_iter; + const NMPlatformIP6Address *addr; + gboolean do_slaac = TRUE; + + if (error) { + g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, NULL, FALSE, error); + return; + } + + if (config) { + /* If the IPv6 configuration only included a Link-Local address, then + * we have to run SLAAC to get the full IPv6 configuration. + */ + nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, config, &addr) { + if (IN6_IS_ADDR_LINKLOCAL(&addr->address)) { + if (!priv->iid.id) + priv->iid.id = ((guint64 *) (&addr->address.s6_addr))[1]; + } else + do_slaac = FALSE; + } + } + g_assert(config || do_slaac); + + g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, config, do_slaac, NULL); +} + +static NMActStageReturn +stage3_ip6_config_request(NMModem *self, NMDeviceStateReason *out_failure_reason) +{ + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return NM_ACT_STAGE_RETURN_FAILURE; +} + +NMActStageReturn +nm_modem_stage3_ip6_config_start(NMModem * self, + NMDevice * device, + NMDeviceStateReason *out_failure_reason) +{ + NMModemPrivate * priv; + NMActRequest * req; + NMActStageReturn ret; + NMConnection * connection; + const char * method; + + g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE); + + req = nm_device_get_act_request(device); + g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE); + + connection = nm_act_request_get_applied_connection(req); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + nm_modem_set_route_parameters_from_device(self, device); + + method = nm_utils_get_ip_config_method(connection, AF_INET6); + + /* Only Ignore, Disabled and Auto methods make sense for WWAN */ + if (NM_IN_STRSET(method, + NM_SETTING_IP6_CONFIG_METHOD_IGNORE, + NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) + return NM_ACT_STAGE_RETURN_IP_DONE; + + if (!nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) { + _LOGW("unhandled WWAN IPv6 method '%s'; will fail", method); + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE); + return NM_ACT_STAGE_RETURN_FAILURE; + } + + priv = NM_MODEM_GET_PRIVATE(self); + switch (priv->ip6_method) { + case NM_MODEM_IP_METHOD_PPP: + ret = ppp_stage3_ip_config_start(self, req, out_failure_reason); + break; + case NM_MODEM_IP_METHOD_STATIC: + case NM_MODEM_IP_METHOD_AUTO: + /* Both static and DHCP/Auto retrieve a base IP config from the modem + * which in the static case is the full config, and the DHCP/Auto case + * is just the IPv6LL address to use for SLAAC. + */ + ret = NM_MODEM_GET_CLASS(self)->stage3_ip6_config_request(self, out_failure_reason); + break; + default: + _LOGI("IPv6 configuration disabled"); + ret = NM_ACT_STAGE_RETURN_IP_FAIL; + break; + } + + return ret; +} + +guint32 +nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force) +{ + NMConnection *connection; + NMSetting * setting; + gint64 mtu_default; + guint mtu = 0; + const char * property_name; + + nm_assert(NM_IS_DEVICE(self)); + nm_assert(out_source); + + connection = nm_device_get_applied_connection(self); + if (!connection) + g_return_val_if_reached(0); + + setting = (NMSetting *) nm_connection_get_setting_gsm(connection); + if (!setting) + setting = (NMSetting *) nm_connection_get_setting_cdma(connection); + + if (setting) { + g_object_get(setting, "mtu", &mtu, NULL); + if (mtu) { + *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION; + return mtu; + } + + property_name = NM_IS_SETTING_GSM(setting) ? "gsm.mtu" : "cdma.mtu"; + mtu_default = + nm_device_get_configured_mtu_from_connection_default(self, property_name, G_MAXUINT32); + if (mtu_default >= 0) { + *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION; + return (guint32) mtu_default; + } + } + + *out_source = NM_DEVICE_MTU_SOURCE_NONE; + return 0; +} + +/*****************************************************************************/ + +static void +cancel_get_secrets(NMModem *self) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + if (priv->secrets_id) + nm_act_request_cancel_secrets(priv->act_request, priv->secrets_id); +} + +static void +modem_secrets_cb(NMActRequest * req, + NMActRequestGetSecretsCallId *call_id, + NMSettingsConnection * connection, + GError * error, + gpointer user_data) +{ + NMModem * self = NM_MODEM(user_data); + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + g_return_if_fail(call_id == priv->secrets_id); + + priv->secrets_id = NULL; + + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) + || g_error_matches(error, NM_AGENT_MANAGER_ERROR, NM_AGENT_MANAGER_ERROR_NO_SECRETS)) + return; + + if (error) + _LOGW("modem-secrets: %s", error->message); + + g_signal_emit(self, signals[AUTH_RESULT], 0, error); +} + +void +nm_modem_get_secrets(NMModem * self, + const char *setting_name, + gboolean request_new, + const char *hint) +{ + NMModemPrivate * priv = NM_MODEM_GET_PRIVATE(self); + NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION; + + cancel_get_secrets(self); + + if (request_new) + flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + priv->secrets_id = nm_act_request_get_secrets(priv->act_request, + FALSE, + setting_name, + flags, + NM_MAKE_STRV(hint), + modem_secrets_cb, + self); + g_return_if_fail(priv->secrets_id); + g_signal_emit(self, signals[AUTH_REQUESTED], 0); +} + +/*****************************************************************************/ + +static NMActStageReturn +modem_act_stage1_prepare(NMModem * modem, + NMConnection * connection, + NMDeviceStateReason *out_failure_reason) +{ + NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_UNKNOWN); + return NM_ACT_STAGE_RETURN_FAILURE; +} + +NMActStageReturn +nm_modem_act_stage1_prepare(NMModem * self, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason) +{ + NMModemPrivate * priv = NM_MODEM_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *hints = NULL; + const char * setting_name = NULL; + NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION; + NMConnection * connection; + + g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE); + + if (priv->act_request) + g_object_unref(priv->act_request); + priv->act_request = g_object_ref(req); + + connection = nm_act_request_get_applied_connection(req); + g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE); + + setting_name = nm_connection_need_secrets(connection, &hints); + if (!setting_name) { + nm_assert(!hints); + return NM_MODEM_GET_CLASS(self)->modem_act_stage1_prepare(self, + connection, + out_failure_reason); + } + + /* Secrets required... */ + if (priv->secrets_tries++) + flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + + if (hints) + g_ptr_array_add(hints, NULL); + + priv->secrets_id = nm_act_request_get_secrets(req, + FALSE, + setting_name, + flags, + hints ? (const char *const *) hints->pdata : NULL, + modem_secrets_cb, + self); + g_return_val_if_fail(priv->secrets_id, NM_ACT_STAGE_RETURN_FAILURE); + g_signal_emit(self, signals[AUTH_REQUESTED], 0); + return NM_ACT_STAGE_RETURN_POSTPONE; +} + +/*****************************************************************************/ + +void +nm_modem_act_stage2_config(NMModem *self) +{ + NMModemPrivate *priv; + + g_return_if_fail(NM_IS_MODEM(self)); + + priv = NM_MODEM_GET_PRIVATE(self); + /* Clear secrets tries counter since secrets were successfully used + * already if we get here. + */ + priv->secrets_tries = 0; +} + +/*****************************************************************************/ + +gboolean +nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_GSM_SETTING_NAME)) { + NMSettingGsm *s_gsm; + const char * str; + + s_gsm = _nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error); + if (!s_gsm) + return FALSE; + + str = nm_setting_gsm_get_device_id(s_gsm); + if (str) { + if (!priv->device_id) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "GSM profile has device-id, device does not"); + return FALSE; + } + if (!nm_streq(str, priv->device_id)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has differing device-id than GSM profile"); + return FALSE; + } + } + + /* SIM properties may not be available before the SIM is unlocked, so + * to ensure that autoconnect works, the connection's SIM properties + * are only compared if present on the device. + */ + + if (priv->sim_id && (str = nm_setting_gsm_get_sim_id(s_gsm))) { + if (!nm_streq(str, priv->sim_id)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has differing sim-id than GSM profile"); + return FALSE; + } + } + + if (priv->sim_operator_id && (str = nm_setting_gsm_get_sim_operator_id(s_gsm))) { + if (!nm_streq(str, priv->sim_operator_id)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY, + "device has differing sim-operator-id than GSM profile"); + return FALSE; + } + } + } + + return NM_MODEM_GET_CLASS(self)->check_connection_compatible_with_modem(self, + connection, + error); +} + +/*****************************************************************************/ + +gboolean +nm_modem_complete_connection(NMModem * self, + const char * iface, + NMConnection * connection, + NMConnection *const *existing_connections, + GError ** error) +{ + NMModemClass *klass; + + klass = NM_MODEM_GET_CLASS(self); + if (!klass->complete_connection) { + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_CONNECTION, + "Modem class %s had no complete_connection method", + G_OBJECT_TYPE_NAME(self)); + return FALSE; + } + + return klass->complete_connection(self, iface, connection, existing_connections, error); +} + +/*****************************************************************************/ + +static void +deactivate_cleanup(NMModem *self, NMDevice *device, gboolean stop_ppp_manager) +{ + NMModemPrivate *priv; + int ifindex; + + g_return_if_fail(NM_IS_MODEM(self)); + + priv = NM_MODEM_GET_PRIVATE(self); + + priv->secrets_tries = 0; + + if (priv->act_request) { + cancel_get_secrets(self); + g_object_unref(priv->act_request); + priv->act_request = NULL; + } + + priv->in_bytes = priv->out_bytes = 0; + + if (priv->ppp_manager) { + g_signal_handlers_disconnect_by_data(priv->ppp_manager, self); + if (stop_ppp_manager) + nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL); + g_clear_object(&priv->ppp_manager); + } + + if (device) { + g_return_if_fail(NM_IS_DEVICE(device)); + + if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC + || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO + || priv->ip6_method == NM_MODEM_IP_METHOD_STATIC + || priv->ip6_method == NM_MODEM_IP_METHOD_AUTO) { + ifindex = nm_device_get_ip_ifindex(device); + if (ifindex > 0) { + NMPlatform *platform = nm_device_get_platform(device); + + nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex); + nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex); + nm_platform_link_set_down(platform, ifindex); + } + } + } + + nm_clear_g_free(&priv->data_port); + priv->mm_ip_timeout = 0; + priv->ip4_method = NM_MODEM_IP_METHOD_UNKNOWN; + priv->ip6_method = NM_MODEM_IP_METHOD_UNKNOWN; + _set_ip_ifindex(self, -1, NULL); +} + +/*****************************************************************************/ + +typedef struct { + NMModem * self; + NMDevice * device; + GCancellable * cancellable; + NMModemDeactivateCallback callback; + gpointer callback_user_data; +} DeactivateContext; + +static void +deactivate_context_complete(DeactivateContext *ctx, GError *error) +{ + NMModem *self = ctx->self; + + _LOGD("modem deactivation finished %s%s%s", + NM_PRINT_FMT_QUOTED(error, "with failure: ", error->message, "", "successfully")); + + if (ctx->callback) + ctx->callback(ctx->self, error, ctx->callback_user_data); + nm_g_object_unref(ctx->cancellable); + g_object_unref(ctx->device); + g_object_unref(ctx->self); + g_slice_free(DeactivateContext, ctx); +} + +static void +_deactivate_call_disconnect_cb(NMModem *self, GError *error, gpointer user_data) +{ + deactivate_context_complete(user_data, error); +} + +static void +_deactivate_call_disconnect(DeactivateContext *ctx) +{ + NM_MODEM_GET_CLASS(ctx->self)->disconnect(ctx->self, + FALSE, + ctx->cancellable, + _deactivate_call_disconnect_cb, + ctx); +} + +static void +_deactivate_ppp_manager_stop_cb(NMPPPManager * ppp_manager, + NMPPPManagerStopHandle *handle, + gboolean was_cancelled, + gpointer user_data) +{ + DeactivateContext *ctx = user_data; + + g_object_unref(ppp_manager); + + if (was_cancelled) { + gs_free_error GError *error = NULL; + + if (!g_cancellable_set_error_if_cancelled(ctx->cancellable, &error)) + nm_assert_not_reached(); + deactivate_context_complete(ctx, error); + return; + } + + nm_assert(!g_cancellable_is_cancelled(ctx->cancellable)); + _deactivate_call_disconnect(ctx); +} + +void +nm_modem_deactivate_async(NMModem * self, + NMDevice * device, + GCancellable * cancellable, + NMModemDeactivateCallback callback, + gpointer user_data) +{ + NMModemPrivate * priv = NM_MODEM_GET_PRIVATE(self); + DeactivateContext *ctx; + NMPPPManager * ppp_manager; + + g_return_if_fail(NM_IS_MODEM(self)); + g_return_if_fail(NM_IS_DEVICE(device)); + g_return_if_fail(G_IS_CANCELLABLE(cancellable)); + + ctx = g_slice_new(DeactivateContext); + ctx->self = g_object_ref(self); + ctx->device = g_object_ref(device); + ctx->cancellable = g_object_ref(cancellable); + ctx->callback = callback; + ctx->callback_user_data = user_data; + + ppp_manager = nm_g_object_ref(priv->ppp_manager); + + NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, ctx->device, FALSE); + + if (ppp_manager) { + /* If we have a PPP manager, stop it. + * + * Pass on the reference in @ppp_manager. */ + nm_ppp_manager_stop(ppp_manager, ctx->cancellable, _deactivate_ppp_manager_stop_cb, ctx); + return; + } + + _deactivate_call_disconnect(ctx); +} + +/*****************************************************************************/ + +void +nm_modem_deactivate(NMModem *self, NMDevice *device) +{ + /* First cleanup */ + NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, device, TRUE); + /* Then disconnect without waiting */ + NM_MODEM_GET_CLASS(self)->disconnect(self, FALSE, NULL, NULL, NULL); +} + +/*****************************************************************************/ + +void +nm_modem_device_state_changed(NMModem *self, NMDeviceState new_state, NMDeviceState old_state) +{ + gboolean was_connected = FALSE, warn = TRUE; + NMModemPrivate *priv; + + g_return_if_fail(NM_IS_MODEM(self)); + + if (old_state >= NM_DEVICE_STATE_PREPARE && old_state <= NM_DEVICE_STATE_DEACTIVATING) + was_connected = TRUE; + + priv = NM_MODEM_GET_PRIVATE(self); + + /* Make sure we don't leave the serial device open */ + switch (new_state) { + case NM_DEVICE_STATE_UNMANAGED: + case NM_DEVICE_STATE_UNAVAILABLE: + case NM_DEVICE_STATE_FAILED: + case NM_DEVICE_STATE_DISCONNECTED: + if (priv->act_request) { + cancel_get_secrets(self); + g_object_unref(priv->act_request); + priv->act_request = NULL; + } + + if (was_connected) { + /* Don't bother warning on FAILED since the modem is already gone */ + if (new_state == NM_DEVICE_STATE_FAILED || new_state == NM_DEVICE_STATE_DISCONNECTED) + warn = FALSE; + /* First cleanup */ + NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, NULL, TRUE); + NM_MODEM_GET_CLASS(self)->disconnect(self, warn, NULL, NULL, NULL); + } + break; + default: + break; + } +} + +/*****************************************************************************/ + +const char * +nm_modem_get_uid(NMModem *self) +{ + g_return_val_if_fail(NM_IS_MODEM(self), NULL); + + return NM_MODEM_GET_PRIVATE(self)->uid; +} + +const char * +nm_modem_get_path(NMModem *self) +{ + g_return_val_if_fail(NM_IS_MODEM(self), NULL); + + return NM_MODEM_GET_PRIVATE(self)->path; +} + +const char * +nm_modem_get_driver(NMModem *self) +{ + g_return_val_if_fail(NM_IS_MODEM(self), NULL); + + return NM_MODEM_GET_PRIVATE(self)->driver; +} + +const char * +nm_modem_get_control_port(NMModem *self) +{ + g_return_val_if_fail(NM_IS_MODEM(self), NULL); + + return NM_MODEM_GET_PRIVATE(self)->control_port; +} + +int +nm_modem_get_ip_ifindex(NMModem *self) +{ + NMModemPrivate *priv; + + g_return_val_if_fail(NM_IS_MODEM(self), 0); + + priv = NM_MODEM_GET_PRIVATE(self); + + /* internally we track an unset ip_ifindex as -1. + * For the caller of nm_modem_get_ip_ifindex(), this + * shall be zero too. */ + return priv->ip_ifindex != -1 ? priv->ip_ifindex : 0; +} + +static void +_set_ip_ifindex(NMModem *self, int ifindex, const char *ifname) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + nm_assert(ifindex >= -1); + nm_assert((ifindex > 0) == !!ifname); + + if (!nm_streq0(priv->ip_iface, ifname)) { + g_free(priv->ip_iface); + priv->ip_iface = g_strdup(ifname); + } + + if (priv->ip_ifindex != ifindex) { + priv->ip_ifindex = ifindex; + _notify(self, PROP_IP_IFINDEX); + } +} + +gboolean +nm_modem_set_data_port(NMModem * self, + NMPlatform * platform, + const char * data_port, + NMModemIPMethod ip4_method, + NMModemIPMethod ip6_method, + guint timeout, + GError ** error) +{ + NMModemPrivate *priv; + gboolean is_ppp; + int ifindex = -1; + + g_return_val_if_fail(NM_IS_MODEM(self), FALSE); + g_return_val_if_fail(NM_IS_PLATFORM(platform), FALSE); + g_return_val_if_fail(!error || !*error, FALSE); + + priv = NM_MODEM_GET_PRIVATE(self); + + if (priv->ppp_manager || priv->data_port || priv->ip_ifindex != -1) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "cannot set data port in activated state"); + /* this really shouldn't happen. Assert. */ + g_return_val_if_reached(FALSE); + } + + if (!data_port) { + g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "missing data port"); + return FALSE; + } + + is_ppp = (ip4_method == NM_MODEM_IP_METHOD_PPP) || (ip6_method == NM_MODEM_IP_METHOD_PPP); + if (is_ppp) { + if (!NM_IN_SET(ip4_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP) + || !NM_IN_SET(ip6_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)) { + g_set_error_literal(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "conflicting ip methods"); + return FALSE; + } + } else if (!NM_IN_SET(ip4_method, + NM_MODEM_IP_METHOD_UNKNOWN, + NM_MODEM_IP_METHOD_STATIC, + NM_MODEM_IP_METHOD_AUTO) + || !NM_IN_SET(ip6_method, + NM_MODEM_IP_METHOD_UNKNOWN, + NM_MODEM_IP_METHOD_STATIC, + NM_MODEM_IP_METHOD_AUTO) + || (ip4_method == NM_MODEM_IP_METHOD_UNKNOWN + && ip6_method == NM_MODEM_IP_METHOD_UNKNOWN)) { + g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "invalid ip methods"); + return FALSE; + } + + if (!is_ppp) { + ifindex = nm_platform_if_nametoindex(platform, data_port); + if (ifindex <= 0) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "cannot find network interface %s", + data_port); + return FALSE; + } + if (!nm_platform_process_events_ensure_link(platform, ifindex, data_port)) { + g_set_error(error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "cannot find network interface %s in platform cache", + data_port); + return FALSE; + } + } + + priv->mm_ip_timeout = timeout; + priv->ip4_method = ip4_method; + priv->ip6_method = ip6_method; + if (is_ppp) { + priv->data_port = g_strdup(data_port); + _set_ip_ifindex(self, -1, NULL); + } else { + priv->data_port = NULL; + _set_ip_ifindex(self, ifindex, data_port); + } + return TRUE; +} + +gboolean +nm_modem_owns_port(NMModem *self, const char *iface) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + g_return_val_if_fail(iface != NULL, FALSE); + + if (NM_MODEM_GET_CLASS(self)->owns_port) + return NM_MODEM_GET_CLASS(self)->owns_port(self, iface); + + return NM_IN_STRSET(iface, priv->ip_iface, priv->data_port, priv->control_port); +} + +gboolean +nm_modem_get_iid(NMModem *self, NMUtilsIPv6IfaceId *out_iid) +{ + g_return_val_if_fail(NM_IS_MODEM(self), FALSE); + + *out_iid = NM_MODEM_GET_PRIVATE(self)->iid; + return TRUE; +} + +/*****************************************************************************/ + +void +nm_modem_get_route_parameters(NMModem *self, + guint32 *out_ip4_route_table, + guint32 *out_ip4_route_metric, + guint32 *out_ip6_route_table, + guint32 *out_ip6_route_metric) +{ + NMModemPrivate *priv; + + g_return_if_fail(NM_IS_MODEM(self)); + + priv = NM_MODEM_GET_PRIVATE(self); + NM_SET_OUT(out_ip4_route_table, priv->ip4_route_table); + NM_SET_OUT(out_ip4_route_metric, priv->ip4_route_metric); + NM_SET_OUT(out_ip6_route_table, priv->ip6_route_table); + NM_SET_OUT(out_ip6_route_metric, priv->ip6_route_metric); +} + +void +nm_modem_set_route_parameters(NMModem *self, + guint32 ip4_route_table, + guint32 ip4_route_metric, + guint32 ip6_route_table, + guint32 ip6_route_metric) +{ + NMModemPrivate *priv; + + g_return_if_fail(NM_IS_MODEM(self)); + + priv = NM_MODEM_GET_PRIVATE(self); + if (priv->ip4_route_table != ip4_route_table || priv->ip4_route_metric != ip4_route_metric + || priv->ip6_route_table != ip6_route_table || priv->ip6_route_metric != ip6_route_metric) { + priv->ip4_route_table = ip4_route_table; + priv->ip4_route_metric = ip4_route_metric; + priv->ip6_route_table = ip6_route_table; + priv->ip6_route_metric = ip6_route_metric; + + _LOGT("route-parameters: table-v4: %u, metric-v4: %u, table-v6: %u, metric-v6: %u", + priv->ip4_route_table, + priv->ip4_route_metric, + priv->ip6_route_table, + priv->ip6_route_metric); + } + + if (priv->ppp_manager) { + nm_ppp_manager_set_route_parameters(priv->ppp_manager, + priv->ip4_route_table, + priv->ip4_route_metric, + priv->ip6_route_table, + priv->ip6_route_metric); + } +} + +void +nm_modem_set_route_parameters_from_device(NMModem *self, NMDevice *device) +{ + g_return_if_fail(NM_IS_DEVICE(device)); + + nm_modem_set_route_parameters(self, + nm_device_get_route_table(device, AF_INET), + nm_device_get_route_metric(device, AF_INET), + nm_device_get_route_table(device, AF_INET6), + nm_device_get_route_metric(device, AF_INET6)); +} + +/*****************************************************************************/ + +void +nm_modem_get_capabilities(NMModem * self, + NMDeviceModemCapabilities *modem_caps, + NMDeviceModemCapabilities *current_caps) +{ + g_return_if_fail(NM_IS_MODEM(self)); + + NM_MODEM_GET_CLASS(self)->get_capabilities(self, modem_caps, current_caps); +} + +/*****************************************************************************/ + +void +_nm_modem_set_operator_code(NMModem *self, const char *operator_code) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + if (g_strcmp0(priv->operator_code, operator_code) != 0) { + g_free(priv->operator_code); + priv->operator_code = g_strdup(operator_code); + _notify(self, PROP_OPERATOR_CODE); + } +} + +void +_nm_modem_set_apn(NMModem *self, const char *apn) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + if (g_strcmp0(priv->apn, apn) != 0) { + g_free(priv->apn); + priv->apn = g_strdup(apn); + _notify(self, PROP_APN); + } +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMModem * self = NM_MODEM(object); + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_PATH: + g_value_set_string(value, priv->path); + break; + case PROP_DRIVER: + g_value_set_string(value, priv->driver); + break; + case PROP_CONTROL_PORT: + g_value_set_string(value, priv->control_port); + break; + case PROP_IP_IFINDEX: + g_value_set_int(value, nm_modem_get_ip_ifindex(self)); + break; + case PROP_UID: + g_value_set_string(value, priv->uid); + break; + case PROP_STATE: + g_value_set_int(value, priv->state); + break; + case PROP_DEVICE_ID: + g_value_set_string(value, priv->device_id); + break; + case PROP_SIM_ID: + g_value_set_string(value, priv->sim_id); + break; + case PROP_IP_TYPES: + g_value_set_uint(value, priv->ip_types); + break; + case PROP_SIM_OPERATOR_ID: + g_value_set_string(value, priv->sim_operator_id); + break; + case PROP_OPERATOR_CODE: + g_value_set_string(value, priv->operator_code); + break; + case PROP_APN: + g_value_set_string(value, priv->apn); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object); + const char * s; + + switch (prop_id) { + case PROP_PATH: + /* construct-only */ + priv->path = g_value_dup_string(value); + g_return_if_fail(priv->path); + break; + case PROP_DRIVER: + /* construct-only */ + priv->driver = g_value_dup_string(value); + break; + case PROP_CONTROL_PORT: + /* construct-only */ + priv->control_port = g_value_dup_string(value); + break; + case PROP_UID: + /* construct-only */ + priv->uid = g_value_dup_string(value); + break; + case PROP_STATE: + /* construct-only */ + priv->state = g_value_get_int(value); + break; + case PROP_DEVICE_ID: + /* construct-only */ + priv->device_id = g_value_dup_string(value); + break; + case PROP_SIM_ID: + g_free(priv->sim_id); + priv->sim_id = g_value_dup_string(value); + break; + case PROP_IP_TYPES: + priv->ip_types = g_value_get_uint(value); + break; + case PROP_SIM_OPERATOR_ID: + nm_clear_g_free(&priv->sim_operator_id); + s = g_value_get_string(value); + if (s && s[0]) + priv->sim_operator_id = g_strdup(s); + break; + case PROP_OPERATOR_CODE: + /* construct-only */ + priv->operator_code = g_value_dup_string(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_modem_init(NMModem *self) +{ + NMModemPrivate *priv; + + self->_priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_MODEM, NMModemPrivate); + priv = self->_priv; + + priv->ip_ifindex = -1; + priv->ip4_route_table = RT_TABLE_MAIN; + priv->ip4_route_metric = 700; + priv->ip6_route_table = RT_TABLE_MAIN; + priv->ip6_route_metric = 700; +} + +static void +constructed(GObject *object) +{ + NMModemPrivate *priv; + + G_OBJECT_CLASS(nm_modem_parent_class)->constructed(object); + + priv = NM_MODEM_GET_PRIVATE(NM_MODEM(object)); + + g_return_if_fail(priv->control_port); +} + +/*****************************************************************************/ + +static void +dispose(GObject *object) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object); + + g_clear_object(&priv->act_request); + + G_OBJECT_CLASS(nm_modem_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object); + + g_free(priv->uid); + g_free(priv->path); + g_free(priv->driver); + g_free(priv->control_port); + g_free(priv->data_port); + g_free(priv->ip_iface); + g_free(priv->device_id); + g_free(priv->sim_id); + g_free(priv->sim_operator_id); + g_free(priv->operator_code); + g_free(priv->apn); + + G_OBJECT_CLASS(nm_modem_parent_class)->finalize(object); +} + +static void +nm_modem_class_init(NMModemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + g_type_class_add_private(object_class, sizeof(NMModemPrivate)); + + object_class->constructed = constructed; + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + klass->modem_act_stage1_prepare = modem_act_stage1_prepare; + klass->stage3_ip6_config_request = stage3_ip6_config_request; + klass->deactivate_cleanup = deactivate_cleanup; + + obj_properties[PROP_UID] = + g_param_spec_string(NM_MODEM_UID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_PATH] = + g_param_spec_string(NM_MODEM_PATH, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DRIVER] = + g_param_spec_string(NM_MODEM_DRIVER, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CONTROL_PORT] = + g_param_spec_string(NM_MODEM_CONTROL_PORT, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IP_IFINDEX] = g_param_spec_int(NM_MODEM_IP_IFINDEX, + "", + "", + 0, + G_MAXINT, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_STATE] = + g_param_spec_int(NM_MODEM_STATE, + "", + "", + NM_MODEM_STATE_UNKNOWN, + _NM_MODEM_STATE_LAST, + NM_MODEM_STATE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_DEVICE_ID] = + g_param_spec_string(NM_MODEM_DEVICE_ID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SIM_ID] = + g_param_spec_string(NM_MODEM_SIM_ID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IP_TYPES] = + g_param_spec_uint(NM_MODEM_IP_TYPES, + "IP Types", + "Supported IP types", + 0, + G_MAXUINT32, + NM_MODEM_IP_TYPE_IPV4, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_SIM_OPERATOR_ID] = + g_param_spec_string(NM_MODEM_SIM_OPERATOR_ID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_OPERATOR_CODE] = + g_param_spec_string(NM_MODEM_OPERATOR_CODE, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_APN] = + g_param_spec_string(NM_MODEM_APN, "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[PPP_STATS] = g_signal_new(NM_MODEM_PPP_STATS, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_UINT /*guint32 in_bytes*/, + G_TYPE_UINT /*guint32 out_bytes*/); + + signals[PPP_FAILED] = g_signal_new(NM_MODEM_PPP_FAILED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_UINT); + + signals[IP4_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP4_CONFIG_RESULT, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_OBJECT, + G_TYPE_POINTER); + + /** + * NMModem::ip6-config-result: + * @modem: the #NMModem on which the signal is emitted + * @config: the #NMIP6Config to apply to the modem's data port + * @do_slaac: %TRUE if IPv6 SLAAC should be started + * @error: a #GError if any error occurred during IP configuration + * + * This signal is emitted when IPv6 configuration has completed or failed. + * If @error is set the configuration failed. If @config is set, then + * the details should be applied to the data port before any further + * configuration (like SLAAC) is done. @do_slaac indicates whether SLAAC + * should be started after applying @config to the data port. + */ + signals[IP6_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP6_CONFIG_RESULT, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_OBJECT, + G_TYPE_BOOLEAN, + G_TYPE_POINTER); + + signals[PREPARE_RESULT] = g_signal_new(NM_MODEM_PREPARE_RESULT, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_BOOLEAN, + G_TYPE_UINT); + + signals[AUTH_REQUESTED] = g_signal_new(NM_MODEM_AUTH_REQUESTED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[AUTH_RESULT] = g_signal_new(NM_MODEM_AUTH_RESULT, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_POINTER); + + signals[REMOVED] = g_signal_new(NM_MODEM_REMOVED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[STATE_CHANGED] = g_signal_new(NM_MODEM_STATE_CHANGED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_INT, + G_TYPE_INT); +} diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h new file mode 100644 index 00000000..87162cfc --- /dev/null +++ b/src/core/devices/wwan/nm-modem.h @@ -0,0 +1,269 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2009 - 2011 Red Hat, Inc. + * Copyright (C) 2009 Novell, Inc. + */ + +#ifndef __NETWORKMANAGER_MODEM_H__ +#define __NETWORKMANAGER_MODEM_H__ + +#include "ppp/nm-ppp-manager.h" +#include "devices/nm-device.h" + +#define NM_TYPE_MODEM (nm_modem_get_type()) +#define NM_MODEM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM, NMModem)) +#define NM_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM, NMModemClass)) +#define NM_IS_MODEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM)) +#define NM_IS_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM)) +#define NM_MODEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM, NMModemClass)) + +/* Properties */ +#define NM_MODEM_UID "uid" +#define NM_MODEM_PATH "path" +#define NM_MODEM_DRIVER "driver" +#define NM_MODEM_CONTROL_PORT "control-port" +#define NM_MODEM_IP_IFINDEX "ip-ifindex" +#define NM_MODEM_STATE "state" +#define NM_MODEM_DEVICE_ID "device-id" +#define NM_MODEM_SIM_ID "sim-id" +#define NM_MODEM_IP_TYPES "ip-types" /* Supported IP types */ +#define NM_MODEM_SIM_OPERATOR_ID "sim-operator-id" +#define NM_MODEM_OPERATOR_CODE "operator-code" +#define NM_MODEM_APN "apn" + +/* Signals */ +#define NM_MODEM_PPP_STATS "ppp-stats" +#define NM_MODEM_PPP_FAILED "ppp-failed" +#define NM_MODEM_PREPARE_RESULT "prepare-result" +#define NM_MODEM_IP4_CONFIG_RESULT "ip4-config-result" +#define NM_MODEM_IP6_CONFIG_RESULT "ip6-config-result" +#define NM_MODEM_AUTH_REQUESTED "auth-requested" +#define NM_MODEM_AUTH_RESULT "auth-result" +#define NM_MODEM_REMOVED "removed" +#define NM_MODEM_STATE_CHANGED "state-changed" + +typedef enum { + NM_MODEM_IP_METHOD_UNKNOWN = 0, + NM_MODEM_IP_METHOD_PPP, + NM_MODEM_IP_METHOD_STATIC, + NM_MODEM_IP_METHOD_AUTO, /* DHCP and/or SLAAC */ +} NMModemIPMethod; + +/** + * NMModemIPType: + * @NM_MODEM_IP_TYPE_UNKNOWN: unknown or no IP support + * @NM_MODEM_IP_TYPE_IPV4: IPv4-only bearers are supported + * @NM_MODEM_IP_TYPE_IPV6: IPv6-only bearers are supported + * @NM_MODEM_IP_TYPE_IPV4V6: dual-stack IPv4 + IPv6 bearers are supported + * + * Indicates what IP protocols the modem supports for an IP bearer. Any + * combination of flags is possible. For example, (%NM_MODEM_IP_TYPE_IPV4 | + * %NM_MODEM_IP_TYPE_IPV6) indicates that the modem supports IPv4 and IPv6 + * but not simultaneously on the same bearer. + */ +typedef enum { + NM_MODEM_IP_TYPE_UNKNOWN = 0x0, + NM_MODEM_IP_TYPE_IPV4 = 0x1, + NM_MODEM_IP_TYPE_IPV6 = 0x2, + NM_MODEM_IP_TYPE_IPV4V6 = 0x4 +} NMModemIPType; + +typedef enum { /*< underscore_name=nm_modem_state >*/ + NM_MODEM_STATE_UNKNOWN = 0, + NM_MODEM_STATE_FAILED = 1, + NM_MODEM_STATE_INITIALIZING = 2, + NM_MODEM_STATE_LOCKED = 3, + NM_MODEM_STATE_DISABLED = 4, + NM_MODEM_STATE_DISABLING = 5, + NM_MODEM_STATE_ENABLING = 6, + NM_MODEM_STATE_ENABLED = 7, + NM_MODEM_STATE_SEARCHING = 8, + NM_MODEM_STATE_REGISTERED = 9, + NM_MODEM_STATE_DISCONNECTING = 10, + NM_MODEM_STATE_CONNECTING = 11, + NM_MODEM_STATE_CONNECTED = 12, + + _NM_MODEM_STATE_LAST0, + _NM_MODEM_STATE_LAST = _NM_MODEM_STATE_LAST0 - 1, +} NMModemState; + +struct _NMModemPrivate; + +struct _NMModem { + GObject parent; + struct _NMModemPrivate *_priv; +}; + +typedef struct _NMModem NMModem; + +typedef void (*_NMModemDisconnectCallback)(NMModem *modem, GError *error, gpointer user_data); + +typedef struct { + GObjectClass parent; + + void (*get_capabilities)(NMModem * self, + NMDeviceModemCapabilities *modem_caps, + NMDeviceModemCapabilities *current_caps); + + gboolean (*get_user_pass)(NMModem * modem, + NMConnection *connection, + const char ** user, + const char ** pass); + + gboolean (*check_connection_compatible_with_modem)(NMModem * modem, + NMConnection *connection, + GError ** error); + + gboolean (*complete_connection)(NMModem * modem, + const char * iface, + NMConnection * connection, + NMConnection *const *existing_connections, + GError ** error); + + NMActStageReturn (*modem_act_stage1_prepare)(NMModem * modem, + NMConnection * connection, + NMDeviceStateReason *out_failure_reason); + + NMActStageReturn (*static_stage3_ip4_config_start)(NMModem * self, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason); + + /* Request the IP6 config; when the config returns the modem + * subclass should emit the ip6_config_result signal. + */ + NMActStageReturn (*stage3_ip6_config_request)(NMModem * self, + NMDeviceStateReason *out_failure_reason); + + void (*set_mm_enabled)(NMModem *self, gboolean enabled); + + void (*disconnect)(NMModem * self, + gboolean warn, + GCancellable * cancellable, + _NMModemDisconnectCallback callback, + gpointer user_data); + + void (*deactivate_cleanup)(NMModem *self, NMDevice *device, gboolean stop_ppp_manager); + + gboolean (*owns_port)(NMModem *self, const char *iface); +} NMModemClass; + +GType nm_modem_get_type(void); + +gboolean nm_modem_is_claimed(NMModem *modem); +NMModem *nm_modem_claim(NMModem *modem); +void nm_modem_unclaim(NMModem *modem); + +const char *nm_modem_get_path(NMModem *modem); +const char *nm_modem_get_uid(NMModem *modem); +const char *nm_modem_get_control_port(NMModem *modem); +int nm_modem_get_ip_ifindex(NMModem *modem); +const char *nm_modem_get_driver(NMModem *modem); +const char *nm_modem_get_device_id(NMModem *modem); +const char *nm_modem_get_sim_id(NMModem *modem); +const char *nm_modem_get_sim_operator_id(NMModem *modem); +gboolean nm_modem_get_iid(NMModem *modem, NMUtilsIPv6IfaceId *out_iid); +const char *nm_modem_get_operator_code(NMModem *modem); +const char *nm_modem_get_apn(NMModem *modem); + +gboolean nm_modem_set_data_port(NMModem * self, + NMPlatform * platform, + const char * data_port, + NMModemIPMethod ip4_method, + NMModemIPMethod ip6_method, + guint timeout, + GError ** error); + +gboolean nm_modem_owns_port(NMModem *modem, const char *iface); + +void nm_modem_get_capabilities(NMModem * self, + NMDeviceModemCapabilities *modem_caps, + NMDeviceModemCapabilities *current_caps); + +gboolean +nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error); + +gboolean nm_modem_complete_connection(NMModem * self, + const char * iface, + NMConnection * connection, + NMConnection *const *existing_connections, + GError ** error); + +void nm_modem_get_route_parameters(NMModem *self, + guint32 *out_ip4_route_table, + guint32 *out_ip4_route_metric, + guint32 *out_ip6_route_table, + guint32 *out_ip6_route_metric); + +void nm_modem_set_route_parameters(NMModem *self, + guint32 ip4_route_table, + guint32 ip4_route_metric, + guint32 ip6_route_table, + guint32 ip6_route_metric); + +void nm_modem_set_route_parameters_from_device(NMModem *modem, NMDevice *device); + +NMActStageReturn nm_modem_act_stage1_prepare(NMModem * modem, + NMActRequest * req, + NMDeviceStateReason *out_failure_reason); + +void nm_modem_act_stage2_config(NMModem *modem); + +NMActStageReturn nm_modem_stage3_ip4_config_start(NMModem * modem, + NMDevice * device, + NMDeviceClass * device_class, + NMDeviceStateReason *out_failure_reason); + +NMActStageReturn nm_modem_stage3_ip6_config_start(NMModem * modem, + NMDevice * device, + NMDeviceStateReason *out_failure_reason); + +void nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config); + +void nm_modem_get_secrets(NMModem * modem, + const char *setting_name, + gboolean request_new, + const char *hint); + +void nm_modem_deactivate(NMModem *modem, NMDevice *device); + +typedef void (*NMModemDeactivateCallback)(NMModem *self, GError *error, gpointer user_data); + +void nm_modem_deactivate_async(NMModem * self, + NMDevice * device, + GCancellable * cancellable, + NMModemDeactivateCallback callback, + gpointer user_data); + +void +nm_modem_device_state_changed(NMModem *modem, NMDeviceState new_state, NMDeviceState old_state); + +void nm_modem_set_mm_enabled(NMModem *self, gboolean enabled); + +NMModemState nm_modem_get_state(NMModem *self); +void nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason); +void nm_modem_set_prev_state(NMModem *self, const char *reason); +const char * nm_modem_state_to_string(NMModemState state); + +NMModemIPType nm_modem_get_supported_ip_types(NMModem *self); + +/* For the modem-manager only */ +void nm_modem_emit_removed(NMModem *self); + +void nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason); + +void nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason); + +GArray *nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error); + +/* For subclasses */ +void nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error); + +const char *nm_modem_ip_type_to_string(NMModemIPType ip_type); + +guint32 +nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force); + +void _nm_modem_set_operator_code(NMModem *self, const char *operator_code); +void _nm_modem_set_apn(NMModem *self, const char *apn); + +#endif /* __NETWORKMANAGER_MODEM_H__ */ diff --git a/src/core/devices/wwan/nm-service-providers.c b/src/core/devices/wwan/nm-service-providers.c new file mode 100644 index 00000000..89add6c5 --- /dev/null +++ b/src/core/devices/wwan/nm-service-providers.c @@ -0,0 +1,455 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2009 Novell, Inc. + * Author: Tambet Ingo (tambet@gmail.com). + * Copyright (C) 2009 - 2019 Red Hat, Inc. + * Copyright (C) 2012 Lanedo GmbH + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-service-providers.h" + +typedef enum { + PARSER_TOPLEVEL = 0, + PARSER_COUNTRY, + PARSER_PROVIDER, + PARSER_METHOD_GSM, + PARSER_METHOD_GSM_APN, + PARSER_METHOD_CDMA, + PARSER_DONE, + PARSER_ERROR +} ParseContextState; + +typedef struct { + char * mccmnc; + NMServiceProvidersGsmApnCallback callback; + gpointer user_data; + GCancellable * cancellable; + GMarkupParseContext * ctx; + char buffer[4096]; + + char * text_buffer; + ParseContextState state; + + gboolean mccmnc_matched; + gboolean found_internet_apn; + char * apn; + char * username; + char * password; + char * gateway; + char * auth_method; + GSList * dns; +} ParseContext; + +/*****************************************************************************/ + +static void +parser_toplevel_start(ParseContext *parse_context, + const char * name, + const char ** attribute_names, + const char ** attribute_values) +{ + int i; + + if (strcmp(name, "serviceproviders") == 0) { + for (i = 0; attribute_names && attribute_names[i]; i++) { + if (strcmp(attribute_names[i], "format") == 0) { + if (strcmp(attribute_values[i], "2.0")) { + g_warning("%s: mobile broadband provider database format '%s'" + " not supported.", + __func__, + attribute_values[i]); + parse_context->state = PARSER_ERROR; + break; + } + } + } + } else if (strcmp(name, "country") == 0) { + parse_context->state = PARSER_COUNTRY; + } +} + +static void +parser_country_start(ParseContext *parse_context, + const char * name, + const char ** attribute_names, + const char ** attribute_values) +{ + if (strcmp(name, "provider") == 0) + parse_context->state = PARSER_PROVIDER; +} + +static void +parser_provider_start(ParseContext *parse_context, + const char * name, + const char ** attribute_names, + const char ** attribute_values) +{ + parse_context->mccmnc_matched = FALSE; + if (strcmp(name, "gsm") == 0) + parse_context->state = PARSER_METHOD_GSM; + else if (strcmp(name, "cdma") == 0) + parse_context->state = PARSER_METHOD_CDMA; +} + +static void +parser_gsm_start(ParseContext *parse_context, + const char * name, + const char ** attribute_names, + const char ** attribute_values) +{ + int i; + + if (strcmp(name, "network-id") == 0) { + const char *mcc = NULL, *mnc = NULL; + + for (i = 0; attribute_names && attribute_names[i]; i++) { + if (strcmp(attribute_names[i], "mcc") == 0) + mcc = attribute_values[i]; + else if (strcmp(attribute_names[i], "mnc") == 0) + mnc = attribute_values[i]; + if (mcc && strlen(mcc) && mnc && strlen(mnc)) { + char *mccmnc = g_strdup_printf("%s%s", mcc, mnc); + + if (strcmp(mccmnc, parse_context->mccmnc) == 0) + parse_context->mccmnc_matched = TRUE; + g_free(mccmnc); + break; + } + } + } else if (strcmp(name, "apn") == 0) { + parse_context->found_internet_apn = FALSE; + nm_clear_g_free(&parse_context->apn); + nm_clear_g_free(&parse_context->username); + nm_clear_g_free(&parse_context->password); + nm_clear_g_free(&parse_context->gateway); + nm_clear_g_free(&parse_context->auth_method); + g_slist_free_full(parse_context->dns, g_free); + parse_context->dns = NULL; + + for (i = 0; attribute_names && attribute_names[i]; i++) { + if (strcmp(attribute_names[i], "value") == 0) { + parse_context->state = PARSER_METHOD_GSM_APN; + parse_context->apn = g_strstrip(g_strdup(attribute_values[i])); + break; + } + } + } +} + +static void +parser_gsm_apn_start(ParseContext *parse_context, + const char * name, + const char ** attribute_names, + const char ** attribute_values) +{ + int i; + + if (strcmp(name, "usage") == 0) { + for (i = 0; attribute_names && attribute_names[i]; i++) { + if ((strcmp(attribute_names[i], "type") == 0) + && (strcmp(attribute_values[i], "internet") == 0)) { + parse_context->found_internet_apn = TRUE; + break; + } + } + } else if (strcmp(name, "authentication") == 0) { + for (i = 0; attribute_names && attribute_names[i]; i++) { + if (strcmp(attribute_names[i], "method") == 0) { + nm_clear_g_free(&parse_context->auth_method); + parse_context->auth_method = g_strstrip(g_strdup(attribute_values[i])); + break; + } + } + } +} + +static void +parser_start_element(GMarkupParseContext *context, + const char * element_name, + const char ** attribute_names, + const char ** attribute_values, + gpointer user_data, + GError ** error) +{ + ParseContext *parse_context = user_data; + + nm_clear_g_free(&parse_context->text_buffer); + + switch (parse_context->state) { + case PARSER_TOPLEVEL: + parser_toplevel_start(parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_COUNTRY: + parser_country_start(parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_PROVIDER: + parser_provider_start(parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_METHOD_GSM: + parser_gsm_start(parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_METHOD_GSM_APN: + parser_gsm_apn_start(parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_METHOD_CDMA: + break; + case PARSER_ERROR: + break; + case PARSER_DONE: + break; + } +} + +static void +parser_country_end(ParseContext *parse_context, const char *name) +{ + if (strcmp(name, "country") == 0) { + nm_clear_g_free(&parse_context->text_buffer); + parse_context->state = PARSER_TOPLEVEL; + } +} + +static void +parser_provider_end(ParseContext *parse_context, const char *name) +{ + if (strcmp(name, "provider") == 0) { + nm_clear_g_free(&parse_context->text_buffer); + parse_context->state = PARSER_COUNTRY; + } +} + +static void +parser_gsm_end(ParseContext *parse_context, const char *name) +{ + if (strcmp(name, "gsm") == 0) { + nm_clear_g_free(&parse_context->text_buffer); + parse_context->state = PARSER_PROVIDER; + } +} + +static void +parser_gsm_apn_end(ParseContext *parse_context, const char *name) +{ + if (strcmp(name, "username") == 0) { + nm_clear_g_free(&parse_context->username); + parse_context->username = g_steal_pointer(&parse_context->text_buffer); + } else if (strcmp(name, "password") == 0) { + nm_clear_g_free(&parse_context->password); + parse_context->password = g_steal_pointer(&parse_context->text_buffer); + } else if (strcmp(name, "dns") == 0) { + parse_context->dns = + g_slist_prepend(parse_context->dns, g_steal_pointer(&parse_context->text_buffer)); + } else if (strcmp(name, "gateway") == 0) { + nm_clear_g_free(&parse_context->gateway); + parse_context->gateway = g_steal_pointer(&parse_context->text_buffer); + } else if (strcmp(name, "apn") == 0) { + nm_clear_g_free(&parse_context->text_buffer); + + if (parse_context->mccmnc_matched && parse_context->found_internet_apn) + parse_context->state = PARSER_DONE; + else + parse_context->state = PARSER_METHOD_GSM; + } +} + +static void +parser_cdma_end(ParseContext *parse_context, const char *name) +{ + if (strcmp(name, "cdma") == 0) { + nm_clear_g_free(&parse_context->text_buffer); + parse_context->state = PARSER_PROVIDER; + } +} + +static void +parser_end_element(GMarkupParseContext *context, + const char * element_name, + gpointer user_data, + GError ** error) +{ + ParseContext *parse_context = user_data; + + switch (parse_context->state) { + case PARSER_TOPLEVEL: + break; + case PARSER_COUNTRY: + parser_country_end(parse_context, element_name); + break; + case PARSER_PROVIDER: + parser_provider_end(parse_context, element_name); + break; + case PARSER_METHOD_GSM: + parser_gsm_end(parse_context, element_name); + break; + case PARSER_METHOD_GSM_APN: + parser_gsm_apn_end(parse_context, element_name); + break; + case PARSER_METHOD_CDMA: + parser_cdma_end(parse_context, element_name); + break; + case PARSER_ERROR: + break; + case PARSER_DONE: + break; + } +} + +static void +parser_text(GMarkupParseContext *context, + const char * text, + gsize text_len, + gpointer user_data, + GError ** error) +{ + ParseContext *parse_context = user_data; + + g_free(parse_context->text_buffer); + parse_context->text_buffer = g_strdup(text); +} + +static const GMarkupParser parser = { + .start_element = parser_start_element, + .end_element = parser_end_element, + .text = parser_text, + .passthrough = NULL, + .error = NULL, +}; + +/*****************************************************************************/ + +static void +finish_parse_context(ParseContext *parse_context, GError *error) +{ + if (parse_context->callback) { + if (error) { + parse_context + ->callback(NULL, NULL, NULL, NULL, NULL, NULL, error, parse_context->user_data); + } else { + parse_context->callback(parse_context->apn, + parse_context->username, + parse_context->password, + parse_context->gateway, + parse_context->auth_method, + parse_context->dns, + error, + parse_context->user_data); + } + } + + g_free(parse_context->mccmnc); + g_markup_parse_context_free(parse_context->ctx); + + g_free(parse_context->text_buffer); + g_free(parse_context->apn); + g_free(parse_context->username); + g_free(parse_context->password); + g_free(parse_context->gateway); + g_free(parse_context->auth_method); + g_slist_free_full(parse_context->dns, g_free); + + g_slice_free(ParseContext, parse_context); +} + +static void read_next_chunk(GInputStream *stream, ParseContext *parse_context); + +static void +stream_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream *stream = G_INPUT_STREAM(source_object); + ParseContext *parse_context = user_data; + gssize len; + GError * error = NULL; + + len = g_input_stream_read_finish(stream, res, &error); + if (len == -1) { + g_prefix_error(&error, "Error reading service provider database: "); + finish_parse_context(parse_context, error); + g_clear_error(&error); + return; + } + + if (len == 0) { + g_set_error(&error, + NM_UTILS_ERROR, + NM_UTILS_ERROR_UNKNOWN, + "Operator ID '%s' not found in service provider database", + parse_context->mccmnc); + finish_parse_context(parse_context, error); + g_clear_error(&error); + return; + } + + if (!g_markup_parse_context_parse(parse_context->ctx, parse_context->buffer, len, &error)) { + g_prefix_error(&error, "Error parsing service provider database: "); + finish_parse_context(parse_context, error); + g_clear_error(&error); + return; + } + + if (parse_context->state == PARSER_DONE) { + finish_parse_context(parse_context, NULL); + return; + } + + read_next_chunk(stream, parse_context); +} + +static void +read_next_chunk(GInputStream *stream, ParseContext *parse_context) +{ + g_input_stream_read_async(stream, + parse_context->buffer, + sizeof(parse_context->buffer), + G_PRIORITY_DEFAULT, + parse_context->cancellable, + stream_read_cb, + parse_context); +} + +static void +file_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GFile * file = G_FILE(source_object); + ParseContext * parse_context = user_data; + GFileInputStream *stream; + gs_free_error GError *error = NULL; + + stream = g_file_read_finish(file, res, &error); + if (!stream) { + g_prefix_error(&error, "Error opening service provider database: "); + finish_parse_context(parse_context, error); + return; + } + + read_next_chunk(G_INPUT_STREAM(stream), parse_context); + + g_object_unref(stream); +} + +/*****************************************************************************/ + +void +nm_service_providers_find_gsm_apn(const char * service_providers, + const char * mccmnc, + GCancellable * cancellable, + NMServiceProvidersGsmApnCallback callback, + gpointer user_data) +{ + GFile * file; + ParseContext *parse_context; + + parse_context = g_slice_new0(ParseContext); + parse_context->mccmnc = g_strdup(mccmnc); + parse_context->cancellable = cancellable; + parse_context->callback = callback; + parse_context->user_data = user_data; + parse_context->ctx = g_markup_parse_context_new(&parser, 0, parse_context, NULL); + + file = g_file_new_for_path(service_providers); + + g_file_read_async(file, G_PRIORITY_DEFAULT, cancellable, file_read_cb, parse_context); + + g_object_unref(file); +} diff --git a/src/core/devices/wwan/nm-service-providers.h b/src/core/devices/wwan/nm-service-providers.h new file mode 100644 index 00000000..959f660a --- /dev/null +++ b/src/core/devices/wwan/nm-service-providers.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2019 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_SERVICE_PROVIDERS_H__ +#define __NETWORKMANAGER_SERVICE_PROVIDERS_H__ + +typedef void (*NMServiceProvidersGsmApnCallback)(const char * apn, + const char * username, + const char * password, + const char * gateway, + const char * auth_method, + const GSList *dns, + GError * error, + gpointer user_data); + +void nm_service_providers_find_gsm_apn(const char * service_providers, + const char * mccmnc, + GCancellable * cancellable, + NMServiceProvidersGsmApnCallback callback, + gpointer user_data); + +#endif /* __NETWORKMANAGER_SERVICE_PROVIDERS_H__ */ diff --git a/src/core/devices/wwan/nm-wwan-factory.c b/src/core/devices/wwan/nm-wwan-factory.c new file mode 100644 index 00000000..5d2ce2b3 --- /dev/null +++ b/src/core/devices/wwan/nm-wwan-factory.c @@ -0,0 +1,146 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <gmodule.h> + +#include "devices/nm-device-factory.h" +#include "nm-setting-gsm.h" +#include "nm-setting-cdma.h" +#include "nm-modem-manager.h" +#include "nm-device-modem.h" +#include "platform/nm-platform.h" + +/*****************************************************************************/ + +#define NM_TYPE_WWAN_FACTORY (nm_wwan_factory_get_type()) +#define NM_WWAN_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactory)) +#define NM_WWAN_FACTORY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass)) +#define NM_IS_WWAN_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WWAN_FACTORY)) +#define NM_IS_WWAN_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WWAN_FACTORY)) +#define NM_WWAN_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass)) + +typedef struct { + NMModemManager *mm; +} NMWwanFactoryPrivate; + +typedef struct { + NMDeviceFactory parent; + NMWwanFactoryPrivate _priv; +} NMWwanFactory; + +typedef struct { + NMDeviceFactoryClass parent; +} NMWwanFactoryClass; + +static GType nm_wwan_factory_get_type(void); + +G_DEFINE_TYPE(NMWwanFactory, nm_wwan_factory, NM_TYPE_DEVICE_FACTORY) + +#define NM_WWAN_FACTORY_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMWwanFactory, NM_IS_WWAN_FACTORY) + +/*****************************************************************************/ + +NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES( + NM_LINK_TYPE_WWAN_NET) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GSM_SETTING_NAME, + NM_SETTING_CDMA_SETTING_NAME)) + +G_MODULE_EXPORT NMDeviceFactory * + nm_device_factory_create(GError **error) +{ + return g_object_new(NM_TYPE_WWAN_FACTORY, NULL); +} + +/*****************************************************************************/ + +static void +modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data) +{ + NMWwanFactory * self = NM_WWAN_FACTORY(user_data); + gs_unref_object NMDevice *device = NULL; + const char * driver; + + if (nm_modem_is_claimed(modem)) + return; + + driver = nm_modem_get_driver(modem); + + /* If it was a Bluetooth modem and no bluetooth device claimed it, ignore + * it. The rfcomm port (and thus the modem) gets created automatically + * by the Bluetooth code during the connection process. + */ + if (driver && strstr(driver, "bluetooth")) { + nm_log_dbg( + LOGD_MB, + "WWAN factory ignores bluetooth modem '%s' which should be handled by bluetooth plugin", + nm_modem_get_control_port(modem)); + return; + } + + /* Make the new modem device */ + device = nm_device_modem_new(modem); + g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device); +} + +static NMDevice * +create_device(NMDeviceFactory * factory, + const char * iface, + const NMPlatformLink *plink, + NMConnection * connection, + gboolean * out_ignore) +{ + g_return_val_if_fail(plink, NULL); + g_return_val_if_fail(plink->type == NM_LINK_TYPE_WWAN_NET, NULL); + *out_ignore = TRUE; + return NULL; +} + +static void +start(NMDeviceFactory *factory) +{ + NMWwanFactory * self = NM_WWAN_FACTORY(factory); + NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self); + + priv->mm = g_object_ref(nm_modem_manager_get()); + + g_signal_connect(priv->mm, NM_MODEM_MANAGER_MODEM_ADDED, G_CALLBACK(modem_added_cb), self); +} + +/*****************************************************************************/ + +static void +nm_wwan_factory_init(NMWwanFactory *self) +{} + +static void +dispose(GObject *object) +{ + NMWwanFactory * self = NM_WWAN_FACTORY(object); + NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self); + + if (priv->mm) + g_signal_handlers_disconnect_by_func(priv->mm, modem_added_cb, self); + g_clear_object(&priv->mm); + + /* Chain up to the parent class */ + G_OBJECT_CLASS(nm_wwan_factory_parent_class)->dispose(object); +} + +static void +nm_wwan_factory_class_init(NMWwanFactoryClass *klass) +{ + GObjectClass * object_class = G_OBJECT_CLASS(klass); + NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass); + + object_class->dispose = dispose; + + factory_class->get_supported_types = get_supported_types; + factory_class->create_device = create_device; + factory_class->start = start; +} diff --git a/src/core/devices/wwan/tests/test-service-providers.c b/src/core/devices/wwan/tests/test-service-providers.c new file mode 100644 index 00000000..f95cccf8 --- /dev/null +++ b/src/core/devices/wwan/tests/test-service-providers.c @@ -0,0 +1,130 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2019 Red Hat + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-service-providers.h" + +#include "nm-test-utils-core.h" + +static void +test_positive_cb(const char * apn, + const char * username, + const char * password, + const char * gateway, + const char * auth_method, + const GSList *dns, + GError * error, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + g_main_loop_quit(loop); + g_assert_no_error(error); + g_assert_cmpstr(apn, ==, "gprs.example.com"); + g_assert_cmpstr(username, ==, "praise"); + g_assert_cmpstr(password, ==, "santa"); + g_assert_cmpstr(gateway, ==, "192.0.2.3"); + g_assert_cmpstr(auth_method, ==, "pap"); + + g_assert_nonnull(dns); + g_assert_cmpstr(dns->data, ==, "192.0.2.2"); + dns = dns->next; + g_assert_nonnull(dns); + g_assert_cmpstr(dns->data, ==, "192.0.2.1"); + g_assert_null(dns->next); +} + +static void +test_positive(void) +{ + GMainLoop *loop = g_main_loop_new(NULL, FALSE); + + nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR + "/src/core/devices/wwan/tests/test-service-providers.xml", + "13337", + NULL, + test_positive_cb, + loop); + g_main_loop_run(loop); + g_main_loop_unref(loop); +} + +/*****************************************************************************/ + +static void +test_negative_cb(const char * apn, + const char * username, + const char * password, + const char * gateway, + const char * auth_method, + const GSList *dns, + GError * error, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + g_main_loop_quit(loop); + g_assert_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN); +} + +static void +test_negative(void) +{ + GMainLoop *loop = g_main_loop_new(NULL, FALSE); + + nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR + "/src/core/devices/wwan/tests/test-service-providers.xml", + "78130", + NULL, + test_negative_cb, + loop); + g_main_loop_run(loop); + g_main_loop_unref(loop); +} + +/*****************************************************************************/ + +static void +test_nonexistent_cb(const char * apn, + const char * username, + const char * password, + const char * gateway, + const char * auth_method, + const GSList *dns, + GError * error, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + g_main_loop_quit(loop); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_AGAIN); +} + +static void +test_nonexistent(void) +{ + GMainLoop *loop = g_main_loop_new(NULL, FALSE); + + nm_service_providers_find_gsm_apn("nonexistent.xml", "13337", NULL, test_nonexistent_cb, loop); + g_main_loop_run(loop); + g_main_loop_unref(loop); +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT"); + + g_test_add_func("/service-providers/positive", test_positive); + g_test_add_func("/service-providers/negative", test_negative); + g_test_add_func("/service-providers/nonexistent", test_nonexistent); + + return g_test_run(); +} diff --git a/src/core/devices/wwan/tests/test-service-providers.xml b/src/core/devices/wwan/tests/test-service-providers.xml new file mode 100644 index 00000000..f0ca2deb --- /dev/null +++ b/src/core/devices/wwan/tests/test-service-providers.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding='utf-8'?> +<!DOCTYPE serviceproviders SYSTEM "serviceproviders.2.dtd"> + +<serviceproviders format="2.0"> + +<country code="feh"> + <provider> + <name>Sophia</name> + <gsm> + <network-id mcc="666" mnc="999"/> + <apn value="access.example.com"> + <plan type="postpaid"/> + <usage type="internet"/> + <name>APN</name> + <dns>192.0.2.1</dns> + <dns>192.0.2.2</dns> + </apn> + </gsm> + </provider> +</country> + +<country code="meh"> + <provider> + <name>Demiurge</name> + <gsm> + <network-id mcc="133" mnc="37"/> + <network-id mcc="133" mnc="666"/> + <apn value="mms"> + <usage type="mms"/> + <name>Unsolicited Nudes MMS</name> + <username>mms</username> + <password>mms</password> + <mmsc>http://mms.example.com/</mmsc> + <mmsproxy>192.0.2.1:8080</mmsproxy> + </apn> + <apn value="gprs.example.com"> + <plan type="postpaid"/> + <usage type="internet"/> + <name>GPRS</name> + <username>praise</username> + <password>santa</password> + <dns>192.0.2.1</dns> + <dns>192.0.2.2</dns> + <gateway>192.0.2.3</gateway> + <authentication method="pap"/> + </apn> + <apn value="second.example.com"> + <plan type="postpaid"/> + <usage type="internet"/> + <name>Second</name> + <username>worship</username> + <password>doom</password> + </apn> + </gsm> + </provider> + + <provider> + <name>Personal</name> + <gsm> + <network-id mcc="666" mnc="999"/> + <apn value="access.example.com"> + <plan type="postpaid"/> + <usage type="internet"/> + <name>APN</name> + <dns>192.0.2.1</dns> + <dns>192.0.2.2</dns> + </apn> + </gsm> + </provider> +</country> + +</serviceproviders> + |