diff options
Diffstat (limited to 'src/libnm-client-impl/nm-vpn-service-plugin.c')
-rw-r--r-- | src/libnm-client-impl/nm-vpn-service-plugin.c | 1342 |
1 files changed, 1342 insertions, 0 deletions
diff --git a/src/libnm-client-impl/nm-vpn-service-plugin.c b/src/libnm-client-impl/nm-vpn-service-plugin.c new file mode 100644 index 0000000000..8262981d70 --- /dev/null +++ b/src/libnm-client-impl/nm-vpn-service-plugin.c @@ -0,0 +1,1342 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2007 - 2008 Novell, Inc. + * Copyright (C) 2007 - 2015 Red Hat, Inc. + */ + +#include "libnm-client-impl/nm-default-libnm.h" + +#include "nm-vpn-service-plugin.h" + +#include <signal.h> +#include <stdlib.h> + +#include "libnm-glib-aux/nm-secret-utils.h" +#include "libnm-glib-aux/nm-dbus-aux.h" +#include "nm-enum-types.h" +#include "nm-utils.h" +#include "nm-connection.h" +#include "nm-dbus-helpers.h" +#include "libnm-core-intern/nm-core-internal.h" +#include "nm-simple-connection.h" + +#include "introspection/org.freedesktop.NetworkManager.VPN.Plugin.h" + +#define NM_VPN_SERVICE_PLUGIN_QUIT_TIMER 180 + +static void nm_vpn_service_plugin_initable_iface_init(GInitableIface *iface); + +G_DEFINE_ABSTRACT_TYPE_WITH_CODE(NMVpnServicePlugin, + nm_vpn_service_plugin, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, + nm_vpn_service_plugin_initable_iface_init);) + +typedef struct { + NMVpnServiceState state; + + /* DBUS-y stuff */ + GDBusConnection *connection; + NMDBusVpnPlugin *dbus_vpn_service_plugin; + char * dbus_service_name; + gboolean dbus_watch_peer; + + /* Temporary stuff */ + guint connect_timer; + guint quit_timer; + guint fail_stop_id; + guint peer_watch_id; + gboolean interactive; + + gboolean got_config; + gboolean has_ip4, got_ip4; + gboolean has_ip6, got_ip6; + + /* Config stuff copied from config to ip4config */ + GVariant *banner, *tundev, *gateway, *mtu; +} NMVpnServicePluginPrivate; + +#define NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE((o), NM_TYPE_VPN_SERVICE_PLUGIN, NMVpnServicePluginPrivate)) + +enum { + STATE_CHANGED, + CONFIG, + IP4_CONFIG, + IP6_CONFIG, + LOGIN_BANNER, + FAILURE, + QUIT, + SECRETS_REQUIRED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_DBUS_SERVICE_NAME, PROP_DBUS_WATCH_PEER, PROP_STATE, ); + +static GSList *active_plugins = NULL; + +static void +nm_vpn_service_plugin_set_connection(NMVpnServicePlugin *plugin, GDBusConnection *connection) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + g_clear_object(&priv->connection); + + if (connection) + priv->connection = g_object_ref(connection); +} + +/** + * nm_vpn_service_plugin_get_connection: + * + * Returns: (transfer full): + * + * Since: 1.2 + */ +GDBusConnection * +nm_vpn_service_plugin_get_connection(NMVpnServicePlugin *plugin) +{ + GDBusConnection *connection; + + g_return_val_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin), NULL); + + connection = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin)->connection; + + if (connection) + g_object_ref(connection); + + return connection; +} + +static NMVpnServiceState +nm_vpn_service_plugin_get_state(NMVpnServicePlugin *plugin) +{ + g_return_val_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin), NM_VPN_SERVICE_STATE_UNKNOWN); + + return NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin)->state; +} + +static void +nm_vpn_service_plugin_set_state(NMVpnServicePlugin *plugin, NMVpnServiceState state) +{ + NMVpnServicePluginPrivate *priv; + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + + priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + if (priv->state != state) { + priv->state = state; + g_signal_emit(plugin, signals[STATE_CHANGED], 0, state); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_state_changed(priv->dbus_vpn_service_plugin, state); + } +} + +void +nm_vpn_service_plugin_set_login_banner(NMVpnServicePlugin *plugin, const char *banner) +{ + NMVpnServicePluginPrivate *priv; + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + g_return_if_fail(banner != NULL); + + priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + g_signal_emit(plugin, signals[LOGIN_BANNER], 0, banner); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_login_banner(priv->dbus_vpn_service_plugin, banner); +} + +static void +_emit_failure(NMVpnServicePlugin *plugin, NMVpnPluginFailure reason) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + g_signal_emit(plugin, signals[FAILURE], 0, reason); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_failure(priv->dbus_vpn_service_plugin, reason); +} + +void +nm_vpn_service_plugin_failure(NMVpnServicePlugin *plugin, NMVpnPluginFailure reason) +{ + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + + _emit_failure(plugin, reason); + nm_vpn_service_plugin_disconnect(plugin, NULL); +} + +gboolean +nm_vpn_service_plugin_disconnect(NMVpnServicePlugin *plugin, GError **err) +{ + gboolean ret = FALSE; + NMVpnServiceState state; + + g_return_val_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin), FALSE); + + state = nm_vpn_service_plugin_get_state(plugin); + switch (state) { + case NM_VPN_SERVICE_STATE_STOPPING: + g_set_error( + err, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_STOPPING_IN_PROGRESS, + "%s", + "Could not process the request because the VPN connection is already being stopped."); + break; + case NM_VPN_SERVICE_STATE_STOPPED: + g_set_error(err, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_ALREADY_STOPPED, + "%s", + "Could not process the request because no VPN connection was active."); + break; + case NM_VPN_SERVICE_STATE_STARTING: + _emit_failure(plugin, NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED); + /* fall-through */ + case NM_VPN_SERVICE_STATE_STARTED: + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STOPPING); + ret = NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->disconnect(plugin, err); + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STOPPED); + break; + case NM_VPN_SERVICE_STATE_INIT: + ret = TRUE; + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STOPPED); + break; + + default: + g_warning("Unhandled VPN service state %d", state); + g_assert_not_reached(); + break; + } + + return ret; +} + +static void +nm_vpn_service_plugin_emit_quit(NMVpnServicePlugin *plugin) +{ + g_signal_emit(plugin, signals[QUIT], 0); +} + +/** + * nm_vpn_service_plugin_shutdown: + * @plugin: the #NMVpnServicePlugin instance + * + * Shutdown the @plugin and disconnect from D-Bus. After this, + * the plugin instance is dead and should no longer be used. + * It ensures to get no more requests from D-Bus. In principle, + * you don't need to shutdown the plugin, disposing the instance + * has the same effect. However, this gives a way to deactivate + * the plugin before giving up the last reference. + * + * Since: 1.12 + */ +void +nm_vpn_service_plugin_shutdown(NMVpnServicePlugin *plugin) +{ + NMVpnServicePluginPrivate *priv; + NMVpnServiceState state; + GError * error = NULL; + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + + priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + nm_clear_g_source(&priv->fail_stop_id); + nm_clear_g_source(&priv->quit_timer); + nm_clear_g_source(&priv->connect_timer); + + state = nm_vpn_service_plugin_get_state(plugin); + if (state == NM_VPN_SERVICE_STATE_STARTED || state == NM_VPN_SERVICE_STATE_STARTING) { + nm_vpn_service_plugin_disconnect(plugin, &error); + + if (error) { + g_warning("Error disconnecting VPN connection: %s", error->message); + g_error_free(error); + } + } + + if (priv->dbus_vpn_service_plugin) { + g_dbus_interface_skeleton_unexport( + G_DBUS_INTERFACE_SKELETON(priv->dbus_vpn_service_plugin)); + g_clear_object(&priv->dbus_vpn_service_plugin); + } +} + +static gboolean +connect_timer_expired(gpointer data) +{ + NMVpnServicePlugin *plugin = NM_VPN_SERVICE_PLUGIN(data); + GError * err = NULL; + + NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin)->connect_timer = 0; + g_message("Connect timer expired, disconnecting."); + nm_vpn_service_plugin_disconnect(plugin, &err); + if (err) { + g_warning("Disconnect failed: %s", err->message); + g_error_free(err); + } + + return G_SOURCE_REMOVE; +} + +static gboolean +quit_timer_expired(gpointer data) +{ + NMVpnServicePlugin *self = NM_VPN_SERVICE_PLUGIN(data); + + NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(self)->quit_timer = 0; + nm_vpn_service_plugin_emit_quit(self); + return G_SOURCE_REMOVE; +} + +static void +schedule_quit_timer(NMVpnServicePlugin *self) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(self); + + nm_clear_g_source(&priv->quit_timer); + priv->quit_timer = + g_timeout_add_seconds(NM_VPN_SERVICE_PLUGIN_QUIT_TIMER, quit_timer_expired, self); +} + +static gboolean +fail_stop(gpointer data) +{ + NMVpnServicePlugin *self = NM_VPN_SERVICE_PLUGIN(data); + + NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(self)->fail_stop_id = 0; + nm_vpn_service_plugin_set_state(self, NM_VPN_SERVICE_STATE_STOPPED); + return G_SOURCE_REMOVE; +} + +static void +schedule_fail_stop(NMVpnServicePlugin *plugin, guint timeout_secs) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + nm_clear_g_source(&priv->fail_stop_id); + if (timeout_secs) + priv->fail_stop_id = g_timeout_add_seconds(timeout_secs, fail_stop, plugin); + else + priv->fail_stop_id = g_idle_add(fail_stop, plugin); +} + +void +nm_vpn_service_plugin_set_config(NMVpnServicePlugin *plugin, GVariant *config) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + g_return_if_fail(config != NULL); + + priv->got_config = TRUE; + + (void) g_variant_lookup(config, NM_VPN_PLUGIN_CONFIG_HAS_IP4, "b", &priv->has_ip4); + (void) g_variant_lookup(config, NM_VPN_PLUGIN_CONFIG_HAS_IP6, "b", &priv->has_ip6); + + /* Record the items that need to also be inserted into the + * ip4config, for compatibility with older daemons. + */ + if (priv->banner) + g_variant_unref(priv->banner); + priv->banner = g_variant_lookup_value(config, NM_VPN_PLUGIN_CONFIG_BANNER, G_VARIANT_TYPE("s")); + if (priv->tundev) + g_variant_unref(priv->tundev); + priv->tundev = g_variant_lookup_value(config, NM_VPN_PLUGIN_CONFIG_TUNDEV, G_VARIANT_TYPE("s")); + if (priv->gateway) + g_variant_unref(priv->gateway); + priv->gateway = + g_variant_lookup_value(config, NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY, G_VARIANT_TYPE("u")); + if (priv->mtu) + g_variant_unref(priv->mtu); + priv->mtu = g_variant_lookup_value(config, NM_VPN_PLUGIN_CONFIG_MTU, G_VARIANT_TYPE("u")); + + g_signal_emit(plugin, signals[CONFIG], 0, config); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_config(priv->dbus_vpn_service_plugin, config); + + if (priv->has_ip4 == priv->got_ip4 && priv->has_ip6 == priv->got_ip6) + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STARTED); +} + +void +nm_vpn_service_plugin_set_ip4_config(NMVpnServicePlugin *plugin, GVariant *ip4_config) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + GVariant * combined_config; + GVariantBuilder builder; + GVariantIter iter; + const char * key; + GVariant * value; + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + g_return_if_fail(ip4_config != NULL); + + priv->got_ip4 = TRUE; + + /* Old plugins won't send the "config" signal and thus can't send + * NM_VPN_SERVICE_PLUGIN_CONFIG_HAS_IP4 either. But since they don't support IPv6, + * we can safely assume that, if we don't receive a "config" signal but do + * receive an "ip4-config" signal, the old plugin supports IPv4. + */ + if (!priv->got_config) + priv->has_ip4 = TRUE; + + /* Older NetworkManager daemons expect all config info to be in + * the ip4 config, so they won't even notice the "config" signal + * being emitted. So just copy all of that data into the ip4 + * config too. + */ + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_iter_init(&iter, ip4_config); + while (g_variant_iter_next(&iter, "{&sv}", &key, &value)) { + g_variant_builder_add(&builder, "{sv}", key, value); + g_variant_unref(value); + } + + if (priv->banner) + g_variant_builder_add(&builder, "{sv}", NM_VPN_PLUGIN_IP4_CONFIG_BANNER, priv->banner); + if (priv->tundev) + g_variant_builder_add(&builder, "{sv}", NM_VPN_PLUGIN_IP4_CONFIG_TUNDEV, priv->tundev); + if (priv->gateway) + g_variant_builder_add(&builder, + "{sv}", + NM_VPN_PLUGIN_IP4_CONFIG_EXT_GATEWAY, + priv->gateway); + if (priv->mtu) + g_variant_builder_add(&builder, "{sv}", NM_VPN_PLUGIN_IP4_CONFIG_MTU, priv->mtu); + + combined_config = g_variant_builder_end(&builder); + g_variant_ref_sink(combined_config); + g_signal_emit(plugin, signals[IP4_CONFIG], 0, combined_config); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_ip4_config(priv->dbus_vpn_service_plugin, combined_config); + g_variant_unref(combined_config); + + if (priv->has_ip4 == priv->got_ip4 && priv->has_ip6 == priv->got_ip6) + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STARTED); +} + +void +nm_vpn_service_plugin_set_ip6_config(NMVpnServicePlugin *plugin, GVariant *ip6_config) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + g_return_if_fail(NM_IS_VPN_SERVICE_PLUGIN(plugin)); + g_return_if_fail(ip6_config != NULL); + + g_variant_ref_sink(ip6_config); + + priv->got_ip6 = TRUE; + g_signal_emit(plugin, signals[IP6_CONFIG], 0, ip6_config); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_ip6_config(priv->dbus_vpn_service_plugin, ip6_config); + + g_variant_unref(ip6_config); + + if (priv->has_ip4 == priv->got_ip4 && priv->has_ip6 == priv->got_ip6) + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STARTED); +} + +static void +connect_timer_start(NMVpnServicePlugin *plugin) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + nm_clear_g_source(&priv->connect_timer); + priv->connect_timer = g_timeout_add_seconds(60, connect_timer_expired, plugin); +} + +static void +peer_vanished(GDBusConnection *connection, + const char * sender_name, + const char * object_path, + const char * interface_name, + const char * signal_name, + GVariant * parameters, + gpointer user_data) +{ + nm_vpn_service_plugin_disconnect(NM_VPN_SERVICE_PLUGIN(user_data), NULL); +} + +static guint +watch_peer(NMVpnServicePlugin *plugin, GDBusMethodInvocation *context) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection(context); + const char *peer = g_dbus_message_get_sender(g_dbus_method_invocation_get_message(context)); + + return nm_dbus_connection_signal_subscribe_name_owner_changed(connection, + peer, + peer_vanished, + plugin, + NULL); +} + +static void +_connect_generic(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * properties, + GVariant * details) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + NMVpnServicePluginClass * vpn_class = NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin); + NMConnection * connection; + gboolean success = FALSE; + GError * error = NULL; + guint fail_stop_timeout = 0; + + if (priv->state != NM_VPN_SERVICE_STATE_STOPPED && priv->state != NM_VPN_SERVICE_STATE_INIT) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_WRONG_STATE, + "Could not start connection: wrong plugin state %d", + priv->state); + return; + } + + connection = + _nm_simple_connection_new_from_dbus(properties, NM_SETTING_PARSE_FLAGS_BEST_EFFORT, &error); + if (!connection) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS, + "Invalid connection: %s", + error->message); + g_clear_error(&error); + return; + } + + priv->interactive = FALSE; + if (details && !vpn_class->connect_interactive) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED, + "Plugin does not implement ConnectInteractive()"); + return; + } + + nm_clear_g_source(&priv->fail_stop_id); + + if (priv->dbus_watch_peer) + priv->peer_watch_id = watch_peer(plugin, context); + + if (details) { + priv->interactive = TRUE; + success = vpn_class->connect_interactive(plugin, connection, details, &error); + if (g_error_matches(error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED)) { + /* Give NetworkManager a bit of time to fall back to Connect() */ + fail_stop_timeout = 5; + } + } else + success = vpn_class->connect(plugin, connection, &error); + + if (success) { + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_STARTING); + + g_dbus_method_invocation_return_value(context, NULL); + + /* Add a timer to make sure we do not wait indefinitely for the successful connect. */ + connect_timer_start(plugin); + } else { + g_dbus_method_invocation_take_error(context, error); + + /* Stop the plugin from an idle handler so that the Connect + * method return gets sent before the STOP StateChanged signal. + */ + schedule_fail_stop(plugin, fail_stop_timeout); + } + + g_object_unref(connection); +} + +static void +impl_vpn_service_plugin_connect(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * connection, + gpointer user_data) +{ + _connect_generic(plugin, context, connection, NULL); +} + +static void +impl_vpn_service_plugin_connect_interactive(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * connection, + GVariant * details, + gpointer user_data) +{ + _connect_generic(plugin, context, connection, details); +} + +/*****************************************************************************/ + +static void +impl_vpn_service_plugin_need_secrets(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * properties, + gpointer user_data) +{ + NMConnection *connection; + const char * setting_name; + gboolean needed; + GError * error = NULL; + + connection = + _nm_simple_connection_new_from_dbus(properties, NM_SETTING_PARSE_FLAGS_BEST_EFFORT, &error); + if (!connection) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_INVALID_CONNECTION, + "The connection was invalid: %s", + error->message); + g_error_free(error); + return; + } + + if (!NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->need_secrets) { + g_dbus_method_invocation_return_value(context, g_variant_new("(s)", "")); + return; + } + + needed = NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->need_secrets(plugin, + connection, + &setting_name, + &error); + if (error) { + g_dbus_method_invocation_take_error(context, error); + return; + } + + if (needed) { + /* Push back the quit timer so the VPN plugin doesn't quit in the + * middle of asking the user for secrets. + */ + schedule_quit_timer(plugin); + + g_assert(setting_name); + g_dbus_method_invocation_return_value(context, g_variant_new("(s)", setting_name)); + } else { + /* No secrets required */ + g_dbus_method_invocation_return_value(context, g_variant_new("(s)", "")); + } +} + +static void +impl_vpn_service_plugin_new_secrets(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * properties, + gpointer user_data) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + NMConnection * connection; + GError * error = NULL; + gboolean success; + + if (priv->state != NM_VPN_SERVICE_STATE_STARTING) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_WRONG_STATE, + "Could not accept new secrets: wrong plugin state %d", + priv->state); + return; + } + + connection = + _nm_simple_connection_new_from_dbus(properties, NM_SETTING_PARSE_FLAGS_BEST_EFFORT, &error); + if (!connection) { + g_dbus_method_invocation_return_error(context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS, + "Invalid connection: %s", + error->message); + g_clear_error(&error); + return; + } + + if (!NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->new_secrets) { + g_dbus_method_invocation_return_error( + context, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED, + "Could not accept new secrets: plugin cannot process interactive secrets"); + g_object_unref(connection); + return; + } + + success = NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->new_secrets(plugin, connection, &error); + if (success) { + g_dbus_method_invocation_return_value(context, NULL); + + /* Add a timer to make sure we do not wait indefinitely for the successful connect. */ + connect_timer_start(plugin); + } else { + g_dbus_method_invocation_take_error(context, error); + + /* Stop the plugin from and idle handler so that the NewSecrets + * method return gets sent before the STOP StateChanged signal. + */ + schedule_fail_stop(plugin, 0); + } + + g_object_unref(connection); +} + +/** + * nm_vpn_service_plugin_secrets_required: + * @plugin: the #NMVpnServicePlugin + * @message: an information message about why secrets are required, if any + * @hints: VPN specific secret names for required new secrets + * + * Called by VPN plugin implementations to signal to NetworkManager that secrets + * are required during the connection process. This signal may be used to + * request new secrets when the secrets originally provided by NetworkManager + * are insufficient, or the VPN process indicates that it needs additional + * information to complete the request. + * + * Since: 1.2 + */ +void +nm_vpn_service_plugin_secrets_required(NMVpnServicePlugin *plugin, + const char * message, + const char ** hints) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + /* Plugin must be able to accept the new secrets if it calls this method */ + g_return_if_fail(NM_VPN_SERVICE_PLUGIN_GET_CLASS(plugin)->new_secrets); + + /* Plugin cannot call this method if NetworkManager didn't originally call + * ConnectInteractive(). + */ + g_return_if_fail(priv->interactive == TRUE); + + /* Cancel the connect timer since secrets might take a while. It'll + * get restarted when the secrets come back via NewSecrets(). + */ + nm_clear_g_source(&priv->connect_timer); + + g_signal_emit(plugin, signals[SECRETS_REQUIRED], 0, message, hints); + if (priv->dbus_vpn_service_plugin) + nmdbus_vpn_plugin_emit_secrets_required(priv->dbus_vpn_service_plugin, message, hints); +} + +/*****************************************************************************/ + +#define DATA_KEY_TAG "DATA_KEY=" +#define DATA_VAL_TAG "DATA_VAL=" +#define SECRET_KEY_TAG "SECRET_KEY=" +#define SECRET_VAL_TAG "SECRET_VAL=" + +/** + * nm_vpn_service_plugin_read_vpn_details: + * @fd: file descriptor to read from, usually stdin (0) + * @out_data: (out) (transfer full): on successful return, a hash table + * (mapping char*:char*) containing the key/value pairs of VPN data items + * @out_secrets: (out) (transfer full): on successful return, a hash table + * (mapping char*:char*) containing the key/value pairsof VPN secrets + * + * Parses key/value pairs from a file descriptor (normally stdin) passed by + * an applet when the applet calls the authentication dialog of the VPN plugin. + * + * Returns: %TRUE if reading values was successful, %FALSE if not + * + * Since: 1.2 + **/ +gboolean +nm_vpn_service_plugin_read_vpn_details(int fd, GHashTable **out_data, GHashTable **out_secrets) +{ + gs_unref_hashtable GHashTable *data = NULL; + gs_unref_hashtable GHashTable *secrets = NULL; + gboolean success = FALSE; + GHashTable * hash = NULL; + GString * key = NULL, *val = NULL; + nm_auto_free_gstring GString *line = NULL; + char c; + + GString *str = NULL; + + if (out_data) + g_return_val_if_fail(*out_data == NULL, FALSE); + if (out_secrets) + g_return_val_if_fail(*out_secrets == NULL, FALSE); + + data = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free); + secrets = + g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret); + + line = g_string_new(NULL); + + /* Read stdin for data and secret items until we get a DONE */ + while (1) { + ssize_t nr; + + nr = read(fd, &c, 1); + if (nr < 0) { + if (errno == EAGAIN) { + g_usleep(100); + continue; + } + break; + } + if (nr > 0 && c != '\n') { + g_string_append_c(line, c); + continue; + } + + if (str && *line->str == '=') { + /* continuation */ + g_string_append_c(str, '\n'); + g_string_append(str, line->str + 1); + } else if (key && val) { + /* done a line */ + g_return_val_if_fail(hash, FALSE); + g_hash_table_insert(hash, g_string_free(key, FALSE), g_string_free(val, FALSE)); + key = NULL; + val = NULL; + hash = NULL; + success = TRUE; /* Got at least one value */ + } + + if (strcmp(line->str, "DONE") == 0) { + /* finish marker */ + break; + } else if (strncmp(line->str, DATA_KEY_TAG, strlen(DATA_KEY_TAG)) == 0) { + if (key != NULL) { + g_warning("a value expected"); + g_string_free(key, TRUE); + } + key = g_string_new(line->str + strlen(DATA_KEY_TAG)); + str = key; + hash = data; + } else if (strncmp(line->str, DATA_VAL_TAG, strlen(DATA_VAL_TAG)) == 0) { + if (val != NULL) + g_string_free(val, TRUE); + if (val || !key || hash != data) { + g_warning("%s not preceded by %s", DATA_VAL_TAG, DATA_KEY_TAG); + break; + } + val = g_string_new(line->str + strlen(DATA_VAL_TAG)); + str = val; + } else if (strncmp(line->str, SECRET_KEY_TAG, strlen(SECRET_KEY_TAG)) == 0) { + if (key != NULL) { + g_warning("a value expected"); + g_string_free(key, TRUE); + } + key = g_string_new(line->str + strlen(SECRET_KEY_TAG)); + str = key; + hash = secrets; + } else if (strncmp(line->str, SECRET_VAL_TAG, strlen(SECRET_VAL_TAG)) == 0) { + if (val != NULL) + g_string_free(val, TRUE); + if (val || !key || hash != secrets) { + g_warning("%s not preceded by %s", SECRET_VAL_TAG, SECRET_KEY_TAG); + break; + } + val = g_string_new(line->str + strlen(SECRET_VAL_TAG)); + str = val; + } + + g_string_truncate(line, 0); + + if (nr == 0) + break; + } + + if (success) { + NM_SET_OUT(out_data, g_steal_pointer(&data)); + NM_SET_OUT(out_secrets, g_steal_pointer(&secrets)); + } + return success; +} + +/** + * nm_vpn_service_plugin_get_secret_flags: + * @data: hash table containing VPN key/value pair data items + * @secret_name: VPN secret key name for which to retrieve flags for + * @out_flags: (out): on success, the flags associated with @secret_name + * + * Given a VPN secret key name, attempts to find the corresponding flags data + * item in @data. If found, converts the flags data item to + * #NMSettingSecretFlags and returns it. + * + * Returns: %TRUE if the flag data item was found and successfully converted + * to flags, %FALSE if not + * + * Since: 1.2 + **/ +gboolean +nm_vpn_service_plugin_get_secret_flags(GHashTable * data, + const char * secret_name, + NMSettingSecretFlags *out_flags) +{ + gs_free char * flag_name_free = NULL; + const char * s; + gint64 t1; + NMSettingSecretFlags t0; + + g_return_val_if_fail(data, FALSE); + g_return_val_if_fail(out_flags && *out_flags == NM_SETTING_SECRET_FLAG_NONE, FALSE); + if (!secret_name || !*secret_name) + g_return_val_if_reached(FALSE); + + s = g_hash_table_lookup(data, nm_construct_name_a("%s-flags", secret_name, &flag_name_free)); + if (!s) + return FALSE; + t1 = _nm_utils_ascii_str_to_int64(s, 10, 0, G_MAXINT64, -1); + if (t1 == -1) + return FALSE; + t0 = (NMSettingSecretFlags) t1; + if ((gint64) t0 != t1) + return FALSE; + NM_SET_OUT(out_flags, t0); + return TRUE; +} + +/*****************************************************************************/ + +static void +impl_vpn_service_plugin_disconnect(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + gpointer user_data) +{ + GError *error = NULL; + + if (nm_vpn_service_plugin_disconnect(plugin, &error)) + g_dbus_method_invocation_return_value(context, NULL); + else + g_dbus_method_invocation_take_error(context, error); +} + +static void +impl_vpn_service_plugin_set_config(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * config, + gpointer user_data) +{ + nm_vpn_service_plugin_set_config(plugin, config); + g_dbus_method_invocation_return_value(context, NULL); +} + +static void +impl_vpn_service_plugin_set_ip4_config(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * config, + gpointer user_data) +{ + nm_vpn_service_plugin_set_ip4_config(plugin, config); + g_dbus_method_invocation_return_value(context, NULL); +} + +static void +impl_vpn_service_plugin_set_ip6_config(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + GVariant * config, + gpointer user_data) +{ + nm_vpn_service_plugin_set_ip6_config(plugin, config); + g_dbus_method_invocation_return_value(context, NULL); +} + +static void +impl_vpn_service_plugin_set_failure(NMVpnServicePlugin * plugin, + GDBusMethodInvocation *context, + char * reason, + gpointer user_data) +{ + nm_vpn_service_plugin_failure(plugin, NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG); + g_dbus_method_invocation_return_value(context, NULL); +} + +/*****************************************************************************/ + +static void +_emit_quit(gpointer data, gpointer user_data) +{ + NMVpnServicePlugin *plugin = data; + + nm_vpn_service_plugin_emit_quit(plugin); +} + +static void +sigterm_handler(int signum) +{ + g_slist_foreach(active_plugins, _emit_quit, NULL); +} + +static void +setup_unix_signal_handler(void) +{ + struct sigaction action; + sigset_t block_mask; + + action.sa_handler = sigterm_handler; + sigemptyset(&block_mask); + action.sa_mask = block_mask; + action.sa_flags = 0; + sigaction(SIGINT, &action, NULL); + sigaction(SIGTERM, &action, NULL); +} + +/*****************************************************************************/ + +static void +one_plugin_destroyed(gpointer data, GObject *object) +{ + active_plugins = g_slist_remove(active_plugins, object); +} + +static void +nm_vpn_service_plugin_init(NMVpnServicePlugin *plugin) +{ + active_plugins = g_slist_append(active_plugins, plugin); + g_object_weak_ref(G_OBJECT(plugin), one_plugin_destroyed, NULL); +} + +static gboolean +init_sync(GInitable *initable, GCancellable *cancellable, GError **error) +{ + NMVpnServicePlugin * plugin = NM_VPN_SERVICE_PLUGIN(initable); + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + gs_unref_object GDBusConnection *connection = NULL; + gs_unref_object GDBusProxy *proxy = NULL; + GVariant * ret; + + if (!priv->dbus_service_name) { + g_set_error_literal(error, + NM_VPN_PLUGIN_ERROR, + NM_VPN_PLUGIN_ERROR_BAD_ARGUMENTS, + _("No service name specified")); + return FALSE; + } + + connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); + if (!connection) + return FALSE; + + proxy = g_dbus_proxy_new_sync(connection, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES + | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, + NULL, + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS, + cancellable, + error); + if (!proxy) + return FALSE; + + priv->dbus_vpn_service_plugin = nmdbus_vpn_plugin_skeleton_new(); + + _nm_dbus_bind_properties(plugin, priv->dbus_vpn_service_plugin); + _nm_dbus_bind_methods(plugin, + priv->dbus_vpn_service_plugin, + "Connect", + impl_vpn_service_plugin_connect, + "ConnectInteractive", + impl_vpn_service_plugin_connect_interactive, + "NeedSecrets", + impl_vpn_service_plugin_need_secrets, + "NewSecrets", + impl_vpn_service_plugin_new_secrets, + "Disconnect", + impl_vpn_service_plugin_disconnect, + "SetConfig", + impl_vpn_service_plugin_set_config, + "SetIp4Config", + impl_vpn_service_plugin_set_ip4_config, + "SetIp6Config", + impl_vpn_service_plugin_set_ip6_config, + "SetFailure", + impl_vpn_service_plugin_set_failure, + NULL); + + if (!g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(priv->dbus_vpn_service_plugin), + connection, + NM_VPN_DBUS_PLUGIN_PATH, + error)) + return FALSE; + + nm_vpn_service_plugin_set_connection(plugin, connection); + nm_vpn_service_plugin_set_state(plugin, NM_VPN_SERVICE_STATE_INIT); + + ret = g_dbus_proxy_call_sync(proxy, + "RequestName", + g_variant_new("(su)", priv->dbus_service_name, 0), + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + error); + if (!ret) { + if (error && *error) + g_dbus_error_strip_remote_error(*error); + return FALSE; + } + g_variant_unref(ret); + + return TRUE; +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_DBUS_SERVICE_NAME: + /* construct-only */ + priv->dbus_service_name = g_value_dup_string(value); + break; + case PROP_DBUS_WATCH_PEER: + /* construct-only */ + priv->dbus_watch_peer = g_value_get_boolean(value); + break; + case PROP_STATE: + nm_vpn_service_plugin_set_state(NM_VPN_SERVICE_PLUGIN(object), + (NMVpnServiceState) g_value_get_enum(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_DBUS_SERVICE_NAME: + g_value_set_string(value, priv->dbus_service_name); + break; + case PROP_DBUS_WATCH_PEER: + g_value_set_boolean(value, priv->dbus_watch_peer); + break; + case PROP_STATE: + g_value_set_enum(value, nm_vpn_service_plugin_get_state(NM_VPN_SERVICE_PLUGIN(object))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +dispose(GObject *object) +{ + nm_vpn_service_plugin_shutdown(NM_VPN_SERVICE_PLUGIN(object)); + G_OBJECT_CLASS(nm_vpn_service_plugin_parent_class)->dispose(object); +} + +static void +finalize(GObject *object) +{ + NMVpnServicePlugin * plugin = NM_VPN_SERVICE_PLUGIN(object); + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + nm_vpn_service_plugin_set_connection(plugin, NULL); + g_free(priv->dbus_service_name); + + nm_clear_pointer(&priv->banner, g_variant_unref); + nm_clear_pointer(&priv->tundev, g_variant_unref); + nm_clear_pointer(&priv->gateway, g_variant_unref); + nm_clear_pointer(&priv->mtu, g_variant_unref); + + G_OBJECT_CLASS(nm_vpn_service_plugin_parent_class)->finalize(object); +} + +static void +state_changed(NMVpnServicePlugin *plugin, NMVpnServiceState state) +{ + NMVpnServicePluginPrivate *priv = NM_VPN_SERVICE_PLUGIN_GET_PRIVATE(plugin); + + switch (state) { + case NM_VPN_SERVICE_STATE_STARTING: + nm_clear_g_source(&priv->quit_timer); + nm_clear_g_source(&priv->fail_stop_id); + break; + case NM_VPN_SERVICE_STATE_STOPPED: + if (priv->dbus_watch_peer) + nm_vpn_service_plugin_emit_quit(plugin); + else + schedule_quit_timer(plugin); + nm_clear_g_dbus_connection_signal(nm_vpn_service_plugin_get_connection(plugin), + &priv->peer_watch_id); + break; + default: + /* Clean up all timers we might have set up. */ + nm_clear_g_source(&priv->connect_timer); + nm_clear_g_source(&priv->quit_timer); + nm_clear_g_source(&priv->fail_stop_id); + break; + } +} + +static void +nm_vpn_service_plugin_class_init(NMVpnServicePluginClass *plugin_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS(plugin_class); + + g_type_class_add_private(object_class, sizeof(NMVpnServicePluginPrivate)); + + object_class->get_property = get_property; + object_class->set_property = set_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + plugin_class->state_changed = state_changed; + + /** + * NMVpnServicePlugin:service-name: + * + * The D-Bus service name of this plugin. + * + * Since: 1.2 + */ + obj_properties[PROP_DBUS_SERVICE_NAME] = + g_param_spec_string(NM_VPN_SERVICE_PLUGIN_DBUS_SERVICE_NAME, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + /** + * NMVpnServicePlugin:watch-peer: + * + * Whether to watch for D-Bus peer's changes. + * + * Since: 1.2 + */ + obj_properties[PROP_DBUS_WATCH_PEER] = + g_param_spec_boolean(NM_VPN_SERVICE_PLUGIN_DBUS_WATCH_PEER, + "", + "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + /** + * NMVpnServicePlugin:state: + * + * The state of the plugin. + * + * Since: 1.2 + */ + obj_properties[PROP_STATE] = g_param_spec_enum(NM_VPN_SERVICE_PLUGIN_STATE, + "", + "", + NM_TYPE_VPN_SERVICE_STATE, + NM_VPN_SERVICE_STATE_INIT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); + + signals[STATE_CHANGED] = g_signal_new("state-changed", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, state_changed), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_UINT); + + signals[SECRETS_REQUIRED] = g_signal_new("secrets-required", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_STRV); + + signals[CONFIG] = g_signal_new("config", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, config), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_VARIANT); + + signals[IP4_CONFIG] = g_signal_new("ip4-config", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, ip4_config), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_VARIANT); + + signals[IP6_CONFIG] = g_signal_new("ip6-config", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, ip6_config), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_VARIANT); + + signals[LOGIN_BANNER] = g_signal_new("login-banner", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, login_banner), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + signals[FAILURE] = g_signal_new("failure", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, failure), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_UINT); + + signals[QUIT] = g_signal_new("quit", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(NMVpnServicePluginClass, quit), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0, + G_TYPE_NONE); + + setup_unix_signal_handler(); +} + +static void +nm_vpn_service_plugin_initable_iface_init(GInitableIface *iface) +{ + iface->init = init_sync; +} + +/*****************************************************************************/ + +/* this header is intended to be copied to users of nm_vpn_editor_plugin_call(), + * to simplify invocation of generic functions. Include it here, to compile + * the code. */ +#include "nm-utils/nm-vpn-editor-plugin-call.h" |