// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2009 - 2014 Red Hat, Inc.
 * Copyright (C) 2009 Novell, Inc.
 */

#include "nm-default.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);
}
