/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* nm-openvpn-service - openvpn integration with NetworkManager
 *
 * Copyright (C) 2005 - 2008 Tim Niemueller <tim@niemueller.de>
 * Copyright (C) 2005 - 2010 Dan Williams <dcbw@redhat.com>
 *
 * 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.
 *
 * $Id: nm-openvpn-service.c 4232 2008-10-29 09:13:40Z tambeti $
 *
 */

#include "config.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <pwd.h>
#include <grp.h>
#include "glib-unix.h"

#include "nm-default.h"

#include <nm-vpn-service-plugin.h>

#include "nm-openvpn-service.h"
#include "nm-utils.h"
#include "utils.h"
#include "nm-macros-internal.h"

#if !defined(DIST_VERSION)
# define DIST_VERSION VERSION
#endif

static struct {
	gboolean debug;
	GSList *pids_pending_list;
} gl/*obal*/;

#define NM_OPENVPN_HELPER_PATH LIBEXECDIR"/nm-openvpn-service-openvpn-helper"

G_DEFINE_TYPE (NMOpenvpnPlugin, nm_openvpn_plugin, NM_TYPE_VPN_SERVICE_PLUGIN)

#define NM_OPENVPN_PLUGIN_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_OPENVPN_PLUGIN, NMOpenvpnPluginPrivate))

typedef struct {
	char *default_username;
	char *username;
	char *password;
	char *priv_key_pass;
	char *proxy_username;
	char *proxy_password;
	char *pending_auth;
	GIOChannel *socket_channel;
	guint socket_channel_eventid;
} NMOpenvpnPluginIOData;

typedef struct {
	GPid pid;
	guint connect_timer;
	guint connect_count;
	NMOpenvpnPluginIOData *io_data;
	gboolean interactive;
	char *mgt_path;
} NMOpenvpnPluginPrivate;

typedef struct {
	const char *name;
	GType type;
	gint int_min;
	gint int_max;
	gboolean address;
} ValidProperty;

typedef struct {
	GPid pid;
	guint watch_id;
	guint kill_id;
	NMOpenvpnPlugin *plugin;
} PidsPendingData;

static ValidProperty valid_properties[] = {
	{ NM_OPENVPN_KEY_AUTH,                 G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CA,                   G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CERT,                 G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CIPHER,               G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_KEYSIZE,              G_TYPE_INT, 1, 65535, FALSE },
	{ NM_OPENVPN_KEY_COMP_LZO,             G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CONNECTION_TYPE,      G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_FLOAT,                G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_FRAGMENT_SIZE,        G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_KEY,                  G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_LOCAL_IP,             G_TYPE_STRING, 0, 0, TRUE },
	{ NM_OPENVPN_KEY_MSSFIX,               G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_PING,                 G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_PING_EXIT,            G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_PING_RESTART,         G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_PROTO_TCP,            G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_PORT,                 G_TYPE_INT, 1, 65535, FALSE },
	{ NM_OPENVPN_KEY_PROXY_TYPE,           G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_PROXY_SERVER,         G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_PROXY_PORT,           G_TYPE_INT, 1, 65535, FALSE },
	{ NM_OPENVPN_KEY_PROXY_RETRY,          G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_HTTP_PROXY_USERNAME,  G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_REMOTE,               G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_REMOTE_RANDOM,        G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_REMOTE_IP,            G_TYPE_STRING, 0, 0, TRUE },
	{ NM_OPENVPN_KEY_RENEG_SECONDS,        G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_STATIC_KEY,           G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_STATIC_KEY_DIRECTION, G_TYPE_INT, 0, 1, FALSE },
	{ NM_OPENVPN_KEY_TA,                   G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_TA_DIR,               G_TYPE_INT, 0, 1, FALSE },
	{ NM_OPENVPN_KEY_TAP_DEV,              G_TYPE_BOOLEAN, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_DEV,                  G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_DEV_TYPE,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_TUN_IPV6,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_TLS_REMOTE,           G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_REMOTE_CERT_TLS,      G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_NS_CERT_TYPE,         G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_TUNNEL_MTU,           G_TYPE_INT, 0, G_MAXINT, FALSE },
	{ NM_OPENVPN_KEY_USERNAME,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_PASSWORD"-flags",     G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CERTPASS"-flags",     G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_NOSECRET,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD"-flags", G_TYPE_STRING, 0, 0, FALSE },
	{ NULL,                                G_TYPE_NONE, FALSE }
};

static ValidProperty valid_secrets[] = {
	{ NM_OPENVPN_KEY_PASSWORD,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_CERTPASS,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_NOSECRET,             G_TYPE_STRING, 0, 0, FALSE },
	{ NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD,  G_TYPE_STRING, 0, 0, FALSE },
	{ NULL,                                G_TYPE_NONE, FALSE }
};

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

#define _LOG(log_always, level, ...) \
	G_STMT_START { \
		if ((log_always) || _LOG_d_enabled ()) { \
			g_log (G_LOG_DOMAIN, level, __VA_ARGS__); \
		} \
	} G_STMT_END

static gboolean
_LOG_d_enabled (void)
{
	return gl.debug;
}

#define _LOG_d(...) _LOG(FALSE, G_LOG_LEVEL_INFO, __VA_ARGS__)
#define _LOG_D(...) _LOG(TRUE,  G_LOG_LEVEL_INFO, __VA_ARGS__)
#define _LOG_I(...) _LOG(TRUE,  G_LOG_LEVEL_MESSAGE, __VA_ARGS__)
#define _LOG_W(...) _LOG(TRUE,  G_LOG_LEVEL_WARNING, __VA_ARGS__)

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

static void
pids_pending_data_free (PidsPendingData *pid_data)
{
	nm_clear_g_source (&pid_data->watch_id);
	nm_clear_g_source (&pid_data->kill_id);
	if (pid_data->plugin)
		g_object_remove_weak_pointer ((GObject *) pid_data->plugin, (gpointer *) &pid_data->plugin);
	g_slice_free (PidsPendingData, pid_data);
}

static PidsPendingData *
pids_pending_get (GPid pid)
{
	GSList *iter;

	for (iter = gl.pids_pending_list; iter; iter = iter->next) {
		if (((PidsPendingData *) iter->data)->pid == pid)
			return iter->data;
	}
	g_return_val_if_reached (NULL);
}

static void openvpn_child_terminated (NMOpenvpnPlugin *plugin, GPid pid, gint status);

static void
pids_pending_child_watch_cb (GPid pid, gint status, gpointer user_data)
{
	PidsPendingData *pid_data = user_data;
	NMOpenvpnPlugin *plugin;

	if (WIFEXITED (status)) {
		int exit_status;

		exit_status = WEXITSTATUS (status);
		if (exit_status != 0)
			_LOG_W ("openvpn[%ld] exited with error code %d", (long) pid, exit_status);
		else
			_LOG_I ("openvpn[%ld] exited with success", (long) pid);
	}
	else if (WIFSTOPPED (status))
		_LOG_W ("openvpn[%ld] stopped unexpectedly with signal %d", (long) pid, WSTOPSIG (status));
	else if (WIFSIGNALED (status))
		_LOG_W ("openvpn[%ld] died with signal %d", (long) pid, WTERMSIG (status));
	else
		_LOG_W ("openvpn[%ld] died from an unnatural cause", (long) pid);

	g_return_if_fail (pid_data);
	g_return_if_fail (pid_data->pid == pid);
	g_return_if_fail (g_slist_find (gl.pids_pending_list, pid_data));

	plugin = pid_data->plugin;

	pid_data->watch_id = 0;
	gl.pids_pending_list = g_slist_remove (gl.pids_pending_list , pid_data);
	pids_pending_data_free (pid_data);

	if (plugin)
		openvpn_child_terminated (plugin, pid, status);
}

static void
pids_pending_add (GPid pid, NMOpenvpnPlugin *plugin)
{
	PidsPendingData *pid_data;

	g_return_if_fail (NM_IS_OPENVPN_PLUGIN (plugin));
	g_return_if_fail (pid > 0);

	_LOG_I ("openvpn[%ld] started", (long) pid);

	pid_data = g_slice_new (PidsPendingData);
	pid_data->pid = pid;
	pid_data->kill_id = 0;
	pid_data->watch_id = g_child_watch_add (pid, pids_pending_child_watch_cb, pid_data);
	pid_data->plugin = plugin;
	g_object_add_weak_pointer ((GObject *) plugin, (gpointer *) &pid_data->plugin);

	gl.pids_pending_list = g_slist_prepend (gl.pids_pending_list, pid_data);
}

static gboolean
pids_pending_ensure_killed (gpointer user_data)
{
	PidsPendingData *pid_data = user_data;

	g_return_val_if_fail (pid_data && pid_data == pids_pending_get (pid_data->pid), FALSE);

	_LOG_I ("openvpn[%ld]: send SIGKILL", (long) pid_data->pid);

	pid_data->kill_id = 0;
	kill (pid_data->pid, SIGKILL);
	return FALSE;
}

static void
pids_pending_send_sigterm (GPid pid)
{
	PidsPendingData *pid_data;

	pid_data = pids_pending_get (pid);
	g_return_if_fail (pid_data);

	_LOG_I ("openvpn[%ld]: send SIGTERM", (long) pid);

	kill (pid, SIGTERM);
	pid_data->kill_id = g_timeout_add (2000, pids_pending_ensure_killed, pid_data);
}

static void
pids_pending_wait_for_processes (GMainLoop *main_loop)
{
	if (gl.pids_pending_list) {
		_LOG_I ("wait for %u openvpn processes to terminate...", g_slist_length (gl.pids_pending_list));

		do {
			g_main_context_iteration (g_main_loop_get_context (main_loop), TRUE);
		} while (gl.pids_pending_list);
	}
}

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

static gboolean
validate_address (const char *address)
{
	const char *p = address;

	if (!address || !strlen (address))
		return FALSE;

	/* Ensure it's a valid DNS name or IP address */
	while (*p) {
		if (!isalnum (*p) && (*p != '-') && (*p != '.'))
			return FALSE;
		p++;
	}
	return TRUE;
}

typedef struct ValidateInfo {
	ValidProperty *table;
	GError **error;
	gboolean have_items;
} ValidateInfo;

static void
validate_one_property (const char *key, const char *value, gpointer user_data)
{
	ValidateInfo *info = (ValidateInfo *) user_data;
	int i;

	if (*(info->error))
		return;

	info->have_items = TRUE;

	/* 'name' is the setting name; always allowed but unused */
	if (!strcmp (key, NM_SETTING_NAME))
		return;

	for (i = 0; info->table[i].name; i++) {
		ValidProperty prop = info->table[i];
		long int tmp;

		if (strcmp (prop.name, key))
			continue;

		switch (prop.type) {
		case G_TYPE_STRING:
			if (!prop.address || validate_address (value))
				return; /* valid */

			g_set_error (info->error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("invalid address '%s'"),
			             key);
			break;
		case G_TYPE_INT:
			errno = 0;
			tmp = strtol (value, NULL, 10);
			if (errno == 0 && tmp >= prop.int_min && tmp <= prop.int_max)
				return; /* valid */

			g_set_error (info->error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("invalid integer property '%s' or out of range [%d -> %d]"),
			             key, prop.int_min, prop.int_max);
			break;
		case G_TYPE_BOOLEAN:
			if (!strcmp (value, "yes") || !strcmp (value, "no"))
				return; /* valid */

			g_set_error (info->error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             /* Translators: keep "yes" and "no" untranslated! */
			             _("invalid boolean property '%s' (not yes or no)"),
			             key);
			break;
		default:
			g_set_error (info->error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("unhandled property '%s' type %s"),
			             key, g_type_name (prop.type));
			break;
		}
	}

	/* Did not find the property from valid_properties or the type did not match */
	if (!info->table[i].name) {
		g_set_error (info->error,
		             NM_VPN_PLUGIN_ERROR,
		             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		             _("property '%s' invalid or not supported"),
		             key);
	}
}

static gboolean
nm_openvpn_properties_validate (NMSettingVpn *s_vpn, GError **error)
{
	GError *validate_error = NULL;
	ValidateInfo info = { &valid_properties[0], &validate_error, FALSE };

	nm_setting_vpn_foreach_data_item (s_vpn, validate_one_property, &info);
	if (!info.have_items) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		                     _("No VPN configuration options."));
		return FALSE;
	}

	if (validate_error) {
		*error = validate_error;
		return FALSE;
	}
	return TRUE;
}

static gboolean
nm_openvpn_secrets_validate (NMSettingVpn *s_vpn, GError **error)
{
	GError *validate_error = NULL;
	ValidateInfo info = { &valid_secrets[0], &validate_error, FALSE };

	nm_setting_vpn_foreach_secret (s_vpn, validate_one_property, &info);
	if (validate_error) {
		g_propagate_error (error, validate_error);
		return FALSE;
	}
	return TRUE;
}

static void
nm_openvpn_disconnect_management_socket (NMOpenvpnPlugin *plugin)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	NMOpenvpnPluginIOData *io_data = priv->io_data;

	/* This should not throw a warning since this can happen in
	   non-password modes */
	if (!io_data)
		return;

	if (io_data->socket_channel_eventid)
		g_source_remove (io_data->socket_channel_eventid);
	if (io_data->socket_channel) {
		g_io_channel_shutdown (io_data->socket_channel, FALSE, NULL);
		g_io_channel_unref (io_data->socket_channel);
	}

	g_free (io_data->username);
	g_free (io_data->proxy_username);
	g_free (io_data->pending_auth);

	if (io_data->password)
		memset (io_data->password, 0, strlen (io_data->password));
	g_free (io_data->password);

	if (io_data->priv_key_pass)
		memset (io_data->priv_key_pass, 0, strlen (io_data->priv_key_pass));
	g_free (io_data->priv_key_pass);

	if (io_data->proxy_password)
		memset (io_data->proxy_password, 0, strlen (io_data->proxy_password));
	g_free (io_data->proxy_password);

	g_free (priv->io_data);
	priv->io_data = NULL;
}

static char *
ovpn_quote_string (const char *unquoted)
{
	char *quoted = NULL, *q;
	char *u = (char *) unquoted;

	g_return_val_if_fail (unquoted != NULL, NULL);

	/* FIXME: use unpaged memory */
	quoted = q = g_malloc0 (strlen (unquoted) * 2);
	while (*u) {
		/* Escape certain characters */
		if (*u == ' ' || *u == '\\' || *u == '"')
			*q++ = '\\';
		*q++ = *u++;
	}

	return quoted;
}

/* sscanf is evil, and since we can't use glib regexp stuff since it's still
 * too new for some distros, do a simple match here.
 */
static char *
get_detail (const char *input, const char *prefix)
{
	char *ret = NULL;
	guint32 i = 0;
	const char *p, *start;

	g_return_val_if_fail (prefix != NULL, NULL);

	if (!g_str_has_prefix (input, prefix))
		return NULL;

	/* Grab characters until the next ' */
	p = start = input + strlen (prefix);
	while (*p) {
		if (*p == '\'') {
			ret = g_malloc0 (i + 1);
			strncpy (ret, start, i);
			break;
		}
		p++, i++;
	}

	return ret;
}

static void
write_user_pass (GIOChannel *channel,
                 const char *authtype,
                 const char *user,
                 const char *pass)
{
	char *quser, *qpass, *buf;

	/* Quote strings passed back to openvpn */
	quser = ovpn_quote_string (user);
	qpass = ovpn_quote_string (pass);
	buf = g_strdup_printf ("username \"%s\" \"%s\"\n"
	                       "password \"%s\" \"%s\"\n",
	                       authtype, quser,
	                       authtype, qpass);
	memset (qpass, 0, strlen (qpass));
	g_free (qpass);
	g_free (quser);

	/* Will always write everything in blocking channels (on success) */
	g_io_channel_write_chars (channel, buf, strlen (buf), NULL, NULL);
	g_io_channel_flush (channel, NULL);

	memset (buf, 0, strlen (buf));
	g_free (buf);
}

static gboolean
handle_auth (NMOpenvpnPluginIOData *io_data,
             const char *requested_auth,
             const char **out_message,
             char ***out_hints)
{
	gboolean handled = FALSE;
	guint i = 0;
	char **hints = NULL;

	g_return_val_if_fail (requested_auth != NULL, FALSE);
	g_return_val_if_fail (out_message != NULL, FALSE);
	g_return_val_if_fail (out_hints != NULL, FALSE);

	if (strcmp (requested_auth, "Auth") == 0) {
		const char *username = io_data->username;

		/* Fall back to the default username if it wasn't overridden by the user */
		if (!username)
			username = io_data->default_username;

		if (username != NULL && io_data->password != NULL) {
			write_user_pass (io_data->socket_channel,
			                 requested_auth,
			                 username,
			                 io_data->password);
		} else {
			hints = g_new0 (char *, 3);
			if (!username) {
				hints[i++] = NM_OPENVPN_KEY_USERNAME;
				*out_message = _("A username is required.");
			}
			if (!io_data->password) {
				hints[i++] = NM_OPENVPN_KEY_PASSWORD;
				*out_message = _("A password is required.");
			}
			if (!username && !io_data->password)
				*out_message = _("A username and password are required.");
		}
		handled = TRUE;
	} else if (!strcmp (requested_auth, "Private Key")) {
		if (io_data->priv_key_pass) {
			char *qpass, *buf;

			/* Quote strings passed back to openvpn */
			qpass = ovpn_quote_string (io_data->priv_key_pass);
			buf = g_strdup_printf ("password \"%s\" \"%s\"\n", requested_auth, qpass);
			memset (qpass, 0, strlen (qpass));
			g_free (qpass);

			/* Will always write everything in blocking channels (on success) */
			g_io_channel_write_chars (io_data->socket_channel, buf, strlen (buf), NULL, NULL);
			g_io_channel_flush (io_data->socket_channel, NULL);
			g_free (buf);
		} else {
			hints = g_new0 (char *, 2);
			hints[i++] = NM_OPENVPN_KEY_CERTPASS;
			*out_message = _("A private key password is required.");
		}
		handled = TRUE;
	} else if (strcmp (requested_auth, "HTTP Proxy") == 0) {
		if (io_data->proxy_username != NULL && io_data->proxy_password != NULL) {
			write_user_pass (io_data->socket_channel,
			                 requested_auth,
			                 io_data->proxy_username,
			                 io_data->proxy_password);
		} else {
			hints = g_new0 (char *, 3);
			if (!io_data->proxy_username) {
				hints[i++] = NM_OPENVPN_KEY_HTTP_PROXY_USERNAME;
				*out_message = _("An HTTP Proxy username is required.");
			}
			if (!io_data->proxy_password) {
				hints[i++] = NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD;
				*out_message = _("An HTTP Proxy password is required.");
			}
			if (!io_data->proxy_username && !io_data->proxy_password)
				*out_message = _("An HTTP Proxy username and password are required.");
		}
		handled = TRUE;
	}

	*out_hints = hints;
	return handled;
}

static gboolean
handle_management_socket (NMOpenvpnPlugin *plugin,
                          GIOChannel *source,
                          GIOCondition condition,
                          NMVpnPluginFailure *out_failure)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	gboolean again = TRUE;
	char *str = NULL, *auth = NULL;
	const char *message = NULL;
	char **hints = NULL;

	g_assert (out_failure);

	if (!(condition & G_IO_IN))
		return TRUE;

	if (g_io_channel_read_line (source, &str, NULL, NULL, NULL) != G_IO_STATUS_NORMAL)
		return TRUE;

	if (!str[0]) {
		g_free (str);
		return TRUE;
	}

	_LOG_d ("VPN request '%s'", str);

	auth = get_detail (str, ">PASSWORD:Need '");
	if (auth) {
		if (priv->io_data->pending_auth)
			g_free (priv->io_data->pending_auth);
		priv->io_data->pending_auth = auth;

		if (handle_auth (priv->io_data, auth, &message, &hints)) {
			/* Request new secrets if we need any */
			if (message) {
				if (priv->interactive) {
					gs_free char *joined = NULL;

					_LOG_d ("Requesting new secrets: '%s', %s%s%s", message,
					        NM_PRINT_FMT_QUOTED (hints, "(", (joined = g_strjoinv (",", (char **) hints)), ")", "no hints"));

					nm_vpn_service_plugin_secrets_required ((NMVpnServicePlugin *) plugin, message, (const char **) hints);
				} else {
					/* Interactive not allowed, can't ask for more secrets */
					_LOG_W ("More secrets required but cannot ask interactively");
					*out_failure = NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED;
					again = FALSE;
				}
			}
			if (hints)
				g_free (hints);  /* elements are 'const' */
		} else {
			_LOG_W ("Unhandled management socket request '%s'", auth);
			*out_failure = NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED;
			again = FALSE;
		}
		goto out;
	}

	auth = get_detail (str, ">PASSWORD:Verification Failed: '");
	if (auth) {
		gboolean fail = TRUE;

		if (!strcmp (auth, "Auth")) {
			_LOG_W ("Password verification failed");
			if (priv->interactive) {
				/* Clear existing password in interactive mode, openvpn
				 * will request a new one after restarting.
				 */
				if (priv->io_data->password)
					memset (priv->io_data->password, 0, strlen (priv->io_data->password));
				g_clear_pointer (&priv->io_data->password, g_free);
				fail = FALSE;
			}
		} else if (!strcmp (auth, "Private Key"))
			_LOG_W ("Private key verification failed");
		else
			_LOG_W ("Unknown verification failed: %s", auth);

		if (fail) {
			*out_failure = NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED;
			again = FALSE;
		}

		g_free (auth);
	}

out:
	g_free (str);
	return again;
}

static gboolean
nm_openvpn_socket_data_cb (GIOChannel *source, GIOCondition condition, gpointer user_data)
{
	NMOpenvpnPlugin *plugin = NM_OPENVPN_PLUGIN (user_data);
	NMVpnPluginFailure failure = NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED;

	if (!handle_management_socket (plugin, source, condition, &failure)) {
		nm_vpn_service_plugin_failure ((NMVpnServicePlugin *) plugin, failure);
		return FALSE;
	}

	return TRUE;
}

static gboolean
nm_openvpn_connect_timer_cb (gpointer data)
{
	NMOpenvpnPlugin *plugin = NM_OPENVPN_PLUGIN (data);
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	NMOpenvpnPluginIOData *io_data = priv->io_data;
	struct sockaddr_un remote = { 0 };
	int fd;

	priv->connect_count++;

	/* open socket and start listener */
	fd = socket (AF_UNIX, SOCK_STREAM, 0);
	if (fd < 0) {
		_LOG_W ("Could not create management socket");
		nm_vpn_service_plugin_failure (NM_VPN_SERVICE_PLUGIN (plugin), NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED);
		goto out;
	}

	remote.sun_family = AF_UNIX;
	g_strlcpy (remote.sun_path, priv->mgt_path, sizeof (remote.sun_path));
	if (connect (fd, (struct sockaddr *) &remote, sizeof (remote)) != 0) {
		close (fd);
		if (priv->connect_count <= 30)
			return G_SOURCE_CONTINUE;

		priv->connect_timer = 0;

		_LOG_W ("Could not open management socket");
		nm_vpn_service_plugin_failure (NM_VPN_SERVICE_PLUGIN (plugin), NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED);
	} else {
		io_data->socket_channel = g_io_channel_unix_new (fd);
		g_io_channel_set_encoding (io_data->socket_channel, NULL, NULL);
		io_data->socket_channel_eventid = g_io_add_watch (io_data->socket_channel,
		                                                  G_IO_IN,
		                                                  nm_openvpn_socket_data_cb,
		                                                  plugin);
	}

out:
	priv->connect_timer = 0;
	return G_SOURCE_REMOVE;
}

static void
nm_openvpn_schedule_connect_timer (NMOpenvpnPlugin *plugin)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);

	if (priv->connect_timer == 0)
		priv->connect_timer = g_timeout_add (200, nm_openvpn_connect_timer_cb, plugin);
}

static void
openvpn_child_terminated (NMOpenvpnPlugin *plugin, GPid pid, gint status)
{
	NMOpenvpnPluginPrivate *priv;
	NMVpnPluginFailure failure = NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED;
	gboolean good_exit = FALSE;

	g_return_if_fail (NM_IS_OPENVPN_PLUGIN (plugin));

	priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	/* Reap child if needed. */
	if (priv->pid != pid) {
		/* the dead child is not the currently active process. Nothing to do, we just
		 * reaped the PID. */
		return;
	}

	priv->pid = 0;

	/* OpenVPN doesn't supply useful exit codes :( */
	if (WIFEXITED (status) && WEXITSTATUS (status) == 0)
		good_exit = TRUE;

	/* Try to get the last bits of data from openvpn */
	if (priv->io_data && priv->io_data->socket_channel) {
		GIOChannel *channel = priv->io_data->socket_channel;
		GIOCondition condition;

		while ((condition = g_io_channel_get_buffer_condition (channel)) & G_IO_IN) {
			if (!handle_management_socket (plugin, channel, condition, &failure)) {
				good_exit = FALSE;
				break;
			}
		}
	}

	if (good_exit)
		nm_vpn_service_plugin_disconnect ((NMVpnServicePlugin *) plugin, NULL);
	else
		nm_vpn_service_plugin_failure ((NMVpnServicePlugin *) plugin, failure);
}

static gboolean
validate_auth (const char *auth)
{
	if (auth) {
		if (   !strcmp (auth, NM_OPENVPN_AUTH_NONE)
		    || !strcmp (auth, NM_OPENVPN_AUTH_RSA_MD4)
		    || !strcmp (auth, NM_OPENVPN_AUTH_MD5)
		    || !strcmp (auth, NM_OPENVPN_AUTH_SHA1)
		    || !strcmp (auth, NM_OPENVPN_AUTH_SHA224)
		    || !strcmp (auth, NM_OPENVPN_AUTH_SHA256)
		    || !strcmp (auth, NM_OPENVPN_AUTH_SHA384)
		    || !strcmp (auth, NM_OPENVPN_AUTH_SHA512)
		    || !strcmp (auth, NM_OPENVPN_AUTH_RIPEMD160))
			return TRUE;
	}
	return FALSE;
}

static const char *
validate_connection_type (const char *ctype)
{
	if (ctype) {
		if (   !strcmp (ctype, NM_OPENVPN_CONTYPE_TLS)
		    || !strcmp (ctype, NM_OPENVPN_CONTYPE_STATIC_KEY)
		    || !strcmp (ctype, NM_OPENVPN_CONTYPE_PASSWORD)
		    || !strcmp (ctype, NM_OPENVPN_CONTYPE_PASSWORD_TLS))
			return ctype;
	}
	return NULL;
}

static gboolean
connection_type_is_tls_mode (const char *connection_type)
{
	return strcmp (connection_type, NM_OPENVPN_CONTYPE_TLS) == 0
	    || strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD) == 0
	    || strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD_TLS) == 0;
}

static const char *
nm_find_openvpn (void)
{
	static const char *openvpn_binary_paths[] = {
		"/usr/sbin/openvpn",
		"/sbin/openvpn",
		"/usr/local/sbin/openvpn",
		NULL
	};
	const char  **openvpn_binary = openvpn_binary_paths;

	while (*openvpn_binary != NULL) {
		if (g_file_test (*openvpn_binary, G_FILE_TEST_EXISTS))
			break;
		openvpn_binary++;
	}

	return *openvpn_binary;
}

static void
free_openvpn_args (GPtrArray *args)
{
	g_ptr_array_foreach (args, (GFunc) g_free, NULL);
	g_ptr_array_free (args, TRUE);
}

static void
add_openvpn_arg (GPtrArray *args, const char *arg)
{
	g_return_if_fail (args != NULL);
	g_return_if_fail (arg != NULL);

	g_ptr_array_add (args, g_strdup (arg));
}

static const char *
add_openvpn_arg_utf8safe (GPtrArray *args, const char *arg)
{
	char *arg_unescaped;

	g_return_val_if_fail (args, NULL);
	g_return_val_if_fail (arg, NULL);

	arg_unescaped = nmv_utils_str_utf8safe_unescape (arg);
	g_ptr_array_add (args, arg_unescaped);
	return arg_unescaped;
}

static gboolean
add_openvpn_arg_int (GPtrArray *args, const char *arg)
{
	long int tmp_int;

	g_return_val_if_fail (args != NULL, FALSE);
	g_return_val_if_fail (arg != NULL, FALSE);

	/* Convert -> int and back to string for security's sake since
	 * strtol() ignores some leading and trailing characters.
	 */
	errno = 0;
	tmp_int = strtol (arg, NULL, 10);
	if (errno != 0)
		return FALSE;
	g_ptr_array_add (args, (gpointer) g_strdup_printf ("%d", (guint32) tmp_int));
	return TRUE;
}

static void
add_cert_args (GPtrArray *args, NMSettingVpn *s_vpn)
{
	const char *ca, *cert, *key;
	gs_free char *ca_free = NULL, *cert_free = NULL, *key_free = NULL;

	g_return_if_fail (args != NULL);
	g_return_if_fail (s_vpn != NULL);

	ca   = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CA);
	cert = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CERT);
	key  = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEY);

	ca   = nmv_utils_str_utf8safe_unescape_c (ca,   &ca_free);
	cert = nmv_utils_str_utf8safe_unescape_c (cert, &cert_free);
	key  = nmv_utils_str_utf8safe_unescape_c (key,  &key_free);

	if (   ca && strlen (ca)
	    && cert && strlen (cert)
	    && key && strlen (key)
	    && !strcmp (ca, cert)
	    && !strcmp (ca, key)) {
		add_openvpn_arg (args, "--pkcs12");
		add_openvpn_arg (args, ca);
	} else {
		if (ca && strlen (ca)) {
			add_openvpn_arg (args, "--ca");
			add_openvpn_arg (args, ca);
		}

		if (cert && strlen (cert)) {
			add_openvpn_arg (args, "--cert");
			add_openvpn_arg (args, cert);
		}

		if (key && strlen (key)) {
			add_openvpn_arg (args, "--key");
			add_openvpn_arg (args, key);
		}
	}
}

static void
update_io_data_from_vpn_setting (NMOpenvpnPluginIOData *io_data,
                                 NMSettingVpn *s_vpn,
                                 const char *default_username)
{
	const char *tmp;

	if (default_username) {
		g_free (io_data->default_username);
		io_data->default_username = g_strdup (default_username);
	}

	g_free (io_data->username);
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_USERNAME);
	io_data->username = tmp ? g_strdup (tmp) : NULL;

	if (io_data->password) {
		memset (io_data->password, 0, strlen (io_data->password));
		g_free (io_data->password);
	}
	tmp = nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_PASSWORD);
	io_data->password = tmp ? g_strdup (tmp) : NULL;

	if (io_data->priv_key_pass) {
		memset (io_data->priv_key_pass, 0, strlen (io_data->priv_key_pass));
		g_free (io_data->priv_key_pass);
	}
	tmp = nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_CERTPASS);
	io_data->priv_key_pass = tmp ? g_strdup (tmp) : NULL;

	g_free (io_data->proxy_username);
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_USERNAME);
	io_data->proxy_username = tmp ? g_strdup (tmp) : NULL;

	if (io_data->proxy_password) {
		memset (io_data->proxy_password, 0, strlen (io_data->proxy_password));
		g_free (io_data->proxy_password);
	}
	tmp = nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD);
	io_data->proxy_password = tmp ? g_strdup (tmp) : NULL;
}

#define MAX_GROUPS 128
static gboolean
is_dir_writable (const char *dir, const char *user)
{
	struct stat sb;
	struct passwd *pw;

	if (stat (dir, &sb) == -1)
		return FALSE;
	pw = getpwnam (user);
	if (!pw)
		return FALSE;

	if (pw->pw_uid == 0)
		return TRUE;

	if (sb.st_mode & S_IWOTH)
		return TRUE;
	else if (sb.st_mode & S_IWGRP) {
		/* Group has write access. Is user in that group? */
		int i, ngroups = MAX_GROUPS;
		gid_t groups[MAX_GROUPS];

		getgrouplist (user, pw->pw_gid, groups, &ngroups);
		for (i = 0; i < ngroups && i < MAX_GROUPS; i++) {
			if (groups[i] == sb.st_gid)
				return TRUE;
		}
	} else if (sb.st_mode & S_IWUSR) {
		/* The owner has write access. Does the user own the file? */
		if (pw->pw_uid == sb.st_uid)
			return TRUE;
	}
	return FALSE;
}

/* Check existence of 'tmp' directory inside @chdir
 * and write access in @chdir and @chdir/tmp for @user.
 */
static gboolean
check_chroot_dir_usability (const char *chdir, const char *user)
{
	char *tmp_dir;
	gboolean b1, b2;

	tmp_dir = g_strdup_printf ("%s/tmp", chdir);
	if (!g_file_test (tmp_dir, G_FILE_TEST_IS_DIR)) {
		g_free (tmp_dir);
		return FALSE;
	}

	b1 = is_dir_writable (chdir, user);
	b2 = is_dir_writable (tmp_dir, user);
	g_free (tmp_dir);
	return b1 && b2;
}

static gboolean
nm_openvpn_start_openvpn_binary (NMOpenvpnPlugin *plugin,
                                 NMSettingVpn *s_vpn,
                                 const char *default_username,
                                 const char *uuid,
                                 GError **error)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	const char *openvpn_binary, *auth, *connection_type, *tmp, *tmp2, *tmp3, *tmp4;
	GPtrArray *args;
	GPid pid;
	gboolean dev_type_is_tap;
	char *stmp;
	const char *defport, *proto_tcp;
	const char *nm_openvpn_user, *nm_openvpn_group, *nm_openvpn_chroot;
	gchar *bus_name;

	/* Find openvpn */
	openvpn_binary = nm_find_openvpn ();
	if (!openvpn_binary) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		                     _("Could not find the openvpn binary."));
		return FALSE;
	}

	auth = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_AUTH);
	if (auth) {
		if (!validate_auth(auth)) {
			g_set_error_literal (error,
			                     NM_VPN_PLUGIN_ERROR,
			                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			                     _("Invalid HMAC auth."));
			return FALSE;
		}
	}

	connection_type = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CONNECTION_TYPE);
	if (!validate_connection_type (connection_type)) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		                     _("Invalid connection type."));
		return FALSE;
	}

	args = g_ptr_array_new ();
	add_openvpn_arg (args, openvpn_binary);

	defport = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PORT);
	if (defport && !defport[0])
		defport = NULL;

	proto_tcp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROTO_TCP);
	if (proto_tcp && !proto_tcp[0])
		proto_tcp = NULL;

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE);
	if (tmp && *tmp) {
		char *tok, *port, *proto;
		char *tmp_dup = strdup (tmp);
		while ((tok = strsep (&tmp_dup, " \t,")) != NULL) {
			if (*tok) {
				port = strchr (tok, ':');
				proto = port ? strchr (port + 1, ':') : NULL;
				if (port)
					*port++ = '\0';
				if (proto)
					*proto++ = '\0';

				add_openvpn_arg (args, "--remote");
				add_openvpn_arg (args, tok);
				if (port) {
					if (!add_openvpn_arg_int (args, port)) {
						g_set_error (error,
						             NM_VPN_PLUGIN_ERROR,
						             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
						             _("Invalid port number '%s'."), port);
						free_openvpn_args (args);
						g_free (tmp_dup);
						return FALSE;
					}
				} else if (defport) {
					if (!add_openvpn_arg_int (args, defport)) {
						g_set_error (error,
						             NM_VPN_PLUGIN_ERROR,
						             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
						             _("Invalid port number '%s'."),
						             defport);
						free_openvpn_args (args);
						return FALSE;
					}
				} else
					add_openvpn_arg (args, "1194"); /* default IANA port */

				if (proto) {
					if (!strcmp (proto, "udp"))
							add_openvpn_arg (args, proto);
					else if (!strcmp (proto, "tcp"))
							add_openvpn_arg (args, "tcp-client");
					else {
						g_set_error (error,
						             NM_VPN_PLUGIN_ERROR,
						             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
						             _("Invalid proto '%s'."), proto);
						free_openvpn_args (args);
						g_free (tmp_dup);
						return FALSE;
					}
				} else if (proto_tcp && !strcmp (proto_tcp, "yes"))
					add_openvpn_arg (args, "tcp-client");
				else
					add_openvpn_arg (args, "udp");
			}
		}
		g_free (tmp_dup);
	}

	/* Remote random */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_RANDOM);
	if (tmp && !strcmp (tmp, "yes"))
		add_openvpn_arg (args, "--remote-random");

	/* tun-ipv6 */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TUN_IPV6);
	if (tmp && !strcmp (tmp, "yes"))
		add_openvpn_arg (args, "--tun-ipv6");

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_TYPE);
	tmp2 = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_SERVER);
	tmp3 = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_PORT);
	tmp4 = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_RETRY);
	if (tmp && strlen (tmp) && tmp2 && strlen (tmp2)) {
		if (!strcmp (tmp, "http")) {
			add_openvpn_arg (args, "--http-proxy");
			add_openvpn_arg (args, tmp2);
			add_openvpn_arg (args, tmp3 ? tmp3 : "8080");
			add_openvpn_arg (args, "auto");  /* Automatic proxy auth method detection */
			if (tmp4)
				add_openvpn_arg (args, "--http-proxy-retry");
		} else if (!strcmp (tmp, "socks")) {
			add_openvpn_arg (args, "--socks-proxy");
			add_openvpn_arg (args, tmp2);
			add_openvpn_arg (args, tmp3 ? tmp3 : "1080");
			if (tmp4)
				add_openvpn_arg (args, "--socks-proxy-retry");
		} else {
			g_set_error (error,
				         NM_VPN_PLUGIN_ERROR,
				         NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
				         _("Invalid proxy type '%s'."),
				         tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_COMP_LZO);
	if (tmp && !strcmp (tmp, "yes"))
		add_openvpn_arg (args, "--comp-lzo");

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_FLOAT);
	if (tmp && !strcmp (tmp, "yes"))
		add_openvpn_arg (args, "--float");

	/* ping, ping-exit, ping-restart */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PING);
	if (tmp) {
		add_openvpn_arg (args, "--ping");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid ping duration '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PING_EXIT);
	if (tmp) {
		add_openvpn_arg (args, "--ping-exit");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid ping-exit duration '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PING_RESTART);
	if (tmp) {
		add_openvpn_arg (args, "--ping-restart");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid ping-restart duration '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	add_openvpn_arg (args, "--nobind");

	/* Device and device type, defaults to tun */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_DEV);
	tmp2 = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_DEV_TYPE);
	tmp3 = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TAP_DEV);
	add_openvpn_arg (args, "--dev");
	if (tmp) {
		const char *tmp_unescaped;

		tmp_unescaped = add_openvpn_arg_utf8safe (args, tmp);
		dev_type_is_tap = g_str_has_prefix (tmp_unescaped, "tap");
	} else if (tmp2) {
		add_openvpn_arg (args, tmp2);
		dev_type_is_tap = FALSE; /* will be reset below (avoid maybe-uninitialized warning) */
	} else if (tmp3 && !strcmp (tmp3, "yes")) {
		add_openvpn_arg (args, "tap");
		dev_type_is_tap = TRUE;
	} else {
		add_openvpn_arg (args, "tun");
		dev_type_is_tap = FALSE;
	}

	/* Add '--dev-type' if the type was explicitly set */
	if (tmp2) {
		add_openvpn_arg (args, "--dev-type");
		add_openvpn_arg (args, tmp2);
		dev_type_is_tap = (strcmp (tmp2, "tap") == 0);
	}

	/* Cipher */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CIPHER);
	if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--cipher");
		add_openvpn_arg (args, tmp);
	}

	/* Keysize */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEYSIZE);
	if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--keysize");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid keysize '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	/* Auth */
	if (auth) {
		add_openvpn_arg (args, "--auth");
		add_openvpn_arg (args, auth);
	}
	add_openvpn_arg (args, "--auth-nocache");

	/* TA */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TA);
	if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--tls-auth");
		add_openvpn_arg_utf8safe (args, tmp);

		tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TA_DIR);
		if (tmp && strlen (tmp))
			add_openvpn_arg (args, tmp);
	}

	/* tls-remote */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_REMOTE);
	if (tmp && strlen (tmp)) {
                add_openvpn_arg (args, "--tls-remote");
                add_openvpn_arg (args, tmp);
	}

	/* remote-cert-tls */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_CERT_TLS);
	if (tmp && strlen (tmp)) {
                add_openvpn_arg (args, "--remote-cert-tls");
                add_openvpn_arg (args, tmp);
	}

	/* ns-cert-type */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_NS_CERT_TYPE);
	if (tmp && tmp[0]) {
                add_openvpn_arg (args, "--ns-cert-type");
                add_openvpn_arg (args, tmp);
	}

	/* Reneg seconds */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_RENEG_SECONDS);
	if (!connection_type_is_tls_mode (connection_type)) {
		/* Ignore --reneg-sec option if we are not in TLS mode (as enabled
		 * by --client below). openvpn will error out otherwise, see bgo#749050. */
	} else if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--reneg-sec");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid reneg seconds '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	} else {
		/* Either the server and client must agree on the renegotiation
		 * interval, or it should be disabled on one side to prevent
		 * too-frequent renegotiations, which make two-factor auth quite
		 * painful.
		 */
		add_openvpn_arg (args, "--reneg-sec");
		add_openvpn_arg (args, "0");
	}

	if (gl.debug) {
		add_openvpn_arg (args, "--verb");
		add_openvpn_arg (args, "10");
	} else {
		/* Syslog */
		add_openvpn_arg (args, "--syslog");
		add_openvpn_arg (args, "nm-openvpn");
	}

	/* TUN MTU size */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TUNNEL_MTU);
	if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--tun-mtu");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid TUN MTU size '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	/* fragment size */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_FRAGMENT_SIZE);
	if (tmp && strlen (tmp)) {
		add_openvpn_arg (args, "--fragment");
		if (!add_openvpn_arg_int (args, tmp)) {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Invalid fragment size '%s'."),
			             tmp);
			free_openvpn_args (args);
			return FALSE;
		}
	}

	/* mssfix */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_MSSFIX);
	if (tmp && !strcmp (tmp, "yes")) {
		add_openvpn_arg (args, "--mssfix");
	}

	/* Punch script security in the face; this option was added to OpenVPN 2.1-rc9
	 * and defaults to disallowing any scripts, a behavior change from previous
	 * versions.
	 */
	add_openvpn_arg (args, "--script-security");
	add_openvpn_arg (args, "2");

	/* Up script, called when connection has been established or has been restarted */
	add_openvpn_arg (args, "--up");
	g_object_get (plugin, NM_VPN_SERVICE_PLUGIN_DBUS_SERVICE_NAME, &bus_name, NULL);
	stmp = g_strdup_printf ("%s%s --bus-name %s %s --", NM_OPENVPN_HELPER_PATH, gl.debug ? " --helper-debug" : "",
	                        bus_name, dev_type_is_tap ? "--tap" : "--tun");
	add_openvpn_arg (args, stmp);
	g_free (stmp);
	add_openvpn_arg (args, "--up-restart");

	/* Keep key and tun if restart is needed */
	add_openvpn_arg (args, "--persist-key");
	add_openvpn_arg (args, "--persist-tun");

	/* Management socket for localhost access to supply username and password */
	add_openvpn_arg (args, "--management");
	g_warn_if_fail (priv->mgt_path == NULL);
	priv->mgt_path = g_strdup_printf (LOCALSTATEDIR "/run/NetworkManager/nm-openvpn-%s", uuid);
	add_openvpn_arg (args, priv->mgt_path);
	add_openvpn_arg (args, "unix");
	add_openvpn_arg (args, "--management-client-user");
	add_openvpn_arg (args, "root");
	add_openvpn_arg (args, "--management-client-group");
	add_openvpn_arg (args, "root");

	/* Query on the management socket for user/pass */
	add_openvpn_arg (args, "--management-query-passwords");
	add_openvpn_arg (args, "--auth-retry");
	add_openvpn_arg (args, "interact");

	/* do not let openvpn setup routes or addresses, NM will handle it */
	add_openvpn_arg (args, "--route-noexec");
	add_openvpn_arg (args, "--ifconfig-noexec");

	/* Now append configuration options which are dependent on the configuration type */
	if (!strcmp (connection_type, NM_OPENVPN_CONTYPE_TLS)) {
		add_openvpn_arg (args, "--client");
		add_cert_args (args, s_vpn);
	} else if (!strcmp (connection_type, NM_OPENVPN_CONTYPE_STATIC_KEY)) {
		tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_STATIC_KEY);
		if (tmp && strlen (tmp)) {
			add_openvpn_arg (args, "--secret");
			add_openvpn_arg_utf8safe (args, tmp);

			tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_STATIC_KEY_DIRECTION);
			if (tmp && strlen (tmp))
				add_openvpn_arg (args, tmp);
		}

		add_openvpn_arg (args, "--ifconfig");

		tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_LOCAL_IP);
		if (!tmp) {
			/* Insufficient data (FIXME: this should really be detected when validating the properties */
			g_set_error_literal (error,
			                     NM_VPN_PLUGIN_ERROR,
			                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			                     _("Missing required local IP address for static key mode."));
			free_openvpn_args (args);
			return FALSE;
		}
		add_openvpn_arg (args, tmp);

		tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_IP);
		if (!tmp) {
			/* Insufficient data (FIXME: this should really be detected when validating the properties */
			g_set_error_literal (error,
			                     NM_VPN_PLUGIN_ERROR,
			                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			                     _("Missing required remote IP address for static key mode."));
			free_openvpn_args (args);
			return FALSE;
		}
		add_openvpn_arg (args, tmp);
	} else if (!strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD)) {
		/* Client mode */
		add_openvpn_arg (args, "--client");
		/* Use user/path authentication */
		add_openvpn_arg (args, "--auth-user-pass");

		tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CA);
		if (tmp && strlen (tmp)) {
			add_openvpn_arg (args, "--ca");
			add_openvpn_arg_utf8safe (args, tmp);
		}
	} else if (!strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD_TLS)) {
		add_openvpn_arg (args, "--client");
		add_cert_args (args, s_vpn);
		/* Use user/path authentication */
		add_openvpn_arg (args, "--auth-user-pass");
	} else {
		g_set_error (error,
		             NM_VPN_PLUGIN_ERROR,
		             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		             _("Unknown connection type '%s'."),
		             connection_type);
		free_openvpn_args (args);
		return FALSE;
	}

	/* Allow openvpn to be run as a specified user:group and drop privileges. */
	nm_openvpn_user = getenv ("NM_OPENVPN_USER");
	nm_openvpn_group = getenv ("NM_OPENVPN_GROUP");
	nm_openvpn_chroot = getenv ("NM_OPENVPN_CHROOT");
	if (!nm_openvpn_user)
		nm_openvpn_user = NM_OPENVPN_USER;
	if (!nm_openvpn_group)
		nm_openvpn_group = NM_OPENVPN_GROUP;
	if (!nm_openvpn_chroot)
		nm_openvpn_chroot = NM_OPENVPN_CHROOT;

	if (*nm_openvpn_user) {
		if (getpwnam (nm_openvpn_user)) {
			add_openvpn_arg (args, "--user");
			add_openvpn_arg (args, nm_openvpn_user);
		} else {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("User '%s' not found, check NM_OPENVPN_USER."),
			             nm_openvpn_user);
			free_openvpn_args (args);
			return FALSE;
		}
	}
	if (*nm_openvpn_group) {
		if (getgrnam (nm_openvpn_group)) {
			add_openvpn_arg (args, "--group");
			add_openvpn_arg (args, nm_openvpn_group);
		} else {
			g_set_error (error,
			             NM_VPN_PLUGIN_ERROR,
			             NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
			             _("Group '%s' not found, check NM_OPENVPN_GROUP."),
			             nm_openvpn_group);
			free_openvpn_args (args);
			return FALSE;
		}
	}
	if (*nm_openvpn_chroot) {
		if (check_chroot_dir_usability (nm_openvpn_chroot, nm_openvpn_user)) {
			add_openvpn_arg (args, "--chroot");
			add_openvpn_arg (args, nm_openvpn_chroot);
		} else
			_LOG_W ("Directory '%s' not usable for chroot by '%s', openvpn will not be chrooted.",
			        nm_openvpn_chroot, nm_openvpn_user);
	}

	g_ptr_array_add (args, NULL);

	{
		gs_free char *cmd = NULL;

		_LOG_d ("EXEC: '%s'", (cmd = g_strjoinv (" ", (char **) args->pdata)));
	}

	if (!g_spawn_async (NULL, (char **) args->pdata, NULL,
	                    G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, error)) {
		free_openvpn_args (args);
		return FALSE;
	}
	free_openvpn_args (args);

	pids_pending_add (pid, plugin);

	g_warn_if_fail (!priv->pid);
	priv->pid = pid;

	/* Listen to the management socket for a few connection types:
	   PASSWORD: Will require username and password
	   X509USERPASS: Will require username and password and maybe certificate password
	   X509: May require certificate password
	*/
	if (   !strcmp (connection_type, NM_OPENVPN_CONTYPE_TLS)
	    || !strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD)
	    || !strcmp (connection_type, NM_OPENVPN_CONTYPE_PASSWORD_TLS)
	    || nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_USERNAME)) {

		priv->io_data = g_malloc0 (sizeof (NMOpenvpnPluginIOData));
		update_io_data_from_vpn_setting (priv->io_data, s_vpn, default_username);
		nm_openvpn_schedule_connect_timer (plugin);
	}

	return TRUE;
}

static const char *
check_need_secrets (NMSettingVpn *s_vpn, gboolean *need_secrets)
{
	const char *tmp, *key, *ctype;
	NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE;
	gs_free char *key_free = NULL;

	g_return_val_if_fail (s_vpn != NULL, FALSE);
	g_return_val_if_fail (need_secrets != NULL, FALSE);

	*need_secrets = FALSE;

	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CONNECTION_TYPE);
	ctype = validate_connection_type (tmp);
	if (!ctype)
		return NULL;

	if (!strcmp (ctype, NM_OPENVPN_CONTYPE_PASSWORD_TLS)) {
		/* Will require a password and maybe private key password */
		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEY);
		key = nmv_utils_str_utf8safe_unescape_c (key, &key_free);
		if (is_encrypted (key) && !nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_CERTPASS))
			*need_secrets = TRUE;

		if (!nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_PASSWORD)) {
			*need_secrets = TRUE;
			if (nm_setting_get_secret_flags (NM_SETTING (s_vpn), NM_OPENVPN_KEY_PASSWORD, &secret_flags, NULL)) {
				if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_REQUIRED)
					*need_secrets = FALSE;
			}
		}
	} else if (!strcmp (ctype, NM_OPENVPN_CONTYPE_PASSWORD)) {
		/* Will require a password */
		if (!nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_PASSWORD)) {
			*need_secrets = TRUE;
			if (nm_setting_get_secret_flags (NM_SETTING (s_vpn), NM_OPENVPN_KEY_PASSWORD, &secret_flags, NULL)) {
				if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_REQUIRED)
					*need_secrets = FALSE;
			}
		}
	} else if (!strcmp (ctype, NM_OPENVPN_CONTYPE_TLS)) {
		/* May require private key password */
		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEY);
		key = nmv_utils_str_utf8safe_unescape_c (key, &key_free);
		if (is_encrypted (key) && !nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_CERTPASS))
			*need_secrets = TRUE;
	} else {
		/* Static key doesn't need passwords */
	}

	/* HTTP Proxy might require a password; assume so if there's an HTTP proxy username */
	tmp = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_USERNAME);
	if (tmp && !nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD))
		*need_secrets = TRUE;

	return ctype;
}

static gboolean
real_disconnect (NMVpnServicePlugin *plugin,
                 GError **err)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);

	g_clear_pointer (&priv->mgt_path, g_free);

	if (priv->pid) {
		pids_pending_send_sigterm (priv->pid);
		priv->pid = 0;
	}

	return TRUE;
}

static gboolean
_connect_common (NMVpnServicePlugin   *plugin,
                 NMConnection  *connection,
                 GVariant      *details,
                 GError       **error)
{
	NMSettingVpn *s_vpn;
	const char *connection_type;
	const char *user_name;

	if (!real_disconnect (plugin, error)) {
		_LOG_W ("Could not clean up previous daemon run: %s", (*error)->message);
		g_clear_error (error);
	}

	s_vpn = nm_connection_get_setting_vpn (connection);
	if (!s_vpn) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_INVALID_CONNECTION,
		                     _("Could not process the request because the VPN connection settings were invalid."));
		return FALSE;
	}

	connection_type = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CONNECTION_TYPE);
	if (!validate_connection_type (connection_type)) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_INVALID_CONNECTION,
		                     _("Could not process the request because the openvpn connection type was invalid."));
		return FALSE;
	}

	/* Validate the properties */
	if (!nm_openvpn_properties_validate (s_vpn, error))
		return FALSE;

	/* Validate secrets */
	if (!nm_openvpn_secrets_validate (s_vpn, error))
		return FALSE;

	/* Finally try to start OpenVPN */
	user_name = nm_setting_vpn_get_user_name (s_vpn);
	if (!nm_openvpn_start_openvpn_binary (NM_OPENVPN_PLUGIN (plugin),
	                                      s_vpn,
	                                      user_name,
	                                      nm_connection_get_uuid (connection),
	                                      error))
		return FALSE;

	return TRUE;
}

static gboolean
real_connect (NMVpnServicePlugin   *plugin,
              NMConnection  *connection,
              GError       **error)
{
	return _connect_common (plugin, connection, NULL, error);
}

static gboolean
real_connect_interactive (NMVpnServicePlugin   *plugin,
                          NMConnection  *connection,
                          GVariant      *details,
                          GError       **error)
{
	if (!_connect_common (plugin, connection, details, error))
		return FALSE;

	NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin)->interactive = TRUE;
	return TRUE;
}

static gboolean
real_need_secrets (NMVpnServicePlugin *plugin,
                   NMConnection *connection,
                   const char **setting_name,
                   GError **error)
{
	NMSettingVpn *s_vpn;
	const char *connection_type;
	gboolean need_secrets = FALSE;

	g_return_val_if_fail (NM_IS_VPN_SERVICE_PLUGIN (plugin), FALSE);
	g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE);

	if (_LOG_d_enabled ()) {
		_LOG_d ("connection -------------------------------------");
		nm_connection_dump (connection);
	}

	s_vpn = nm_connection_get_setting_vpn (connection);
	if (!s_vpn) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_INVALID_CONNECTION,
		                     _("Could not process the request because the VPN connection settings were invalid."));
		return FALSE;
	}

	connection_type = check_need_secrets (s_vpn, &need_secrets);
	if (!connection_type) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS,
		                     _("Invalid connection type."));
		return FALSE;
	}

	if (need_secrets)
		*setting_name = NM_SETTING_VPN_SETTING_NAME;

	return need_secrets;
}

static gboolean
real_new_secrets (NMVpnServicePlugin *plugin,
                  NMConnection *connection,
                  GError **error)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);
	NMSettingVpn *s_vpn;
	const char *message = NULL;
	char **hints = NULL;

	s_vpn = nm_connection_get_setting_vpn (connection);
	if (!s_vpn) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_INVALID_CONNECTION,
		                     _("Could not process the request because the VPN connection settings were invalid."));
		return FALSE;
	}

	_LOG_d ("VPN received new secrets; sending to management interface");

	update_io_data_from_vpn_setting (priv->io_data, s_vpn, NULL);

	g_warn_if_fail (priv->io_data->pending_auth);
	if (!handle_auth (priv->io_data, priv->io_data->pending_auth, &message, &hints)) {
		g_set_error_literal (error,
		                     NM_VPN_PLUGIN_ERROR,
		                     NM_VPN_PLUGIN_ERROR_FAILED,
		                     _("Unhandled pending authentication."));
		return FALSE;
	}

	/* Request new secrets if we need any */
	if (message) {
		_LOG_d ("Requesting new secrets: '%s'", message);
		nm_vpn_service_plugin_secrets_required (plugin, message, (const char **) hints);
	}
	if (hints)
		g_free (hints);  /* elements are 'const' */
	return TRUE;
}

static void
nm_openvpn_plugin_init (NMOpenvpnPlugin *plugin)
{
}

static void
dispose (GObject *object)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (object);

	nm_clear_g_source (&priv->connect_timer);

	if (priv->pid) {
		pids_pending_send_sigterm (priv->pid);
		priv->pid = 0;
	}

	G_OBJECT_CLASS (nm_openvpn_plugin_parent_class)->dispose (object);
}

static void
nm_openvpn_plugin_class_init (NMOpenvpnPluginClass *plugin_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (plugin_class);
	NMVpnServicePluginClass *parent_class = NM_VPN_SERVICE_PLUGIN_CLASS (plugin_class);

	g_type_class_add_private (object_class, sizeof (NMOpenvpnPluginPrivate));

	object_class->dispose = dispose;

	/* virtual methods */
	parent_class->connect      = real_connect;
	parent_class->connect_interactive = real_connect_interactive;
	parent_class->need_secrets = real_need_secrets;
	parent_class->disconnect   = real_disconnect;
	parent_class->new_secrets  = real_new_secrets;
}

static void
plugin_state_changed (NMOpenvpnPlugin *plugin,
                      NMVpnServiceState state,
                      gpointer user_data)
{
	NMOpenvpnPluginPrivate *priv = NM_OPENVPN_PLUGIN_GET_PRIVATE (plugin);

	switch (state) {
	case NM_VPN_SERVICE_STATE_UNKNOWN:
	case NM_VPN_SERVICE_STATE_INIT:
	case NM_VPN_SERVICE_STATE_SHUTDOWN:
	case NM_VPN_SERVICE_STATE_STOPPING:
	case NM_VPN_SERVICE_STATE_STOPPED:
		/* Cleanup on failure */
		nm_clear_g_source (&priv->connect_timer);
		nm_openvpn_disconnect_management_socket (plugin);
		break;
	default:
		break;
	}
}

NMOpenvpnPlugin *
nm_openvpn_plugin_new (const char *bus_name)
{
	NMOpenvpnPlugin *plugin;
	GError *error = NULL;

	plugin =  (NMOpenvpnPlugin *) g_initable_new (NM_TYPE_OPENVPN_PLUGIN, NULL, &error,
	                                              NM_VPN_SERVICE_PLUGIN_DBUS_SERVICE_NAME, bus_name,
	                                              NM_VPN_SERVICE_PLUGIN_DBUS_WATCH_PEER, !gl.debug,
	                                              NULL);

	if (plugin) {
		g_signal_connect (G_OBJECT (plugin), "state-changed", G_CALLBACK (plugin_state_changed), NULL);
	} else {
		_LOG_W ("Failed to initialize a plugin instance: %s", error->message);
		g_error_free (error);
	}

	return plugin;
}

static gboolean
signal_handler (gpointer user_data)
{
	g_main_loop_quit (user_data);
	return G_SOURCE_REMOVE;
}

static void
quit_mainloop (NMVpnServicePlugin *plugin, gpointer user_data)
{
	g_main_loop_quit ((GMainLoop *) user_data);
}

int
main (int argc, char *argv[])
{
	NMOpenvpnPlugin *plugin;
	gboolean persist = FALSE;
	GOptionContext *opt_ctx = NULL;
	gchar *bus_name = NM_DBUS_SERVICE_OPENVPN;
	GError *error = NULL;
	GMainLoop *loop;

	GOptionEntry options[] = {
		{ "persist", 0, 0, G_OPTION_ARG_NONE, &persist, N_("Don't quit when VPN connection terminates"), NULL },
		{ "debug", 0, 0, G_OPTION_ARG_NONE, &gl.debug, N_("Enable verbose debug logging (may expose passwords)"), NULL },
		{ "bus-name", 0, 0, G_OPTION_ARG_STRING, &bus_name, N_("D-Bus name to use for this instance"), NULL },
		{NULL}
	};

#if !GLIB_CHECK_VERSION (2, 35, 0)
	g_type_init ();
#endif

	if (getenv ("OPENVPN_DEBUG"))
		gl.debug = TRUE;

	/* locale will be set according to environment LC_* variables */
	setlocale (LC_ALL, "");

	bindtextdomain (GETTEXT_PACKAGE, NM_OPENVPN_LOCALEDIR);
	bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
	textdomain (GETTEXT_PACKAGE);

	/* Parse options */
	opt_ctx = g_option_context_new (NULL);
	g_option_context_set_translation_domain (opt_ctx, GETTEXT_PACKAGE);
	g_option_context_set_ignore_unknown_options (opt_ctx, FALSE);
	g_option_context_set_help_enabled (opt_ctx, TRUE);
	g_option_context_add_main_entries (opt_ctx, options, NULL);

	g_option_context_set_summary (opt_ctx,
	                              _("nm-openvpn-service provides integrated "
	                                "OpenVPN capability to NetworkManager."));

	if (!g_option_context_parse (opt_ctx, &argc, &argv, &error)) {
		_LOG_W ("Error parsing the command line options: %s", error->message);
		g_option_context_free (opt_ctx);
		g_clear_error (&error);
		exit (1);
	}
	g_option_context_free (opt_ctx);

	_LOG_d ("nm-openvpn-service (version " DIST_VERSION ") starting...");

	if (   !g_file_test ("/sys/class/misc/tun", G_FILE_TEST_EXISTS)
	    && (system ("/sbin/modprobe tun") == -1))
		exit (EXIT_FAILURE);

	plugin = nm_openvpn_plugin_new (bus_name);
	if (!plugin)
		exit (EXIT_FAILURE);

	loop = g_main_loop_new (NULL, FALSE);

	if (!persist)
		g_signal_connect (plugin, "quit", G_CALLBACK (quit_mainloop), loop);

	signal (SIGPIPE, SIG_IGN);
	g_unix_signal_add (SIGTERM, signal_handler, loop);
	g_unix_signal_add (SIGINT, signal_handler, loop);

	g_main_loop_run (loop);
	g_object_unref (plugin);

	pids_pending_wait_for_processes (loop);

	g_main_loop_unref (loop);

	exit (EXIT_SUCCESS);
}
