/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2012-2013  Intel Corporation. All rights reserved.
 *  Copyright (C) 2019-2021  Jolla Ltd. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <glib.h>

#include <gdbus.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/technology.h>
#include <connman/plugin.h>
#include <connman/log.h>
#include <connman/dbus.h>
#include <connman/provider.h>
#include <connman/ipaddress.h>
#include <connman/notifier.h>
#include <connman/vpn-dbus.h>
#include <connman/inet.h>
#include <gweb/gresolv.h>

#define DBUS_TIMEOUT 10000

static DBusConnection *connection;

static GHashTable *vpn_connections = NULL;
static guint watch;
static guint added_watch;
static guint removed_watch;
static guint property_watch;

struct vpn_route {
	int family;
	char *network;
	char *netmask;
	char *gateway;
};

struct config_create_data {
	connection_ready_cb callback;
	DBusMessage *message;
	char *path;
};

struct connection_data {
	char *path;
	char *ident;
	struct connman_provider *provider;
	int index;
	DBusPendingCall *call;
	DBusPendingCall *disconnect_call;
	bool connect_pending;
	struct config_create_data *cb_data;
	char *service_ident;

	char *state;
	char *type;
	char *name;
	char *host;
	char **host_ip;
	char *domain;
	char **nameservers;
	bool immutable;
	bool default_route_set;

	GHashTable *server_routes;
	GHashTable *user_routes;
	GHashTable *setting_strings;

	struct connman_ipaddress *ip;

	GResolv *resolv;
	guint resolv_id;
	guint remove_resolv_id;
};

static int set_string(struct connman_provider *provider,
					const char *key, const char *value)
{
	struct connection_data *data;

	data = connman_provider_get_data(provider);
	if (!data)
		return -EINVAL;

	DBG("data %p provider %p key %s value %s", data, provider, key, value);

	if (g_str_equal(key, "Type")) {
		g_free(data->type);
		data->type = g_strdup(value);
	} else if (g_str_equal(key, "Name")) {
		g_free(data->name);
		data->name = g_strdup(value);
	} else if (g_str_equal(key, "Host")) {
		g_free(data->host);
		data->host = g_strdup(value);
	} else if (g_str_equal(key, "VPN.Domain") ||
				g_str_equal(key, "Domain")) {
		g_free(data->domain);
		data->domain = g_strdup(value);
	} else
		g_hash_table_replace(data->setting_strings,
				g_strdup(key), g_strdup(value));
	return 0;
}

static const char *get_string(struct connman_provider *provider,
							const char *key)
{
	struct connection_data *data;

	data = connman_provider_get_data(provider);
	if (!data)
		return NULL;

	DBG("data %p provider %p key %s", data, provider, key);

	if (g_str_equal(key, "Type"))
		return data->type;
	else if (g_str_equal(key, "Name"))
		return data->name;
	else if (g_str_equal(key, "Host"))
		return data->host;
	else if (g_str_equal(key, "HostIP")) {
		if (!data->host_ip ||
				!data->host_ip[0])
			return data->host;
		else
			return data->host_ip[0];
	} else if (g_str_equal(key, "VPN.Domain"))
		return data->domain;
	else if (g_str_equal(key, "Transport"))
		return data->service_ident;

	return g_hash_table_lookup(data->setting_strings, key);
}

static char *get_ident(const char *path)
{
	char *pos;

	if (*path != '/')
		return NULL;

	pos = strrchr(path, '/');
	if (!pos)
		return NULL;

	return pos + 1;
}

static void cancel_host_resolv(struct connection_data *data)
{

	if (data->remove_resolv_id)
		g_source_remove(data->remove_resolv_id);

	if (data->resolv && data->resolv_id)
		g_resolv_cancel_lookup(data->resolv, data->resolv_id);

	data->resolv_id = 0;
	data->remove_resolv_id = 0;

	g_resolv_unref(data->resolv);
	data->resolv = NULL;
}

static gboolean remove_resolv(gpointer user_data)
{
	struct connection_data *data = user_data;

	cancel_host_resolv(data);

	return FALSE;
}

static void resolv_result(GResolvResultStatus status,
					char **results, gpointer user_data)
{
	struct connection_data *data = user_data;

	DBG("status %d", status);

	if (status == G_RESOLV_RESULT_STATUS_SUCCESS && results &&
						g_strv_length(results) > 0) {
		g_strfreev(data->host_ip);
		data->host_ip = g_strdupv(results);
	}

	/*
	 * We cannot unref the resolver here as resolv struct is manipulated
	 * by gresolv.c after we return from this callback.
	 */
	data->remove_resolv_id = g_idle_add(remove_resolv, data);

	data->resolv_id = 0;
}

static void resolv_host_addr(struct connection_data *data)
{
	if (!data->host)
		return;

	if (connman_inet_check_ipaddress(data->host) > 0)
		return;

	if (data->host_ip)
		return;

	data->resolv = g_resolv_new(0);
	if (!data->resolv) {
		DBG("Cannot resolv %s", data->host);
		return;
	}

	DBG("Trying to resolv %s", data->host);

	data->resolv_id = g_resolv_lookup_hostname(data->resolv, data->host,
						resolv_result, data);
}

static void free_config_cb_data(struct config_create_data *cb_data)
{
	if (!cb_data)
		return;

	g_free(cb_data->path);
	cb_data->path = NULL;

	if (cb_data->message) {
		dbus_message_unref(cb_data->message);
		cb_data->message = NULL;
	}

	cb_data->callback = NULL;

	g_free(cb_data);
}

static bool provider_is_connected(struct connection_data *data)
{
	return data && (g_str_equal(data->state, "ready") ||
			g_str_equal(data->state, "configuration"));
}

static void set_provider_state(struct connection_data *data)
{
	enum connman_provider_state state = CONNMAN_PROVIDER_STATE_UNKNOWN;
	bool connected;
	int err = 0;

	DBG("provider %p new state %s", data->provider, data->state);

	connected = provider_is_connected(data);

	if (g_str_equal(data->state, "ready")) {
		state = CONNMAN_PROVIDER_STATE_READY;
		goto set;
	} else if (g_str_equal(data->state, "configuration")) {
		state = CONNMAN_PROVIDER_STATE_CONNECT;
	} else if (g_str_equal(data->state, "idle")) {
		state = CONNMAN_PROVIDER_STATE_IDLE;
	} else if (g_str_equal(data->state, "disconnect")) {
		err = ECONNREFUSED;
		state = CONNMAN_PROVIDER_STATE_DISCONNECT;
		goto set;
	} else if (g_str_equal(data->state, "failure")) {
		err = ECONNREFUSED;
		state = CONNMAN_PROVIDER_STATE_FAILURE;
		goto set;
	}

	connman_provider_set_state(data->provider, state);
	goto free;

set:
	if (data->cb_data)
		data->cb_data->callback(data->cb_data->message,
					err, data->ident);

	connman_provider_set_state(data->provider, state);

	free_config_cb_data(data->cb_data);
	data->cb_data = NULL;

free:
	if (!connected) {
		g_free(data->service_ident);
		data->service_ident = NULL;
	}
}

static int create_provider(struct connection_data *data, void *user_data)
{
	struct connman_provider_driver *driver = user_data;
	int err;

	DBG("%s", data->path);

	data->provider = connman_provider_get(data->ident);
	if (!data->provider)
		return -ENOMEM;

	DBG("provider %p name %s", data->provider, data->name);

	connman_provider_set_data(data->provider, data);
	connman_provider_set_driver(data->provider, driver);

	err = connman_provider_create_service(data->provider);
	if (err == 0) {
		connman_provider_set_immutable(data->provider, data->immutable);
		if (g_str_equal(data->state, "ready")) {
			connman_provider_set_index(data->provider,
							data->index);
			if (data->ip)
				connman_provider_set_ipaddress(data->provider,
								data->ip);
		}

		set_provider_state(data);
	}

	return 0;
}

static void destroy_route(gpointer user_data)
{
	struct vpn_route *route = user_data;

	g_free(route->network);
	g_free(route->netmask);
	g_free(route->gateway);
	g_free(route);
}

static struct connection_data *create_connection_data(const char *path)
{
	struct connection_data *data;

	data = g_try_new0(struct connection_data, 1);
	if (!data)
		return NULL;

	DBG("path %s", path);

	data->path = g_strdup(path);
	data->ident = g_strdup(get_ident(path));
	data->index = -1;

	data->setting_strings = g_hash_table_new_full(g_str_hash,
						g_str_equal, g_free, g_free);

	data->server_routes = g_hash_table_new_full(g_direct_hash,
					g_str_equal, g_free, destroy_route);
	data->user_routes = g_hash_table_new_full(g_str_hash,
					g_str_equal, g_free, destroy_route);

	return data;
}

static int extract_ip(DBusMessageIter *array, int family,
						struct connection_data *data)
{
	DBusMessageIter dict;
	char *address = NULL, *gateway = NULL, *netmask = NULL, *peer = NULL;
	unsigned char prefix_len;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
		return -EINVAL;

	dbus_message_iter_recurse(array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "Address")) {
			dbus_message_iter_get_basic(&value, &address);
			DBG("address %s", address);
		} else if (g_str_equal(key, "Netmask")) {
			dbus_message_iter_get_basic(&value, &netmask);
			DBG("netmask %s", netmask);
		} else if (g_str_equal(key, "PrefixLength")) {
			dbus_message_iter_get_basic(&value, &prefix_len);
			DBG("prefix length %u", prefix_len);
		} else if (g_str_equal(key, "Peer")) {
			dbus_message_iter_get_basic(&value, &peer);
			DBG("peer %s", peer);
		} else if (g_str_equal(key, "Gateway")) {
			dbus_message_iter_get_basic(&value, &gateway);
			DBG("gateway %s", gateway);
		}

		dbus_message_iter_next(&dict);
	}

	connman_ipaddress_free(data->ip);
	data->ip = connman_ipaddress_alloc(family);
	if (!data->ip)
		return -ENOMEM;

	switch (family) {
	case AF_INET:
		connman_ipaddress_set_ipv4(data->ip, address, netmask,
								gateway);
		break;
	case AF_INET6:
		connman_ipaddress_set_ipv6(data->ip, address, prefix_len,
								gateway);
		break;
	default:
		return -EINVAL;
	}

	connman_ipaddress_set_peer(data->ip, peer);
	connman_ipaddress_set_p2p(data->ip, true);

	return 0;
}

static int extract_nameservers(DBusMessageIter *array,
						struct connection_data *data)
{
	DBusMessageIter entry;
	char **nameservers = NULL;
	int i = 0;

	dbus_message_iter_recurse(array, &entry);

	while (dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
		const char *nameserver;

		dbus_message_iter_get_basic(&entry, &nameserver);

		nameservers = g_try_renew(char *, nameservers, i + 2);
		if (!nameservers)
			return -ENOMEM;

		DBG("[%d] %s", i, nameserver);

		nameservers[i] = g_strdup(nameserver);
		if (!nameservers[i])
			return -ENOMEM;

		nameservers[++i] = NULL;

		dbus_message_iter_next(&entry);
	}

	g_strfreev(data->nameservers);
	data->nameservers = nameservers;

	return 0;
}

static int errorstr2val(const char *error) {
	if (g_strcmp0(error, CONNMAN_ERROR_INTERFACE ".InProgress") == 0)
		return -EINPROGRESS;

	if (g_strcmp0(error, CONNMAN_ERROR_INTERFACE ".AlreadyConnected") == 0)
		return -EISCONN;

	if (g_strcmp0(error, CONNMAN_ERROR_INTERFACE ".NoCarrier") == 0)
		return -ENOLINK;

	if (g_strcmp0(error, CONNMAN_ERROR_INTERFACE ".OperationCanceled") == 0)
		return -ECANCELED;

	return -ECONNREFUSED;
}

static void connect_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;
	struct connection_data *data = user_data;
	struct config_create_data *cb_data = data->cb_data;

	DBG("");

	if (!dbus_pending_call_get_completed(call)) {
		connman_warn("vpn connect reply pending call incomplete");
		goto out;
	}

	if (call != data->call) {
		connman_error("invalid call %p to VPN connect_reply data %p "
					" call %p ", call, data, data->call);
		dbus_pending_call_unref(call);
		return;
	}

	DBG("user_data %p path %s", user_data, cb_data ? cb_data->path : NULL);

	reply = dbus_pending_call_steal_reply(call);
	if (!reply)
		goto out;

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		int err = errorstr2val(error.name);

		switch (err) {
		case -EINPROGRESS:
			break;
		/*
		 * ECANCELED means that user has canceled authentication
		 * dialog. That's not really an error, it's part of a normal
		 * workflow. We also take it as a request to turn autoconnect
		 * off, in case if it was on.
		 */
		case -ECANCELED:
			DBG("%s connect canceled", data->path);
			connman_provider_set_autoconnect(data->provider, false);
			break;
		case -ENOLINK: /* vpnd reports that connmand is not online. */
		case -EISCONN:
		case -ECONNREFUSED:
		default:
			connman_error("Connect reply: %s (%s)", error.message,
								error.name);
			DBG("data %p cb_data %p", data, cb_data);

			if (cb_data) {
				cb_data->callback(cb_data->message, err, NULL);
				free_config_cb_data(cb_data);
				data->cb_data = NULL;
			}
		}

		dbus_error_free(&error);
	}

	/*
	 * The vpn connection is up when we get a "ready" state
	 * property so at this point we do nothing for the provider
	 * state.
	 */

	dbus_message_unref(reply);

out:
	dbus_pending_call_unref(data->call);

	data->call = NULL;
	data->connect_pending = false;
}

static int connect_provider(struct connection_data *data, void *user_data,
			const char *dbus_sender)
{
	DBusPendingCall *call;
	DBusMessage *message;
	struct config_create_data *cb_data = user_data;
	struct connman_service *transport = connman_service_get_default();

	DBG("data %p user %p path %s sender %s", data, cb_data, data->path,
								dbus_sender);

	if (!transport) {
		DBG("no default service, refusing to connect");
		return -EINVAL;
	}

	if (data->connect_pending && data->call) {
		connman_info("connect already pending");
		return -EALREADY;
	}

	/* We need to pass original dbus sender to connman-vpnd,
	 * use a Connect2 method for that if the original dbus sender is set.
	 * Connect method requires no parameter, Connect2 requires dbus sender
	 * name to be set.
	 */
	message = dbus_message_new_method_call(VPN_SERVICE, data->path,
					VPN_CONNECTION_INTERFACE,
					dbus_sender && *dbus_sender ?
						VPN_CONNECT2 : VPN_CONNECT);
	if (!message)
		return -ENOMEM;

	if (dbus_sender && *dbus_sender)
		dbus_message_append_args(message, DBUS_TYPE_STRING,
					&dbus_sender, NULL);
	else
		dbus_sender = "";

	if (!dbus_connection_send_with_reply(connection, message,
						&call, DBUS_TIMEOUT)) {
		connman_error("Unable to call %s.%s()",
			VPN_CONNECTION_INTERFACE, dbus_sender && *dbus_sender ?
						VPN_CONNECT2 : VPN_CONNECT);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!call) {
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (data->call) {
		dbus_pending_call_cancel(data->call);
		dbus_pending_call_unref(data->call);
	}

	data->call = call;
	data->connect_pending = true;

	if (cb_data) {
		g_free(cb_data->path);
		cb_data->path = g_strdup(data->path);
	}

	/*
	 * This is the service which (most likely) will be used
	 * as a transport for VPN connection.
	 */
	g_free(data->service_ident);
	data->service_ident =
		g_strdup(connman_service_get_identifier(transport));
	DBG("transport %s", data->service_ident);

	dbus_pending_call_set_notify(call, connect_reply, data, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static void add_connection(const char *path, DBusMessageIter *properties,
			void *user_data)
{
	struct connection_data *data;
	int err;
	char *ident = get_ident(path);
	bool found = false;

	data = g_hash_table_lookup(vpn_connections, ident);
	if (data) {
		/*
		 * We might have a dummy connection struct here that
		 * was created by configuration_create_reply() so in
		 * that case just continue.
		 */
		if (!data->connect_pending)
			return;

		found = true;
	} else {
		data = create_connection_data(path);
		if (!data)
			return;
	}

	DBG("data %p path %s", data, path);

	while (dbus_message_iter_get_arg_type(properties) ==
			DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		const char *key;
		char *str;

		dbus_message_iter_recurse(properties, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "State")) {
			dbus_message_iter_get_basic(&value, &str);
			DBG("state %s -> %s", data->state, str);
			data->state = g_strdup(str);
		} else if (g_str_equal(key, "IPv4")) {
			extract_ip(&value, AF_INET, data);
		} else if (g_str_equal(key, "IPv6")) {
			extract_ip(&value, AF_INET6, data);
		} else if (g_str_equal(key, "Name")) {
			dbus_message_iter_get_basic(&value, &str);
			data->name = g_strdup(str);
		} else if (g_str_equal(key, "Type")) {
			dbus_message_iter_get_basic(&value, &str);
			data->type = g_strdup(str);
		} else if (g_str_equal(key, "Immutable")) {
			dbus_bool_t immutable;

			dbus_message_iter_get_basic(&value, &immutable);
			data->immutable = immutable;
		} else if (g_str_equal(key, "Host")) {
			dbus_message_iter_get_basic(&value, &str);
			data->host = g_strdup(str);
		} else if (g_str_equal(key, "Domain")) {
			dbus_message_iter_get_basic(&value, &str);
			g_free(data->domain);
			data->domain = g_strdup(str);
		} else if (g_str_equal(key, "Nameservers")) {
			extract_nameservers(&value, data);
		} else if (g_str_equal(key, "Index")) {
			dbus_message_iter_get_basic(&value, &data->index);
		} else if (g_str_equal(key, "ServerRoutes")) {
			/* Ignored */
		} else if (g_str_equal(key, "UserRoutes")) {
			/* Ignored */
		} else {
			if (dbus_message_iter_get_arg_type(&value) ==
							DBUS_TYPE_STRING) {
				dbus_message_iter_get_basic(&value, &str);
				g_hash_table_replace(data->setting_strings,
						g_strdup(key), g_strdup(str));
			} else {
				DBG("unknown key %s", key);
			}
		}

		dbus_message_iter_next(properties);
	}

	if (!found)
		g_hash_table_insert(vpn_connections, g_strdup(data->ident),
									data);

	err = create_provider(data, user_data);
	if (err < 0)
		goto out;

	resolv_host_addr(data);

	if (data->nameservers)
		connman_provider_set_nameservers(data->provider,
						data->nameservers);

	if (data->domain)
		connman_provider_set_domain(data->provider,
						data->domain);

	if (data->connect_pending) {
		const char *dbus_sender = NULL;

		if (data->cb_data && data->cb_data->message) {
			dbus_sender =
				dbus_message_get_sender(data->cb_data->message);
		}
		connect_provider(data, data->cb_data, dbus_sender);
	}

	return;

out:
	DBG("removing %s", data->ident);
	g_hash_table_remove(vpn_connections, data->ident);
}

static void get_connections_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;
	DBusMessageIter array, dict;
	const char *signature = DBUS_TYPE_ARRAY_AS_STRING
		DBUS_STRUCT_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_OBJECT_PATH_AS_STRING
		DBUS_TYPE_ARRAY_AS_STRING
		DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING
		DBUS_DICT_ENTRY_END_CHAR_AS_STRING
		DBUS_STRUCT_END_CHAR_AS_STRING;

	DBG("");

	if (!dbus_pending_call_get_completed(call)) {
		connman_warn("get connections reply pending call incomplete");
		goto out;
	}

	reply = dbus_pending_call_steal_reply(call);
	if (!reply)
		goto out;

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("%s", error.message);
		dbus_error_free(&error);
		goto done;
	}

	if (!dbus_message_has_signature(reply, signature)) {
		connman_error("vpnd signature \"%s\" does not match "
							"expected \"%s\"",
			dbus_message_get_signature(reply), signature);
		goto done;
	}

	if (!dbus_message_iter_init(reply, &array))
		goto done;

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_STRUCT) {
		DBusMessageIter value, properties;
		const char *path;

		dbus_message_iter_recurse(&dict, &value);
		dbus_message_iter_get_basic(&value, &path);

		dbus_message_iter_next(&value);
		dbus_message_iter_recurse(&value, &properties);

		add_connection(path, &properties, user_data);

		dbus_message_iter_next(&dict);
	}

done:
	dbus_message_unref(reply);

out:
	dbus_pending_call_unref(call);
}

static int get_connections(void *user_data)
{
	DBusPendingCall *call;
	DBusMessage *message;

	DBG("");

	message = dbus_message_new_method_call(VPN_SERVICE, "/",
					VPN_MANAGER_INTERFACE,
					GET_CONNECTIONS);
	if (!message)
		return -ENOMEM;

	if (!dbus_connection_send_with_reply(connection, message,
						&call, DBUS_TIMEOUT)) {
		connman_error("Unable to call %s.%s()", VPN_MANAGER_INTERFACE,
							GET_CONNECTIONS);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!call) {
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(call, get_connections_reply,
							user_data, NULL);

	dbus_message_unref(message);

	return -EINPROGRESS;
}

static int provider_probe(struct connman_provider *provider)
{
	return 0;
}

static void remove_connection_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;

	DBG("");

	if (!dbus_pending_call_get_completed(call)) {
		connman_warn("remove connection reply pending call incomplete");
		goto out;
	}

	reply = dbus_pending_call_steal_reply(call);
	if (!reply)
		goto out;

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		/*
		 * If the returned error is NotFound, it means that we
		 * have actually removed the provider in vpnd already.
		 */
		if (!dbus_error_has_name(&error,
				CONNMAN_ERROR_INTERFACE".NotFound"))
			connman_error("%s", error.message);

		dbus_error_free(&error);
	}

	dbus_message_unref(reply);

out:
	dbus_pending_call_unref(call);
}

static int provider_remove(struct connman_provider *provider)
{
	DBusPendingCall *call;
	DBusMessage *message;
	struct connection_data *data;

	data = connman_provider_get_data(provider);

	DBG("provider %p data %p", provider, data);

	if (!data) {
		/*
		 * This means the provider is already removed,
		 * just ignore the dbus in this case.
		 */
		return -EALREADY;
	}

	/*
	 * When provider.c:provider_remove() calls this function,
	 * it will remove the provider itself after the call.
	 * This means that we cannot use the provider pointer later
	 * as it is no longer valid.
	 */
	data->provider = NULL;

	message = dbus_message_new_method_call(VPN_SERVICE, "/",
					VPN_MANAGER_INTERFACE,
					VPN_REMOVE);
	if (!message)
		return -ENOMEM;

	dbus_message_append_args(message, DBUS_TYPE_OBJECT_PATH, &data->path,
				NULL);

	if (!dbus_connection_send_with_reply(connection, message,
						&call, DBUS_TIMEOUT)) {
		connman_error("Unable to call %s.%s()", VPN_MANAGER_INTERFACE,
							VPN_REMOVE);
		dbus_message_unref(message);
		return -EINVAL;
	}

	if (!call) {
		dbus_message_unref(message);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(call, remove_connection_reply,
							NULL, NULL);

	dbus_message_unref(message);

	return 0;
}

static int provider_connect(struct connman_provider *provider,
					const char *dbus_sender)
{
	struct connection_data *data;

	data = connman_provider_get_data(provider);
	if (!data)
		return -EINVAL;

	return connect_provider(data, NULL, dbus_sender);
}

static void disconnect_reply(DBusPendingCall *call, void *user_data)
{
	struct connection_data *data = user_data;
	DBusMessage *reply;
	DBusError error;

	DBG("user %p", user_data);

	reply = dbus_pending_call_steal_reply(call);

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("%s", error.message);
		dbus_error_free(&error);
		goto done;
	}

done:
	dbus_message_unref(reply);
	dbus_pending_call_unref(call);
	data->disconnect_call = NULL;
}

static int disconnect_provider(struct connection_data *data)
{
	bool sent;
	DBusMessage *message;

	DBG("data %p path %s", data, data->path);

	if (data->disconnect_call) {
		DBG("already disconnecting");
		return -EALREADY;
	}

	message = dbus_message_new_method_call(VPN_SERVICE, data->path,
					VPN_CONNECTION_INTERFACE,
					VPN_DISCONNECT);
	if (!message)
		return -ENOMEM;

	sent = dbus_connection_send_with_reply(connection, message,
					&data->disconnect_call, DBUS_TIMEOUT);
	dbus_message_unref(message);

	if (!sent || !data->disconnect_call) {
		connman_error("Unable to call %s.%s()",
			VPN_CONNECTION_INTERFACE, VPN_DISCONNECT);
		return -EINVAL;
	}

	dbus_pending_call_set_notify(data->disconnect_call, disconnect_reply,
								data, NULL);

	data->default_route_set = false;

	connman_provider_set_state(data->provider,
					CONNMAN_PROVIDER_STATE_DISCONNECT);

	g_free(data->service_ident);
	data->service_ident = NULL;

	return -EINPROGRESS;
}

static int provider_disconnect(struct connman_provider *provider)
{
	int err = 0;

	struct connection_data *data;

	DBG("provider %p", provider);

	data = connman_provider_get_data(provider);
	if (!data)
		return -EINVAL;

	if (provider_is_connected(data))
		err = disconnect_provider(data);

	if (data->call) {
		dbus_pending_call_cancel(data->call);
		dbus_pending_call_unref(data->call);
		data->call = NULL;
	}

	data->connect_pending = false;

	return err;
}

static void configuration_create_reply(DBusPendingCall *call, void *user_data)
{
	DBusMessage *reply;
	DBusError error;
	DBusMessageIter iter;
	const char *signature = DBUS_TYPE_OBJECT_PATH_AS_STRING;
	const char *path;
	char *ident;
	struct connection_data *data;
	struct config_create_data *cb_data = user_data;

	DBG("user %p", cb_data);

	if (!dbus_pending_call_get_completed(call)) {
		connman_warn("configuration create reply pending call incomplete");
		goto out;
	}

	reply = dbus_pending_call_steal_reply(call);
	if (!reply)
		goto out;

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		connman_error("dbus error: %s", error.message);
		dbus_error_free(&error);
		goto done;
	}

	if (!dbus_message_has_signature(reply, signature)) {
		connman_error("vpn configuration signature \"%s\" does not "
						"match expected \"%s\"",
			dbus_message_get_signature(reply), signature);
		goto done;
	}

	if (!dbus_message_iter_init(reply, &iter))
		goto done;

	dbus_message_iter_get_basic(&iter, &path);

	/*
	 * Then try to connect the VPN as expected by ConnectProvider API
	 */
	ident = get_ident(path);

	data = g_hash_table_lookup(vpn_connections, ident);
	if (!data) {
		/*
		 * Someone removed the data. We cannot really continue.
		 */
		DBG("Pending data not found for %s, cannot continue!", ident);
	} else {
		data->call = NULL;
		data->connect_pending = true;

		if (!data->cb_data)
			data->cb_data = cb_data;
		else
			DBG("Connection callback data already in use!");

		/*
		 * Connection is created in add_connections() after
		 * we have received the ConnectionAdded signal.
		 */

		DBG("cb %p msg %p", data->cb_data,
			data->cb_data ? data->cb_data->message : NULL);
	}

done:
	dbus_message_unref(reply);

out:
	dbus_pending_call_unref(call);
}

static void set_dbus_ident(char *ident)
{
	int i, len = strlen(ident);

	for (i = 0; i < len; i++) {
		if (ident[i] >= '0' && ident[i] <= '9')
			continue;
		if (ident[i] >= 'a' && ident[i] <= 'z')
			continue;
		if (ident[i] >= 'A' && ident[i] <= 'Z')
			continue;
		ident[i] = '_';
	}
}

static struct vpn_route *parse_user_route(const char *user_route)
{
	char *network, *netmask;
	struct vpn_route *route = NULL;
	int family = PF_UNSPEC;
	char **elems = g_strsplit(user_route, "/", 0);

	if (!elems)
		return NULL;

	network = elems[0];
	if (!network || *network == '\0') {
		DBG("no network/netmask set");
		goto out;
	}

	netmask = elems[1];
	if (netmask && *netmask == '\0') {
		DBG("no netmask set");
		goto out;
	}

	if (g_strrstr(network, ":"))
		family = AF_INET6;
	else if (g_strrstr(network, ".")) {
		family = AF_INET;

		if (!g_strrstr(netmask, ".")) {
			/* We have netmask length */
			in_addr_t addr;
			struct in_addr netmask_in;
			unsigned char prefix_len = 32;

			if (netmask) {
				char *ptr;
				long int value = strtol(netmask, &ptr, 10);
				if (ptr != netmask && *ptr == '\0' &&
						value && value <= 32)
					prefix_len = value;
			}

			addr = 0xffffffff << (32 - prefix_len);
			netmask_in.s_addr = htonl(addr);
			netmask = inet_ntoa(netmask_in);

			DBG("network %s netmask %s", network, netmask);
		}
	}

	route = g_try_new(struct vpn_route, 1);
	if (!route)
		goto out;

	route->network = g_strdup(network);
	route->netmask = g_strdup(netmask);
	route->gateway = NULL;
	route->family = family;

out:
	g_strfreev(elems);
	return route;
}

static GSList *get_user_networks(DBusMessageIter *array)
{
	DBusMessageIter entry;
	GSList *list = NULL;

	dbus_message_iter_recurse(array, &entry);

	while (dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) {
		const char *val;
		struct vpn_route *route;

		dbus_message_iter_get_basic(&entry, &val);

		route = parse_user_route(val);
		if (route)
			list = g_slist_prepend(list, route);

		dbus_message_iter_next(&entry);
	}

	return list;
}

static void append_route(DBusMessageIter *iter, void *user_data)
{
	struct vpn_route *route = user_data;
	DBusMessageIter item;
	int family = 0;

	connman_dbus_dict_open(iter, &item);

	if (!route)
		goto empty_dict;

	if (route->family == AF_INET)
		family = 4;
	else if (route->family == AF_INET6)
		family = 6;

	if (family != 0)
		connman_dbus_dict_append_basic(&item, "ProtocolFamily",
					DBUS_TYPE_INT32, &family);

	if (route->network)
		connman_dbus_dict_append_basic(&item, "Network",
					DBUS_TYPE_STRING, &route->network);

	if (route->netmask)
		connman_dbus_dict_append_basic(&item, "Netmask",
					DBUS_TYPE_STRING, &route->netmask);

	if (route->gateway)
		connman_dbus_dict_append_basic(&item, "Gateway",
					DBUS_TYPE_STRING, &route->gateway);

empty_dict:
	connman_dbus_dict_close(iter, &item);
}

static void append_routes(DBusMessageIter *iter, void *user_data)
{
	GSList *list, *routes = user_data;

	DBG("routes %p", routes);

	for (list = routes; list; list = g_slist_next(list)) {
		DBusMessageIter dict;
		struct vpn_route *route = list->data;

		dbus_message_iter_open_container(iter, DBUS_TYPE_STRUCT, NULL,
						&dict);
		append_route(&dict, route);
		dbus_message_iter_close_container(iter, &dict);
	}
}

static int create_configuration(DBusMessage *msg, connection_ready_cb callback)
{
	DBusMessage *new_msg = NULL;
	DBusPendingCall *call;
	DBusMessageIter iter, array, new_iter, new_dict;
	const char *type = NULL, *name = NULL;
	const char *host = NULL, *domain = NULL;
	char *ident, *me = NULL;
	int err = 0;
	dbus_bool_t result;
	struct connection_data *data;
	struct config_create_data *user_data = NULL;
	GSList *networks = NULL;

	/*
	 * We copy the old message data into new message. We cannot
	 * just use the old message as is because the user route
	 * information is not in the same format in vpnd.
	 */
	new_msg = dbus_message_new(DBUS_MESSAGE_TYPE_METHOD_CALL);
	dbus_message_iter_init_append(new_msg, &new_iter);
	connman_dbus_dict_open(&new_iter, &new_dict);

	dbus_message_iter_init(msg, &iter);
	dbus_message_iter_recurse(&iter, &array);

	while (dbus_message_iter_get_arg_type(&array) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry, value;
		void *item_value;
		const char *key;
		int value_type;

		dbus_message_iter_recurse(&array, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		value_type = dbus_message_iter_get_arg_type(&value);
		item_value = NULL;

		switch (value_type) {
		case DBUS_TYPE_STRING:
			dbus_message_iter_get_basic(&value, &item_value);

			if (g_str_equal(key, "Type"))
				type = (const char *)item_value;
			else if (g_str_equal(key, "Name"))
				name = (const char *)item_value;
			else if (g_str_equal(key, "Host"))
				host = (const char *)item_value;
			else if (g_str_equal(key, "VPN.Domain"))
				domain = (const char *)item_value;

			DBG("%s %s", key, (char *)item_value);

			if (item_value)
				connman_dbus_dict_append_basic(&new_dict, key,
						value_type, &item_value);
			break;
		case DBUS_TYPE_ARRAY:
			if (g_str_equal(key, "Networks")) {
				networks = get_user_networks(&value);
				connman_dbus_dict_append_array(&new_dict,
							"UserRoutes",
							DBUS_TYPE_DICT_ENTRY,
							append_routes,
							networks);
			}
			break;
		}

		dbus_message_iter_next(&array);
	}

	connman_dbus_dict_close(&new_iter, &new_dict);

	DBG("VPN type %s name %s host %s domain %s networks %p",
		type, name, host, domain, networks);

	if (!host || !domain) {
		err = -EINVAL;
		goto done;
	}

	if (!type || !name) {
		err = -EOPNOTSUPP;
		goto done;
	}

	ident = g_strdup_printf("%s_%s", host, domain);
	set_dbus_ident(ident);

	DBG("ident %s", ident);

	data = g_hash_table_lookup(vpn_connections, ident);
	if (data) {
		if (data->call || data->cb_data) {
			DBG("create configuration call already pending");
			err = -EINPROGRESS;
			goto done;
		}
	} else {
		char *path = g_strdup_printf("%s/connection/%s", VPN_PATH,
								ident);
		data = create_connection_data(path);
		g_free(path);

		if (!data) {
			err = -ENOMEM;
			goto done;
		}

		g_hash_table_insert(vpn_connections, g_strdup(ident), data);
	}

	/*
	 * User called net.connman.Manager.ConnectProvider if we are here.
	 * So use the data from original message in the new msg.
	 */
	me = g_strdup(dbus_message_get_destination(msg));

	dbus_message_set_interface(new_msg, VPN_MANAGER_INTERFACE);
	dbus_message_set_path(new_msg, "/");
	dbus_message_set_destination(new_msg, VPN_SERVICE);
	dbus_message_set_sender(new_msg, me);
	dbus_message_set_member(new_msg, "Create");

	user_data = g_try_new0(struct config_create_data, 1);
	if (!user_data) {
		err = -ENOMEM;
		goto done;
	}

	user_data->callback = callback;
	user_data->message = dbus_message_ref(msg);
	user_data->path = NULL;

	DBG("cb %p msg %p", user_data, msg);

	result = dbus_connection_send_with_reply(connection, new_msg,
						&call, DBUS_TIMEOUT);
	if (!result || !call) {
		err = -EIO;
		goto done;
	}

	dbus_pending_call_set_notify(call, configuration_create_reply,
							user_data, NULL);
	data->call = call;

done:
	if (new_msg)
		dbus_message_unref(new_msg);

	if (networks)
		g_slist_free_full(networks, destroy_route);

	g_free(me);
	return err;
}

static bool check_host(char **hosts, char *host)
{
	int i;

	if (!hosts)
		return false;

	for (i = 0; hosts[i]; i++) {
		if (g_strcmp0(hosts[i], host) == 0)
			return true;
	}

	return false;
}

static void set_route(struct connection_data *data, struct vpn_route *route)
{
	/*
	 * If the VPN administrator/user has given a route to
	 * VPN server, then we must discard that because the
	 * server cannot be contacted via VPN tunnel.
	 */
	if (check_host(data->host_ip, route->network)) {
		DBG("Discarding VPN route to %s via %s at index %d",
			route->network, route->gateway, data->index);
		return;
	}

	DBG("set route provider %p %s/%s/%s", data->provider,
						route->network, route->gateway,
						route->netmask);

	/* Do not add default route for split routed VPNs.*/
	if (connman_provider_is_split_routing(data->provider) &&
				connman_inet_is_default_route(route->family,
					route->network, route->gateway,
					route->netmask))
		return;

	if (route->family == AF_INET6) {
		unsigned char prefix_len = atoi(route->netmask);

		connman_inet_add_ipv6_network_route(data->index,
							route->network,
							route->gateway,
							prefix_len);
	} else {
		connman_inet_add_network_route(data->index, route->network,
						route->gateway,
						route->netmask);
	}

	if (connman_inet_is_default_route(route->family, route->network,
					route->gateway, route->netmask))
		data->default_route_set = true;
}

static int save_route(GHashTable *routes, int family, const char *network,
			const char *netmask, const char *gateway);

static int add_network_route(struct connection_data *data)
{
	struct vpn_route rt = { 0, };
	int err;

	if (!data)
		return -EINVAL;

	rt.family = connman_provider_get_family(data->provider);
	switch (rt.family) {
	case PF_INET:
		err = connman_inet_get_route_addresses(data->index,
					&rt.network, &rt.netmask, &rt.gateway);
		break;
	case PF_INET6:
		err = connman_inet_ipv6_get_route_addresses(data->index,
					&rt.network, &rt.netmask, &rt.gateway);
		break;
	default:
		connman_error("invalid protocol family %d", rt.family);
		return -EINVAL;
	}

	DBG("network %s gateway %s netmask %s for provider %p",
						rt.network, rt.gateway, rt.netmask,
						data->provider);

	if (err) {
		connman_error("cannot get network/gateway/netmask for %p",
							data->provider);
		goto out;
	}

	err = save_route(data->server_routes, rt.family, rt.network, rt.netmask,
				rt.gateway);
	if (err) {
		connman_warn("failed to add network route for provider"
					"%p", data->provider);
		goto out;
	}

	set_route(data, &rt);

out:
	g_free(rt.network);
	g_free(rt.netmask);
	g_free(rt.gateway);

	return 0;
}

static bool is_valid_route_table(struct connman_provider *provider,
							GHashTable *table)
{
	GHashTableIter iter;
	gpointer value, key;
	struct vpn_route *route;
	size_t table_size;

	if (!table)
		return false;

	table_size = g_hash_table_size(table);

	/* Non-split routed may have only the default route */
	if (table_size > 0 && !connman_provider_is_split_routing(provider))
		return true;

	/* Split routed has more than the default route */
	if (table_size > 1)
		return true;

	/*
	 * Only one route for split routed VPN, which should not be the
	 * default route.
	 */
	g_hash_table_iter_init(&iter, table);
	if (!g_hash_table_iter_next(&iter, &key, &value)) /* First and only */
		return false;

	route = value;
	if (!route)
		return false;

	DBG("check route %d %s/%s/%s", route->family, route->network,
					route->gateway, route->netmask);

	if (!connman_inet_is_default_route(route->family, route->network,
				route->gateway, route->netmask))
		return true;

	return false;
}

static bool check_routes(struct connman_provider *provider)
{
	struct connection_data *data;;

	DBG("provider %p", provider);

	data = connman_provider_get_data(provider);
	if (!data)
		return false;

	if (is_valid_route_table(provider, data->user_routes))
		return true;

	if (is_valid_route_table(provider, data->server_routes))
		return true;

	return false;
}

static int set_routes(struct connman_provider *provider,
				enum connman_provider_route_type type)
{
	struct connection_data *data;
	GHashTableIter iter;
	gpointer value, key;

	DBG("provider %p", provider);

	data = connman_provider_get_data(provider);
	if (!data)
		return -EINVAL;

	if (type == CONNMAN_PROVIDER_ROUTE_ALL ||
					type == CONNMAN_PROVIDER_ROUTE_USER) {
		g_hash_table_iter_init(&iter, data->user_routes);

		while (g_hash_table_iter_next(&iter, &key, &value))
			set_route(data, value);
	}

	if (type == CONNMAN_PROVIDER_ROUTE_ALL ||
				type == CONNMAN_PROVIDER_ROUTE_SERVER) {
		g_hash_table_iter_init(&iter, data->server_routes);

		while (g_hash_table_iter_next(&iter, &key, &value))
			set_route(data, value);
	}

	/* If non-split routed VPN does not have a default route, add it */
	if (!connman_provider_is_split_routing(provider) &&
						!data->default_route_set) {
		int family = connman_provider_get_family(provider);
		const char *ipaddr_any = family == AF_INET6 ?
							"::" : "0.0.0.0";
		struct vpn_route def_route = {family, (char*) ipaddr_any,
						(char*) ipaddr_any, NULL};

		set_route(data, &def_route);
	}

	/* Split routed VPN must have at least one route to the network */
	if (connman_provider_is_split_routing(provider) &&
						!check_routes(provider)) {
		int err = add_network_route(data);
		if (err) {
			connman_warn("cannot add network route provider %p",
								provider);
			return err;
		}
	}

	return 0;
}

static struct connman_provider_driver provider_driver = {
	.name = "VPN",
	.type = CONNMAN_PROVIDER_TYPE_VPN,
	.probe = provider_probe,
	.remove = provider_remove,
	.connect = provider_connect,
	.disconnect = provider_disconnect,
	.set_property = set_string,
	.get_property = get_string,
	.create = create_configuration,
	.set_routes = set_routes,
	.check_routes = check_routes,
};

static void destroy_provider(struct connection_data *data)
{
	DBG("data %p", data);

	if (provider_is_connected(data))
		connman_provider_disconnect(data->provider);

	connman_provider_set_data(data->provider, NULL);
	connman_provider_remove(data->provider);
	data->provider = NULL;
}

static void connection_destroy(gpointer hash_data)
{
	struct connection_data *data = hash_data;

	DBG("data %p", data);

	if (data->provider)
		destroy_provider(data);

	if (data->call) {
		dbus_pending_call_cancel(data->call);
		dbus_pending_call_unref(data->call);
	}

	if (data->disconnect_call) {
		dbus_pending_call_cancel(data->disconnect_call);
		dbus_pending_call_unref(data->disconnect_call);
	}

	g_free(data->service_ident);
	g_free(data->path);
	g_free(data->ident);
	g_free(data->state);
	g_free(data->type);
	g_free(data->name);
	g_free(data->host);
	g_strfreev(data->host_ip);
	g_free(data->domain);
	g_hash_table_destroy(data->server_routes);
	g_hash_table_destroy(data->user_routes);
	g_strfreev(data->nameservers);
	g_hash_table_destroy(data->setting_strings);
	connman_ipaddress_free(data->ip);

	cancel_host_resolv(data);

	g_free(data);
}

static void vpnd_created(DBusConnection *conn, void *user_data)
{
	DBG("connection %p", conn);

	get_connections(user_data);
}

static void vpnd_removed(DBusConnection *conn, void *user_data)
{
	DBG("connection %p", conn);

	g_hash_table_remove_all(vpn_connections);
}

static void remove_connection(DBusConnection *conn, const char *path)
{
	DBG("path %s", path);

	g_hash_table_remove(vpn_connections, get_ident(path));
}

static gboolean connection_removed(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	const char *path;
	const char *signature = DBUS_TYPE_OBJECT_PATH_AS_STRING;
	struct connection_data *data;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("vpn removed signature \"%s\" does not match "
							"expected \"%s\"",
			dbus_message_get_signature(message), signature);
		return TRUE;
	}

	dbus_message_get_args(message, NULL, DBUS_TYPE_OBJECT_PATH, &path,
				DBUS_TYPE_INVALID);

	data = g_hash_table_lookup(vpn_connections, get_ident(path));
	if (data)
		remove_connection(conn, path);

	return TRUE;
}

static gboolean connection_added(DBusConnection *conn, DBusMessage *message,
				void *user_data)
{
	DBusMessageIter iter, properties;
	const char *path;
	const char *signature = DBUS_TYPE_OBJECT_PATH_AS_STRING
		DBUS_TYPE_ARRAY_AS_STRING
		DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
		DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING
		DBUS_DICT_ENTRY_END_CHAR_AS_STRING;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("vpn ConnectionAdded signature \"%s\" does not "
						"match expected \"%s\"",
			dbus_message_get_signature(message), signature);
		return TRUE;
	}

	DBG("");

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &path);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &properties);

	add_connection(path, &properties, user_data);

	return TRUE;
}

static int save_route(GHashTable *routes, int family, const char *network,
			const char *netmask, const char *gateway)
{
	struct vpn_route *route;
	char *key = g_strdup_printf("%d/%s/%s", family, network, netmask);

	DBG("family %d network %s netmask %s", family, network, netmask);

	route = g_hash_table_lookup(routes, key);
	if (!route) {
		route = g_try_new0(struct vpn_route, 1);
		if (!route) {
			connman_error("out of memory");
			return -ENOMEM;
		}

		route->family = family;
		route->network = g_strdup(network);
		route->netmask = g_strdup(netmask);
		route->gateway = g_strdup(gateway);

		g_hash_table_replace(routes, key, route);
	} else {
		g_free(key);
		return -EALREADY;
	}

	return 0;
}

static void change_provider_split_routing(struct connman_provider *provider,
							bool split_routing)
{
	struct connection_data *data;
	int err;

	if (!provider)
		return;

	if (connman_provider_is_split_routing(provider) == split_routing)
		return;

	data = connman_provider_get_data(provider);
	if (split_routing && data && provider_is_connected(data) &&
						!check_routes(provider)) {
		err = add_network_route(data);
		if (err) {
			connman_warn("cannot add network route provider %p",
								provider);
			return;
		}
	}

	err = connman_provider_set_split_routing(provider, split_routing);
	switch (err) {
	case 0:
		/* fall through */
	case -EALREADY:
		break;
	case -EINVAL:
		/* fall through */
	case -EOPNOTSUPP:
		connman_warn("cannot change split routing %d", err);
	default:
		break;
	}
}

static int read_route_dict(GHashTable *routes, DBusMessageIter *dicts)
{
	DBusMessageIter dict;
	const char *network, *netmask, *gateway;
	int family;

	dbus_message_iter_recurse(dicts, &dict);

	network = netmask = gateway = NULL;
	family = PF_UNSPEC;

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {

		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);

		if (g_str_equal(key, "ProtocolFamily")) {
			int pf;
			dbus_message_iter_get_basic(&value, &pf);
			switch (pf) {
			case 4:
				family = AF_INET;
				break;
			case 6:
				family = AF_INET6;
				break;
			}
			DBG("family %d", family);
		} else if (g_str_equal(key, "Netmask")) {
			dbus_message_iter_get_basic(&value, &netmask);
			DBG("netmask %s", netmask);
		} else if (g_str_equal(key, "Network")) {
			dbus_message_iter_get_basic(&value, &network);
			DBG("host %s", network);
		} else if (g_str_equal(key, "Gateway")) {
			dbus_message_iter_get_basic(&value, &gateway);
			DBG("gateway %s", gateway);
		}

		dbus_message_iter_next(&dict);
	}

	if (!netmask || !network || !gateway) {
		DBG("Value missing.");
		return -EINVAL;
	}

	return save_route(routes, family, network, netmask, gateway);
}

static int routes_changed(DBusMessageIter *array, GHashTable *routes)
{
	DBusMessageIter entry;
	int ret = -EINVAL;

	if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY) {
		DBG("Expecting array, ignoring routes.");
		return -EINVAL;
	}

	while (dbus_message_iter_get_arg_type(array) == DBUS_TYPE_ARRAY) {

		dbus_message_iter_recurse(array, &entry);

		while (dbus_message_iter_get_arg_type(&entry) ==
							DBUS_TYPE_STRUCT) {
			DBusMessageIter dicts;

			dbus_message_iter_recurse(&entry, &dicts);

			while (dbus_message_iter_get_arg_type(&dicts) ==
							DBUS_TYPE_ARRAY) {
				int err = read_route_dict(routes, &dicts);
				if (ret != 0)
					ret = err;
				dbus_message_iter_next(&dicts);
			}

			dbus_message_iter_next(&entry);
		}

		dbus_message_iter_next(array);
	}

	return ret;
}

static gboolean property_changed(DBusConnection *conn,
				DBusMessage *message,
				void *user_data)
{
	const char *path = dbus_message_get_path(message);
	struct connection_data *data = NULL;
	DBusMessageIter iter, value;
	bool ip_set = false;
	int err;
	char *str;
	const char *key;
	const char *signature =	DBUS_TYPE_STRING_AS_STRING
		DBUS_TYPE_VARIANT_AS_STRING;

	if (!dbus_message_has_signature(message, signature)) {
		connman_error("vpn property signature \"%s\" does not match "
							"expected \"%s\"",
			dbus_message_get_signature(message), signature);
		return TRUE;
	}

	data = g_hash_table_lookup(vpn_connections, get_ident(path));
	if (!data)
		return TRUE;

	if (!dbus_message_iter_init(message, &iter))
		return TRUE;

	dbus_message_iter_get_basic(&iter, &key);

	dbus_message_iter_next(&iter);
	dbus_message_iter_recurse(&iter, &value);

	DBG("key %s", key);

	if (g_str_equal(key, "State")) {
		dbus_message_iter_get_basic(&value, &str);

		DBG("%s %s -> %s", data->path, data->state, str);

		if (g_str_equal(data->state, str))
			return TRUE;

		g_free(data->state);
		data->state = g_strdup(str);

		set_provider_state(data);
	} else if (g_str_equal(key, "Index")) {
		dbus_message_iter_get_basic(&value, &data->index);
		connman_provider_set_index(data->provider, data->index);
	} else if (g_str_equal(key, "IPv4")) {
		err = extract_ip(&value, AF_INET, data);
		ip_set = true;
	} else if (g_str_equal(key, "IPv6")) {
		err = extract_ip(&value, AF_INET6, data);
		ip_set = true;
	} else if (g_str_equal(key, "ServerRoutes")) {
		err = routes_changed(&value, data->server_routes);
		/*
		 * Note that the vpnd will delay the route sending a bit
		 * (in order to collect the routes from VPN client),
		 * so we might have got the State changed property before
		 * we got ServerRoutes. This means that we must try to set
		 * the routes here because they would be left unset otherwise.
		 */
		if (err == 0)
			set_routes(data->provider,
						CONNMAN_PROVIDER_ROUTE_SERVER);
	} else if (g_str_equal(key, "UserRoutes")) {
		err = routes_changed(&value, data->user_routes);
		if (err == 0)
			set_routes(data->provider,
						CONNMAN_PROVIDER_ROUTE_USER);
	} else if (g_str_equal(key, "Nameservers")) {
		if (extract_nameservers(&value, data) == 0 &&
						data->nameservers)
			connman_provider_set_nameservers(data->provider,
							data->nameservers);
	} else if (g_str_equal(key, "Domain")) {
		dbus_message_iter_get_basic(&value, &str);
		g_free(data->domain);
		data->domain = g_strdup(str);
		connman_provider_set_domain(data->provider, data->domain);
	} else if (g_str_equal(key, "SplitRouting")) {
		dbus_bool_t split_routing;
		dbus_message_iter_get_basic(&value, &split_routing);
		change_provider_split_routing(data->provider, split_routing);
	}

	if (ip_set && err == 0) {
		err = connman_provider_set_ipaddress(data->provider, data->ip);
		if (err < 0)
			DBG("setting provider IP address failed (%s/%d)",
				strerror(-err), -err);
	}

	return TRUE;
}

static int vpn_find_online_transport_cb(struct connman_service *service,
							void *user_data)
{
	if (connman_service_get_type(service) != CONNMAN_SERVICE_TYPE_VPN) {
		switch (connman_service_get_state(service)) {
		case CONNMAN_SERVICE_STATE_ONLINE:
			*((struct connman_service**)user_data) = service;
			return 1;
		default:
			break;
		}
	}

	return 0;
}

static struct connman_service *vpn_find_online_transport()
{
	struct connman_service *service = NULL;

	connman_service_iterate_services(vpn_find_online_transport_cb,
								&service);
	return service;
}

static bool vpn_is_valid_transport(struct connman_service *transport)
{
	if (transport) {
		struct connman_service *online;

		switch (connman_service_get_state(transport)) {
		case CONNMAN_SERVICE_STATE_READY:
			online = vpn_find_online_transport();

			/* Stay connected if there are no online services */
			if (!online)
				return true;

			DBG("%s is ready, %s is online, disconnecting",
				connman_service_get_identifier(transport),
				connman_service_get_identifier(online));
			break;

		case CONNMAN_SERVICE_STATE_ONLINE:
			online = vpn_find_online_transport();

			/* Check if our transport is still the default */
			if (online == transport)
				return true;

			DBG("%s is replaced by %s as default, disconnecting",
				connman_service_get_identifier(transport),
				connman_service_get_identifier(online));
			break;

		default:
			break;
		}
	} else {
		DBG("transport gone");
	}

	return false;
}

static void vpn_disconnect_check_provider(struct connection_data *data)
{
	if (provider_is_connected(data)) {
		/* With NULL service ident NULL is returned immediately */
		struct connman_service *service =
			connman_service_lookup_from_identifier
						(data->service_ident);

		if (!vpn_is_valid_transport(service)) {
			connman_provider_disconnect(data->provider);
		}

		/* VPN moved to be split routed, default route is not set */
		if (connman_provider_is_split_routing(data->provider))
			data->default_route_set = false;
	}
}

static void vpn_disconnect_check()
{
	GHashTableIter iter;
	gpointer value;

	DBG("");
	g_hash_table_iter_init(&iter, vpn_connections);
	while (g_hash_table_iter_next(&iter, NULL, &value))
		vpn_disconnect_check_provider(value);
}

static void vpn_service_add(struct connman_service *service, const char *name)
{
	vpn_disconnect_check();
}

static void vpn_service_list_changed(struct connman_service *service)
{
	vpn_disconnect_check();
}

static void vpn_service_state_changed(struct connman_service *service,
					enum connman_service_state state)
{
	vpn_disconnect_check();
}

static const struct connman_notifier vpn_notifier = {
	.name                   = "vpn",
	.priority               = CONNMAN_NOTIFIER_PRIORITY_DEFAULT,
	.default_changed        = vpn_service_list_changed,
	.service_add            = vpn_service_add,
	.service_remove         = vpn_service_list_changed,
	.service_state_changed  = vpn_service_state_changed
};

static int vpn_init(void)
{
	int err;

	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	watch = g_dbus_add_service_watch(connection, VPN_SERVICE,
			vpnd_created, vpnd_removed, &provider_driver, NULL);

	added_watch = g_dbus_add_signal_watch(connection, VPN_SERVICE, NULL,
					VPN_MANAGER_INTERFACE,
					CONNECTION_ADDED, connection_added,
					&provider_driver, NULL);

	removed_watch = g_dbus_add_signal_watch(connection, VPN_SERVICE, NULL,
					VPN_MANAGER_INTERFACE,
					CONNECTION_REMOVED, connection_removed,
					NULL, NULL);

	property_watch = g_dbus_add_signal_watch(connection, VPN_SERVICE, NULL,
					VPN_CONNECTION_INTERFACE,
					PROPERTY_CHANGED, property_changed,
					NULL, NULL);

	if (added_watch == 0 || removed_watch == 0 || property_watch == 0) {
		err = -EIO;
		goto remove;
	}

	err = connman_provider_driver_register(&provider_driver);
	if (err == 0) {
		vpn_connections = g_hash_table_new_full(g_str_hash,
						g_str_equal,
						g_free, connection_destroy);

		vpnd_created(connection, &provider_driver);
	}

	connman_notifier_register(&vpn_notifier);
	return err;

remove:
	g_dbus_remove_watch(connection, watch);
	g_dbus_remove_watch(connection, added_watch);
	g_dbus_remove_watch(connection, removed_watch);
	g_dbus_remove_watch(connection, property_watch);

	dbus_connection_unref(connection);

	return err;
}

static void vpn_exit(void)
{
	g_dbus_remove_watch(connection, watch);
	g_dbus_remove_watch(connection, added_watch);
	g_dbus_remove_watch(connection, removed_watch);
	g_dbus_remove_watch(connection, property_watch);

	connman_notifier_unregister(&vpn_notifier);
	connman_provider_driver_unregister(&provider_driver);

	if (vpn_connections)
		g_hash_table_destroy(vpn_connections);

	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(vpn, "VPN plugin", VERSION,
		CONNMAN_PLUGIN_PRIORITY_DEFAULT, vpn_init, vpn_exit)
