/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* NetworkManager -- Network link manager
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright 2004 - 2016 Red Hat, Inc.
 * Copyright 2005 - 2008 Novell, Inc.
 */

#include "nm-default.h"

#include "NetworkManagerUtils.h"

#include "nm-common-macros.h"
#include "nm-utils.h"
#include "nm-setting-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "nm-core-internal.h"

#include "nm-platform.h"
#include "nm-exported-object.h"
#include "nm-auth-utils.h"

/******************************************************************************/

/**
 * nm_utils_get_shared_wifi_permission:
 * @connection: the NMConnection to lookup the permission.
 *
 * Returns: a static string of the wifi-permission (if any) or %NULL.
 */
const char *
nm_utils_get_shared_wifi_permission (NMConnection *connection)
{
	NMSettingWireless *s_wifi;
	NMSettingWirelessSecurity *s_wsec;
	const char *method = NULL;

	method = nm_utils_get_ip_config_method (connection, NM_TYPE_SETTING_IP4_CONFIG);
	if (strcmp (method, NM_SETTING_IP4_CONFIG_METHOD_SHARED) != 0)
		return NULL;  /* Not shared */

	s_wifi = nm_connection_get_setting_wireless (connection);
	if (s_wifi) {
		s_wsec = nm_connection_get_setting_wireless_security (connection);
		if (s_wsec)
			return NM_AUTH_PERMISSION_WIFI_SHARE_PROTECTED;
		else
			return NM_AUTH_PERMISSION_WIFI_SHARE_OPEN;
	}

	return NULL;
}

/******************************************************************************/

static char *
get_new_connection_name (const GSList *existing,
                         const char *preferred,
                         const char *fallback_prefix)
{
	GSList *names = NULL;
	const GSList *iter;
	char *cname = NULL;
	int i = 0;
	gboolean preferred_found = FALSE;

	g_assert (fallback_prefix);

	for (iter = existing; iter; iter = g_slist_next (iter)) {
		NMConnection *candidate = NM_CONNECTION (iter->data);
		const char *id;

		id = nm_connection_get_id (candidate);
		g_assert (id);
		names = g_slist_append (names, (gpointer) id);

		if (preferred && !preferred_found && (strcmp (preferred, id) == 0))
			preferred_found = TRUE;
	}

	/* Return the preferred name if it was unique */
	if (preferred && !preferred_found) {
		g_slist_free (names);
		return g_strdup (preferred);
	}

	/* Otherwise find the next available unique connection name using the given
	 * connection name template.
	 */
	while (!cname && (i++ < 10000)) {
		char *temp;
		gboolean found = FALSE;

		/* Translators: the first %s is a prefix for the connection id, such
		 * as "Wired Connection" or "VPN Connection". The %d is a number
		 * that is combined with the first argument to create a unique
		 * connection id. */
		temp = g_strdup_printf (C_("connection id fallback", "%s %d"),
		                        fallback_prefix, i);
		for (iter = names; iter; iter = g_slist_next (iter)) {
			if (!strcmp (iter->data, temp)) {
				found = TRUE;
				break;
			}
		}
		if (!found)
			cname = temp;
		else
			g_free (temp);
	}

	g_slist_free (names);
	return cname;
}

static char *
get_new_connection_ifname (NMPlatform *platform,
                           const GSList *existing,
                           const char *prefix)
{
	int i;
	char *name;
	const GSList *iter;
	gboolean found;

	for (i = 0; i < 500; i++) {
		name = g_strdup_printf ("%s%d", prefix, i);

		if (nm_platform_link_get_by_ifname (platform, name))
			goto next;

		for (iter = existing, found = FALSE; iter; iter = g_slist_next (iter)) {
			NMConnection *candidate = iter->data;

			if (g_strcmp0 (nm_connection_get_interface_name (candidate), name) == 0) {
				found = TRUE;
				break;
			}
		}

		if (!found)
			return name;

	next:
		g_free (name);
	}

	return NULL;
}

const char *
nm_utils_get_ip_config_method (NMConnection *connection,
                               GType         ip_setting_type)
{
	NMSettingConnection *s_con;
	NMSettingIPConfig *s_ip4, *s_ip6;
	const char *method;

	s_con = nm_connection_get_setting_connection (connection);

	if (ip_setting_type == NM_TYPE_SETTING_IP4_CONFIG) {
		g_return_val_if_fail (s_con != NULL, NM_SETTING_IP4_CONFIG_METHOD_AUTO);

		if (nm_setting_connection_get_master (s_con))
			return NM_SETTING_IP4_CONFIG_METHOD_DISABLED;
		else {
			s_ip4 = nm_connection_get_setting_ip4_config (connection);
			if (!s_ip4)
				return NM_SETTING_IP4_CONFIG_METHOD_DISABLED;
			method = nm_setting_ip_config_get_method (s_ip4);
			g_return_val_if_fail (method != NULL, NM_SETTING_IP4_CONFIG_METHOD_AUTO);

			return method;
		}

	} else if (ip_setting_type == NM_TYPE_SETTING_IP6_CONFIG) {
		g_return_val_if_fail (s_con != NULL, NM_SETTING_IP6_CONFIG_METHOD_AUTO);

		if (nm_setting_connection_get_master (s_con))
			return NM_SETTING_IP6_CONFIG_METHOD_IGNORE;
		else {
			s_ip6 = nm_connection_get_setting_ip6_config (connection);
			if (!s_ip6)
				return NM_SETTING_IP6_CONFIG_METHOD_IGNORE;
			method = nm_setting_ip_config_get_method (s_ip6);
			g_return_val_if_fail (method != NULL, NM_SETTING_IP6_CONFIG_METHOD_AUTO);

			return method;
		}

	} else
		g_assert_not_reached ();
}

void
nm_utils_complete_generic (NMPlatform *platform,
                           NMConnection *connection,
                           const char *ctype,
                           const GSList *existing,
                           const char *preferred_id,
                           const char *fallback_id_prefix,
                           const char *ifname_prefix,
                           gboolean default_enable_ipv6)
{
	NMSettingConnection *s_con;
	char *id, *uuid, *ifname;
	GHashTable *parameters;

	g_assert (fallback_id_prefix);

	s_con = nm_connection_get_setting_connection (connection);
	if (!s_con) {
		s_con = (NMSettingConnection *) nm_setting_connection_new ();
		nm_connection_add_setting (connection, NM_SETTING (s_con));
	}
	g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_TYPE, ctype, NULL);

	if (!nm_setting_connection_get_uuid (s_con)) {
		uuid = nm_utils_uuid_generate ();
		g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_UUID, uuid, NULL);
		g_free (uuid);
	}

	/* Add a connection ID if absent */
	if (!nm_setting_connection_get_id (s_con)) {
		id = get_new_connection_name (existing, preferred_id, fallback_id_prefix);
		g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_ID, id, NULL);
		g_free (id);
	}

	/* Add an interface name, if requested */
	if (ifname_prefix && !nm_setting_connection_get_interface_name (s_con)) {
		ifname = get_new_connection_ifname (platform, existing, ifname_prefix);
		g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_INTERFACE_NAME, ifname, NULL);
		g_free (ifname);
	}

	/* Normalize */
	parameters = g_hash_table_new (g_str_hash, g_str_equal);
	g_hash_table_insert (parameters, NM_CONNECTION_NORMALIZE_PARAM_IP6_CONFIG_METHOD,
	                     default_enable_ipv6 ? NM_SETTING_IP6_CONFIG_METHOD_AUTO : NM_SETTING_IP6_CONFIG_METHOD_IGNORE);
	nm_connection_normalize (connection, parameters, NULL, NULL);
	g_hash_table_destroy (parameters);
}

/*****************************************************************************/

static GHashTable *
check_property_in_hash (GHashTable *hash,
                        const char *s_name,
                        const char *p_name)
{
	GHashTable *props;

	props = g_hash_table_lookup (hash, s_name);
	if (   !props
	    || !g_hash_table_lookup (props, p_name)) {
		return NULL;
	}
	return props;
}

static void
remove_from_hash (GHashTable *s_hash,
                  GHashTable *p_hash,
                  const char *s_name,
                  const char *p_name)
{
	if (!p_hash)
		return;

	g_hash_table_remove (p_hash, p_name);
	if (g_hash_table_size (p_hash) == 0)
		g_hash_table_remove (s_hash, s_name);
}

static gboolean
check_ip6_method (NMConnection *orig,
                  NMConnection *candidate,
                  GHashTable *settings)
{
	GHashTable *props;
	const char *orig_ip6_method, *candidate_ip6_method;
	NMSettingIPConfig *candidate_ip6;
	gboolean allow = FALSE;

	props = check_property_in_hash (settings,
	                                NM_SETTING_IP6_CONFIG_SETTING_NAME,
	                                NM_SETTING_IP_CONFIG_METHOD);
	if (!props)
		return TRUE;

	/* If the generated connection is 'link-local' and the candidate is both 'auto'
	 * and may-fail=TRUE, then the candidate is OK to use.  may-fail is included
	 * in the decision because if the candidate is 'auto' but may-fail=FALSE, then
	 * the connection could not possibly have been previously activated on the
	 * device if the device has no non-link-local IPv6 address.
	 */
	orig_ip6_method = nm_utils_get_ip_config_method (orig, NM_TYPE_SETTING_IP6_CONFIG);
	candidate_ip6_method = nm_utils_get_ip_config_method (candidate, NM_TYPE_SETTING_IP6_CONFIG);
	candidate_ip6 = nm_connection_get_setting_ip6_config (candidate);

	if (   strcmp (orig_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL) == 0
	    && strcmp (candidate_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO) == 0
	    && (!candidate_ip6 || nm_setting_ip_config_get_may_fail (candidate_ip6))) {
		allow = TRUE;
	}

	/* If the generated connection method is 'link-local' or 'auto' and the candidate
	 * method is 'ignore' we can take the connection, because NM didn't simply take care
	 * of IPv6.
	 */
	if (  (   strcmp (orig_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL) == 0
	       || strcmp (orig_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO) == 0)
	    && strcmp (candidate_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE) == 0) {
		allow = TRUE;
	}

	if (allow) {
		remove_from_hash (settings, props,
		                  NM_SETTING_IP6_CONFIG_SETTING_NAME,
		                  NM_SETTING_IP_CONFIG_METHOD);
	}
	return allow;
}

static int
route_compare (NMIPRoute *route1, NMIPRoute *route2, gint64 default_metric)
{
	gint64 r, metric1, metric2;

	r = g_strcmp0 (nm_ip_route_get_dest (route1), nm_ip_route_get_dest (route2));
	if (r)
		return r;

	r = nm_ip_route_get_prefix (route1) - nm_ip_route_get_prefix (route2);
	if (r)
		return r > 0 ? 1 : -1;

	r = g_strcmp0 (nm_ip_route_get_next_hop (route1), nm_ip_route_get_next_hop (route2));
	if (r)
		return r;

	metric1 = nm_ip_route_get_metric (route1) == -1 ? default_metric : nm_ip_route_get_metric (route1);
	metric2 = nm_ip_route_get_metric (route2) == -1 ? default_metric : nm_ip_route_get_metric (route2);

	r = metric1 - metric2;
	if (r)
		return r > 0 ? 1 : -1;

	r = nm_ip_route_get_family (route1) - nm_ip_route_get_family (route2);
	if (r)
		return r > 0 ? 1 : -1;

	return 0;
}

static int
route_ptr_compare (const void *a, const void *b)
{
	return route_compare (*(NMIPRoute **) a, *(NMIPRoute **) b, -1);
}

static gboolean
check_ip_routes (NMConnection *orig,
                 NMConnection *candidate,
                 GHashTable *settings,
                 gint64 default_metric,
                 gboolean v4)
{
	gs_free NMIPRoute **routes1 = NULL, **routes2 = NULL;
	NMSettingIPConfig *s_ip1, *s_ip2;
	const char *s_name;
	GHashTable *props;
	guint i, num;

	s_name = v4 ? NM_SETTING_IP4_CONFIG_SETTING_NAME :
	              NM_SETTING_IP6_CONFIG_SETTING_NAME;

	props = check_property_in_hash (settings,
	                                s_name,
	                                NM_SETTING_IP_CONFIG_ROUTES);
	if (!props)
		return TRUE;

	s_ip1 = (NMSettingIPConfig *) nm_connection_get_setting_by_name (orig, s_name);
	s_ip2 = (NMSettingIPConfig *) nm_connection_get_setting_by_name (candidate, s_name);

	if (!s_ip1 || !s_ip2)
		return FALSE;

	num = nm_setting_ip_config_get_num_routes (s_ip1);
	if (num != nm_setting_ip_config_get_num_routes (s_ip2))
		return FALSE;

	routes1 = g_new (NMIPRoute *, num);
	routes2 = g_new (NMIPRoute *, num);

	for (i = 0; i < num; i++) {
		routes1[i] = nm_setting_ip_config_get_route (s_ip1, i);
		routes2[i] = nm_setting_ip_config_get_route (s_ip2, i);
	}

	qsort (routes1, num, sizeof (NMIPRoute *), route_ptr_compare);
	qsort (routes2, num, sizeof (NMIPRoute *), route_ptr_compare);

	for (i = 0; i < num; i++) {
		if (route_compare (routes1[i], routes2[i], default_metric))
			return FALSE;
	}

	remove_from_hash (settings, props, s_name, NM_SETTING_IP_CONFIG_ROUTES);
	return TRUE;
}

static gboolean
check_ip4_method (NMConnection *orig,
                  NMConnection *candidate,
                  GHashTable *settings,
                  gboolean device_has_carrier)
{
	GHashTable *props;
	const char *orig_ip4_method, *candidate_ip4_method;
	NMSettingIPConfig *candidate_ip4;

	props = check_property_in_hash (settings,
	                                NM_SETTING_IP4_CONFIG_SETTING_NAME,
	                                NM_SETTING_IP_CONFIG_METHOD);
	if (!props)
		return TRUE;

	/* If the generated connection is 'disabled' (device had no IP addresses)
	 * but it has no carrier, that most likely means that IP addressing could
	 * not complete and thus no IP addresses were assigned.  In that case, allow
	 * matching to the "auto" method.
	 */
	orig_ip4_method = nm_utils_get_ip_config_method (orig, NM_TYPE_SETTING_IP4_CONFIG);
	candidate_ip4_method = nm_utils_get_ip_config_method (candidate, NM_TYPE_SETTING_IP4_CONFIG);
	candidate_ip4 = nm_connection_get_setting_ip4_config (candidate);

	if (   strcmp (orig_ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) == 0
	    && strcmp (candidate_ip4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO) == 0
	    && (!candidate_ip4 || nm_setting_ip_config_get_may_fail (candidate_ip4))
	    && (device_has_carrier == FALSE)) {
		remove_from_hash (settings, props,
		                  NM_SETTING_IP4_CONFIG_SETTING_NAME,
		                  NM_SETTING_IP_CONFIG_METHOD);
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_connection_interface_name (NMConnection *orig,
                                 NMConnection *candidate,
                                 GHashTable *settings)
{
	GHashTable *props;
	const char *orig_ifname, *cand_ifname;
	NMSettingConnection *s_con_orig, *s_con_cand;

	props = check_property_in_hash (settings,
	                                NM_SETTING_CONNECTION_SETTING_NAME,
	                                NM_SETTING_CONNECTION_INTERFACE_NAME);
	if (!props)
		return TRUE;

	/* If one of the interface names is NULL, we accept that connection */
	s_con_orig = nm_connection_get_setting_connection (orig);
	s_con_cand = nm_connection_get_setting_connection (candidate);
	orig_ifname = nm_setting_connection_get_interface_name (s_con_orig);
	cand_ifname = nm_setting_connection_get_interface_name (s_con_cand);

	if (!orig_ifname || !cand_ifname) {
		remove_from_hash (settings, props,
		                  NM_SETTING_CONNECTION_SETTING_NAME,
		                  NM_SETTING_CONNECTION_INTERFACE_NAME);
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_connection_mac_address (NMConnection *orig,
                              NMConnection *candidate,
                              GHashTable *settings)
{
	GHashTable *props;
	const char *orig_mac = NULL, *cand_mac = NULL;
	NMSettingWired *s_wired_orig, *s_wired_cand;

	props = check_property_in_hash (settings,
	                                NM_SETTING_WIRED_SETTING_NAME,
	                                NM_SETTING_WIRED_MAC_ADDRESS);
	if (!props)
		return TRUE;

	/* If one of the MAC addresses is NULL, we accept that connection */
	s_wired_orig = nm_connection_get_setting_wired (orig);
	if (s_wired_orig)
		orig_mac = nm_setting_wired_get_mac_address (s_wired_orig);

	s_wired_cand = nm_connection_get_setting_wired (candidate);
	if (s_wired_cand)
		cand_mac = nm_setting_wired_get_mac_address (s_wired_cand);

	if (!orig_mac || !cand_mac) {
		remove_from_hash (settings, props,
		                  NM_SETTING_WIRED_SETTING_NAME,
		                  NM_SETTING_WIRED_MAC_ADDRESS);
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_connection_cloned_mac_address (NMConnection *orig,
                              NMConnection *candidate,
                              GHashTable *settings)
{
	GHashTable *props;
	const char *orig_mac = NULL, *cand_mac = NULL;
	NMSettingWired *s_wired_orig, *s_wired_cand;

	props = check_property_in_hash (settings,
	                                NM_SETTING_WIRED_SETTING_NAME,
	                                NM_SETTING_WIRED_CLONED_MAC_ADDRESS);
	if (!props)
		return TRUE;

	/* If one of the MAC addresses is NULL, we accept that connection */
	s_wired_orig = nm_connection_get_setting_wired (orig);
	if (s_wired_orig)
		orig_mac = nm_setting_wired_get_cloned_mac_address (s_wired_orig);

	s_wired_cand = nm_connection_get_setting_wired (candidate);
	if (s_wired_cand)
		cand_mac = nm_setting_wired_get_cloned_mac_address (s_wired_cand);

	if (!orig_mac || !cand_mac) {
		remove_from_hash (settings, props,
		                  NM_SETTING_WIRED_SETTING_NAME,
		                  NM_SETTING_WIRED_CLONED_MAC_ADDRESS);
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_connection_s390_props (NMConnection *orig,
                             NMConnection *candidate,
                             GHashTable *settings)
{
	GHashTable *props1, *props2, *props3;
	NMSettingWired *s_wired_orig, *s_wired_cand;

	props1 = check_property_in_hash (settings,
	                                 NM_SETTING_WIRED_SETTING_NAME,
	                                 NM_SETTING_WIRED_S390_SUBCHANNELS);
	props2 = check_property_in_hash (settings,
	                                 NM_SETTING_WIRED_SETTING_NAME,
	                                 NM_SETTING_WIRED_S390_NETTYPE);
	props3 = check_property_in_hash (settings,
	                                 NM_SETTING_WIRED_SETTING_NAME,
	                                 NM_SETTING_WIRED_S390_OPTIONS);
	if (!props1 && !props2 && !props3)
		return TRUE;

	/* If the generated connection did not contain wired setting,
	 * allow it to match to a connection with a wired setting,
	 * but default (empty) s390-* properties */
	s_wired_orig = nm_connection_get_setting_wired (orig);
	s_wired_cand = nm_connection_get_setting_wired (candidate);
	if (!s_wired_orig && s_wired_cand) {
		const char * const *subchans = nm_setting_wired_get_s390_subchannels (s_wired_cand);
		const char *nettype = nm_setting_wired_get_s390_nettype (s_wired_cand);
		guint32 num_options = nm_setting_wired_get_num_s390_options (s_wired_cand);

		if ((!subchans || !*subchans) && !nettype && num_options == 0) {
			remove_from_hash (settings, props1,
			                  NM_SETTING_WIRED_SETTING_NAME,
			                  NM_SETTING_WIRED_S390_SUBCHANNELS);
			remove_from_hash (settings, props2,
			                  NM_SETTING_WIRED_SETTING_NAME,
			                  NM_SETTING_WIRED_S390_NETTYPE);
			remove_from_hash (settings, props3,
			                  NM_SETTING_WIRED_SETTING_NAME,
			                  NM_SETTING_WIRED_S390_OPTIONS);
			return TRUE;
		}
	}
	return FALSE;
}

static NMConnection *
check_possible_match (NMConnection *orig,
                      NMConnection *candidate,
                      GHashTable *settings,
                      gboolean device_has_carrier,
                      gint64 default_v4_metric,
                      gint64 default_v6_metric)
{
	g_return_val_if_fail (settings != NULL, NULL);

	if (!check_ip6_method (orig, candidate, settings))
		return NULL;

	if (!check_ip4_method (orig, candidate, settings, device_has_carrier))
		return NULL;

	if (!check_ip_routes (orig, candidate, settings, default_v4_metric, TRUE))
		return NULL;

	if (!check_ip_routes (orig, candidate, settings, default_v6_metric, FALSE))
		return NULL;

	if (!check_connection_interface_name (orig, candidate, settings))
		return NULL;

	if (!check_connection_mac_address (orig, candidate, settings))
		return NULL;

	if (!check_connection_cloned_mac_address (orig, candidate, settings))
		return NULL;

	if (!check_connection_s390_props (orig, candidate, settings))
		return NULL;

	if (g_hash_table_size (settings) == 0)
		return candidate;
	else
		return NULL;
}

/**
 * nm_utils_match_connection:
 * @connections: a (optionally pre-sorted) list of connections from which to
 * find a matching connection to @original based on "inferrable" properties
 * @original: the #NMConnection to find a match for from @connections
 * @device_has_carrier: pass %TRUE if the device that generated @original has
 * a carrier, %FALSE if not
 * @match_filter_func: a function to check whether each connection from @connections
 * should be considered for matching.  This function should return %TRUE if the
 * connection should be considered, %FALSE if the connection should be ignored
 * @match_compat_data: data pointer passed to @match_filter_func
 *
 * Checks each connection from @connections until a matching connection is found
 * considering only setting properties marked with %NM_SETTING_PARAM_INFERRABLE
 * and checking a few other characteristics like IPv6 method.  If the caller
 * desires some priority order of the connections, @connections should be
 * sorted before calling this function.
 *
 * Returns: the best #NMConnection matching @original, or %NULL if no connection
 * matches well enough.
 */
NMConnection *
nm_utils_match_connection (GSList *connections,
                           NMConnection *original,
                           gboolean device_has_carrier,
                           gint64 default_v4_metric,
                           gint64 default_v6_metric,
                           NMUtilsMatchFilterFunc match_filter_func,
                           gpointer match_filter_data)
{
	NMConnection *best_match = NULL;
	GSList *iter;

	for (iter = connections; iter; iter = iter->next) {
		NMConnection *candidate = NM_CONNECTION (iter->data);
		GHashTable *diffs = NULL;

		if (match_filter_func) {
			if (!match_filter_func (candidate, match_filter_data))
				continue;
		}

		if (!nm_connection_diff (original, candidate, NM_SETTING_COMPARE_FLAG_INFERRABLE, &diffs)) {
			if (!best_match) {
				best_match = check_possible_match (original, candidate, diffs, device_has_carrier,
				                                   default_v4_metric, default_v6_metric);
			}

			if (!best_match && nm_logging_enabled (LOGL_DEBUG, LOGD_CORE)) {
				GString *diff_string;
				GHashTableIter s_iter, p_iter;
				gpointer setting_name, setting;
				gpointer property_name, value;

				diff_string = g_string_new (NULL);
				g_hash_table_iter_init (&s_iter, diffs);
				while (g_hash_table_iter_next (&s_iter, &setting_name, &setting)) {
					g_hash_table_iter_init (&p_iter, setting);
					while (g_hash_table_iter_next (&p_iter, &property_name, &value)) {
						if (diff_string->len)
							g_string_append (diff_string, ", ");
						g_string_append_printf (diff_string, "%s.%s",
						                        (char *) setting_name,
						                        (char *) property_name);
					}
				}

				nm_log_dbg (LOGD_CORE, "Connection '%s' differs from candidate '%s' in %s",
				            nm_connection_get_id (original),
				            nm_connection_get_id (candidate),
				            diff_string->str);
				g_string_free (diff_string, TRUE);
			}

			g_hash_table_unref (diffs);
			continue;
		}

		/* Exact match */
		return candidate;
	}

	/* Best match (if any) */
	return best_match;
}

/******************************************************************************/

/**
 * nm_utils_g_value_set_object_path:
 * @value: a #GValue, initialized to store an object path
 * @object: (allow-none): an #NMExportedObject
 *
 * Sets @value to @object's object path. If @object is %NULL, or not
 * exported, @value is set to "/".
 */
void
nm_utils_g_value_set_object_path (GValue *value, gpointer object)
{
	g_return_if_fail (!object || NM_IS_EXPORTED_OBJECT (object));

	if (object && nm_exported_object_is_exported (object))
		g_value_set_string (value, nm_exported_object_get_path (object));
	else
		g_value_set_string (value, "/");
}

/**
 * nm_utils_g_value_set_object_path_array:
 * @value: a #GValue, initialized to store an object path
 * @objects: a #GSList of #NMExportedObjects
 * @filter_func: (allow-none): function to call on each object in @objects
 * @user_data: data to pass to @filter_func
 *
 * Sets @value to an array of object paths of the objects in @objects.
 */
void
nm_utils_g_value_set_object_path_array (GValue *value,
                                        GSList *objects,
                                        NMUtilsObjectFunc filter_func,
                                        gpointer user_data)
{
	char **paths;
	guint i;
	GSList *iter;

	paths = g_new (char *, g_slist_length (objects) + 1);
	for (i = 0, iter = objects; iter; iter = iter->next) {
		NMExportedObject *object = iter->data;
		const char *path;

		path = nm_exported_object_get_path (object);
		if (!path)
			continue;
		if (filter_func && !filter_func ((GObject *) object, user_data))
			continue;
		paths[i++] = g_strdup (path);
	}
	paths[i] = NULL;
	g_value_take_boxed (value, paths);
}

/******************************************************************************/
