/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2005 - 2013 Red Hat, Inc.
 * Copyright (C) 2006 - 2008 Novell, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-vpn-connection.h"

#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/rtnetlink.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <syslog.h>
#include <unistd.h>

#include "NetworkManagerUtils.h"
#include "dns/nm-dns-manager.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-glib-aux/nm-dbus-aux.h"
#include "libnm-platform/nm-platform.h"
#include "libnm-std-aux/unaligned.h"
#include "nm-active-connection.h"
#include "nm-config.h"
#include "nm-dbus-manager.h"
#include "nm-dispatcher.h"
#include "nm-firewalld-manager.h"
#include "nm-ip-config.h"
#include "nm-l3-config-data.h"
#include "nm-netns.h"
#include "nm-pacrunner-manager.h"
#include "nm-vpn-manager.h"
#include "nm-vpn-plugin-info.h"
#include "settings/nm-agent-manager.h"
#include "settings/nm-settings-connection.h"

/* FIXME(l3cfg): Check that we handle it correctly if the parent device is VRF type. */

/* FIXME(l3cfg): Proxy settings are no longer configured. That needs to be handled by NML3Cfg. */

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

#define DBUS_DEFAULT_TIMEOUT_MSEC 10000

typedef enum {
    L3CD_TYPE_GW_EXTERN,

    L3CD_TYPE_STATIC,
    L3CD_TYPE_GENERIC,
    L3CD_TYPE_IP_4,
    L3CD_TYPE_IP_6,

#define L3CD_TYPE_IP_X(IS_IPv4) ((IS_IPv4) ? L3CD_TYPE_IP_4 : L3CD_TYPE_IP_6)

    _L3CD_TYPE_NUM,
} L3CDType;

typedef enum {
    /* Only system secrets */
    SECRETS_REQ_SYSTEM = 0,
    /* All existing secrets including agent secrets */
    SECRETS_REQ_EXISTING = 1,
    /* New secrets required; ask an agent */
    SECRETS_REQ_NEW = 2,
    /* Plugin requests secrets interactively */
    SECRETS_REQ_INTERACTIVE = 3,
    /* Placeholder for bounds checking */
    SECRETS_REQ_LAST
} SecretsReq;

/* Internal VPN states, private to NMVpnConnection */
typedef enum {
    STATE_UNKNOWN = 0,
    STATE_WAITING,
    STATE_PREPARE,
    STATE_NEED_AUTH,
    STATE_CONNECT,
    STATE_IP_CONFIG_GET,
    STATE_PRE_UP,
    STATE_ACTIVATED,
    STATE_DEACTIVATING,
    STATE_DISCONNECTED,
    STATE_FAILED,
} VpnState;

enum {
    INTERNAL_STATE_CHANGED,
    INTERNAL_RETRY_AFTER_FAILURE,

    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

NM_GOBJECT_PROPERTIES_DEFINE(NMVpnConnection, PROP_VPN_STATE, PROP_BANNER,
#define PROP_IP4_CONFIG 2000
#define PROP_IP6_CONFIG 2001
#define PROP_MASTER     2002
#define PROP_CONTROLLER 2003
);

typedef struct {
    NMIPConfig *ip_config;

    NMIPAddr gw_internal;
    NMIPAddr gw_external;

    /* Whether VPN auto-configuration is enabled in the connection profile for
     * this address family. */
    bool method_auto : 1;

    /* Whether VPN auto-configuration is enabled, in the connection profile AND
     * in the configuration reported by the VPN. If not, then we won't have a
     * l3cd instance, but the activation for this address family is still
     * complete. */
    bool enabled : 1;
} IPData;

typedef struct {
    gboolean service_can_persist;
    gboolean connection_can_persist;

    NMSettingsConnectionCallId *secrets_id;
    SecretsReq                  secrets_idx;
    char                       *username;

    VpnState                      vpn_state;
    NMDispatcherCallId           *dispatcher_id;
    NMActiveConnectionStateReason failure_reason;

    NMVpnServiceState service_state;
    GSource          *start_timeout_source;
    NMVpnPluginInfo  *plugin_info;

    NMNetns *netns;

    NML3Cfg                 *l3cfg_if;
    NML3CfgCommitTypeHandle *l3cfg_commit_type_if;

    NML3Cfg                 *l3cfg_dev;
    NML3CfgCommitTypeHandle *l3cfg_commit_type_dev;

    struct {
        GDBusConnection *connection;
        char            *bus_name;
        char            *owner;
        guint            signal_id_vpn;
        guint            signal_id_name_changed;
        bool             name_owner_initialized : 1;
    } dbus;

    NMFirewalldManagerCallId *fw_call;

    union {
        const NML3ConfigData *const l3cds[_L3CD_TYPE_NUM];
        const NML3ConfigData       *l3cds_[_L3CD_TYPE_NUM];
    };

    /* This combines the l3cds of the VPN (basically, excluding l3cd_gw_extern which
     * is only about configuration for the parent device). This is used to configure
     * DNS. */
    const NML3ConfigData *l3cd_combined;

    union {
        struct {
            IPData ip_data_6;
            IPData ip_data_4;
        };
        IPData ip_data_x[2];
    };

    GSource           *init_fail_on_idle_source;
    GSource           *connect_timeout_source;
    GCancellable      *main_cancellable;
    GVariant          *connect_hash;
    char              *banner;
    NMPacrunnerConfId *pacrunner_conf_id;

    int ifindex_if;
    int ifindex_dev;

    guint32 mtu;

    bool wait_for_pre_up_state : 1;

    bool dbus_service_started : 1;

    bool generic_config_received : 1;

    bool l3cds_changed : 1;
} NMVpnConnectionPrivate;

struct _NMVpnConnection {
    NMActiveConnection     parent;
    NMVpnConnectionPrivate _priv;
};

struct _NMVpnConnectionClass {
    NMActiveConnectionClass parent;
};

G_DEFINE_TYPE(NMVpnConnection, nm_vpn_connection, NM_TYPE_ACTIVE_CONNECTION)

#define NM_VPN_CONNECTION_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMVpnConnection, NM_IS_VPN_CONNECTION, NMActiveConnection)

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

static const NMDBusInterfaceInfoExtended interface_info_vpn_connection;
static const GDBusSignalInfo             signal_info_vpn_state_changed;

static NMSettingsConnection *_get_settings_connection(NMVpnConnection *self,
                                                      gboolean         allow_missing);

static void _secrets_get(NMVpnConnection *self, SecretsReq secrets_idx, const char *const *hints);

static guint32 get_route_table(NMVpnConnection *self, int addr_family, gboolean fallback_main);

static void _set_vpn_state(NMVpnConnection              *self,
                           VpnState                      vpn_state,
                           NMActiveConnectionStateReason reason,
                           gboolean                      quitting);

static void
_l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMVpnConnection *self);

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

#define _NMLOG_DOMAIN      LOGD_VPN
#define _NMLOG_PREFIX_NAME "vpn"

#define __NMLOG_prefix_buf_len 128

static const char *
__LOG_create_prefix(char *buf, NMVpnConnection *self, NMSettingsConnection *con)
{
    NMVpnConnectionPrivate *priv;
    const char             *id;
    const char             *iface;
    char                    buf1[100];
    char                    buf2[100];

    if (!self)
        return _NMLOG_PREFIX_NAME;

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    id   = con ? nm_settings_connection_get_id(con) : NULL;

    iface = nm_vpn_connection_get_ip_iface(self, FALSE);

    g_snprintf(buf,
               __NMLOG_prefix_buf_len,
               "%s["
               "%p"       /*self*/
               "%s%s"     /*con-uuid*/
               "%s%s%s%s" /*con-id*/
               "%s"       /*ifindex_if*/
               "%s"       /*ifindex_dev*/
               "%s%s%s"   /*iface*/
               "]",
               _NMLOG_PREFIX_NAME,
               self,
               con ? "," : "--",
               con ? (nm_settings_connection_get_uuid(con) ?: "??") : "",
               con ? "," : "",
               NM_PRINT_FMT_QUOTED(id, "\"", id, "\"", con ? "??" : ""),
               priv->ifindex_if > 0 ? nm_sprintf_buf(buf1, ",if:%d", priv->ifindex_if) : "",
               priv->ifindex_dev > 0 ? nm_sprintf_buf(buf2, ",dev:%d", priv->ifindex_dev) : "",
               NM_PRINT_FMT_QUOTED(iface, ":(", iface, ")", ""));

    return buf;
}

#define _NMLOG(level, ...)                                                                   \
    G_STMT_START                                                                             \
    {                                                                                        \
        const NMLogLevel      _level = (level);                                              \
        NMSettingsConnection *_con   = (self) ? _get_settings_connection(self, TRUE) : NULL; \
                                                                                             \
        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                     \
            char __prefix[__NMLOG_prefix_buf_len];                                           \
                                                                                             \
            _nm_log(_level,                                                                  \
                    _NMLOG_DOMAIN,                                                           \
                    0,                                                                       \
                    (self) ? nm_vpn_connection_get_ip_iface(self, FALSE) : NULL,             \
                    (_con) ? nm_settings_connection_get_uuid(_con) : NULL,                   \
                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                               \
                    __LOG_create_prefix(__prefix, (self), _con)                              \
                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                                  \
        }                                                                                    \
    }                                                                                        \
    G_STMT_END

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

static NM_UTILS_LOOKUP_STR_DEFINE(_l3cd_type_to_string,
                                  L3CDType,
                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL),
                                  NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(),
                                  NM_UTILS_LOOKUP_STR_ITEM(L3CD_TYPE_GW_EXTERN, "gw-extern"),
                                  NM_UTILS_LOOKUP_STR_ITEM(L3CD_TYPE_STATIC, "static"),
                                  NM_UTILS_LOOKUP_STR_ITEM(L3CD_TYPE_GENERIC, "generic"),
                                  NM_UTILS_LOOKUP_STR_ITEM(L3CD_TYPE_IP_4, "ip-4"),
                                  NM_UTILS_LOOKUP_STR_ITEM(L3CD_TYPE_IP_6, "ip-6"), );

static NM_UTILS_LOOKUP_STR_DEFINE(
    _vpn_service_state_to_string,
    NMVpnServiceState,
    NM_UTILS_LOOKUP_DEFAULT(NULL),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_UNKNOWN, "unknown"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_INIT, "init"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_SHUTDOWN, "shutdown"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STARTING, "starting"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STARTED, "started"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STOPPING, "stopping"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STOPPED, "stopped"), );

#define vpn_service_state_to_string_a(state) \
    NM_UTILS_LOOKUP_STR_A(_vpn_service_state_to_string, state)

static NM_UTILS_LOOKUP_STR_DEFINE(_vpn_state_to_string,
                                  VpnState,
                                  NM_UTILS_LOOKUP_DEFAULT(NULL),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_UNKNOWN, "unknown"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_WAITING, "waiting"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_PREPARE, "prepare"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_NEED_AUTH, "need-auth"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_CONNECT, "connect"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_IP_CONFIG_GET, "ip-config-get"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_PRE_UP, "pre-up"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_ACTIVATED, "activated"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_DEACTIVATING, "deactivating"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_DISCONNECTED, "disconnected"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_FAILED, "failed"), );

#define vpn_state_to_string_a(state) NM_UTILS_LOOKUP_STR_A(_vpn_state_to_string, state)

static NM_UTILS_LOOKUP_STR_DEFINE(
    _vpn_plugin_failure_to_string,
    NMVpnPluginFailure,
    NM_UTILS_LOOKUP_DEFAULT(NULL),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED, "login-failed"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED, "connect-failed"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG, "bad-ip-config"), );

#define vpn_plugin_failure_to_string_a(failure) \
    NM_UTILS_LOOKUP_STR_A(_vpn_plugin_failure_to_string, failure)

static NMVpnConnectionState
_state_to_nm_vpn_state(VpnState state)
{
    switch (state) {
    case STATE_WAITING:
    case STATE_PREPARE:
        return NM_VPN_CONNECTION_STATE_PREPARE;
    case STATE_NEED_AUTH:
        return NM_VPN_CONNECTION_STATE_NEED_AUTH;
    case STATE_CONNECT:
        return NM_VPN_CONNECTION_STATE_CONNECT;
    case STATE_IP_CONFIG_GET:
    case STATE_PRE_UP:
        return NM_VPN_CONNECTION_STATE_IP_CONFIG_GET;
    case STATE_ACTIVATED:
        return NM_VPN_CONNECTION_STATE_ACTIVATED;
    case STATE_DEACTIVATING:
    {
        /* Map DEACTIVATING to ACTIVATED to preserve external API behavior,
         * since our API has no DEACTIVATING state of its own.  Since this can
         * take some time, and the VPN isn't actually disconnected until it
         * hits the DISCONNECTED state, to clients it should still appear
         * connected.
         */
        return NM_VPN_CONNECTION_STATE_ACTIVATED;
    }
    case STATE_DISCONNECTED:
        return NM_VPN_CONNECTION_STATE_DISCONNECTED;
    case STATE_FAILED:
        return NM_VPN_CONNECTION_STATE_FAILED;
    default:
        return NM_VPN_CONNECTION_STATE_UNKNOWN;
    }
}

static NMActiveConnectionState
_state_to_ac_state(VpnState vpn_state)
{
    /* Set the NMActiveConnection state based on VPN state */
    switch (vpn_state) {
    case STATE_WAITING:
    case STATE_PREPARE:
    case STATE_NEED_AUTH:
    case STATE_CONNECT:
    case STATE_IP_CONFIG_GET:
    case STATE_PRE_UP:
        return NM_ACTIVE_CONNECTION_STATE_ACTIVATING;
    case STATE_ACTIVATED:
        return NM_ACTIVE_CONNECTION_STATE_ACTIVATED;
    case STATE_DEACTIVATING:
        return NM_ACTIVE_CONNECTION_STATE_DEACTIVATING;
    case STATE_DISCONNECTED:
    case STATE_FAILED:
        return NM_ACTIVE_CONNECTION_STATE_DEACTIVATED;
    default:
        break;
    }
    return NM_ACTIVE_CONNECTION_STATE_UNKNOWN;
}

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

static NMSettingsConnection *
_get_settings_connection(NMVpnConnection *self, gboolean allow_missing)
{
    NMSettingsConnection *con;

    /* Currently, we operate on the assumption, that the settings-connection
     * never changes after it is set (though initially, it might be unset).
     * Later we might want to change that, but then we need fixes here too. */

    con = _nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(self));
    if (!con && !allow_missing)
        g_return_val_if_reached(NULL);
    return con;
}

static NMConnection *
_get_applied_connection(NMVpnConnection *connection)
{
    NMConnection *con;

    con = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(connection));
    g_return_val_if_fail(con, NULL);
    return con;
}

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

static void
_dbus_connection_call(NMVpnConnection    *self,
                      const char         *method_name,
                      GVariant           *parameters,
                      const GVariantType *reply_type,
                      GAsyncReadyCallback callback)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    g_return_if_fail(priv->dbus.bus_name);

    _LOGT("dbus: call %s on %s", method_name, priv->dbus.bus_name);
    g_dbus_connection_call(priv->dbus.connection,
                           priv->dbus.bus_name,
                           NM_VPN_DBUS_PLUGIN_PATH,
                           NM_VPN_DBUS_PLUGIN_INTERFACE,
                           method_name,
                           parameters,
                           reply_type,
                           G_DBUS_CALL_FLAGS_NONE,
                           DBUS_DEFAULT_TIMEOUT_MSEC,
                           priv->main_cancellable,
                           callback,
                           self);
}

static NML3ConfigMergeFlags
_l3cfg_get_merge_flags(NMVpnConnection *self, L3CDType l3cd_type)
{
    NMConnection        *applied;
    NMSettingIPConfig   *s_ip4;
    NMSettingIPConfig   *s_ip6;
    NML3ConfigMergeFlags merge_flags;

    merge_flags = NM_L3_CONFIG_MERGE_FLAGS_NONE;

    if (l3cd_type == L3CD_TYPE_IP_4) {
        applied = _get_applied_connection(self);
        s_ip4   = applied ? nm_connection_get_setting_ip_config(applied, AF_INET) : NULL;

        if (s_ip4 && nm_setting_ip_config_get_ignore_auto_routes(s_ip4))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_ROUTES;
        if (s_ip4 && nm_setting_ip_config_get_never_default(s_ip4))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_DEFAULT_ROUTES;
        if (s_ip4 && nm_setting_ip_config_get_ignore_auto_dns(s_ip4))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_DNS;
    } else if (l3cd_type == L3CD_TYPE_IP_6) {
        applied = _get_applied_connection(self);
        s_ip6   = applied ? nm_connection_get_setting_ip_config(applied, AF_INET6) : NULL;

        if (s_ip6 && nm_setting_ip_config_get_ignore_auto_routes(s_ip6))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_ROUTES;
        if (s_ip6 && nm_setting_ip_config_get_never_default(s_ip6))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_DEFAULT_ROUTES;
        if (s_ip6 && nm_setting_ip_config_get_ignore_auto_dns(s_ip6))
            merge_flags |= NM_L3_CONFIG_MERGE_FLAGS_NO_DNS;
    }

    return merge_flags;
}

static NML3ConfigData *
_l3cfg_l3cd_new(NMVpnConnection *self, int ifindex)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    return nm_l3_config_data_new(nm_netns_get_multi_idx(priv->netns),
                                 ifindex,
                                 NM_IP_CONFIG_SOURCE_VPN);
}

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

guint32
nm_vpn_connection_get_ip4_internal_gateway(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->ip_data_4.gw_internal.addr4;
}

struct in6_addr *
nm_vpn_connection_get_ip6_internal_gateway(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    return &NM_VPN_CONNECTION_GET_PRIVATE(self)->ip_data_6.gw_internal.addr6;
}

NMVpnConnectionState
nm_vpn_connection_get_vpn_state(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NM_VPN_CONNECTION_STATE_UNKNOWN);

    return _state_to_nm_vpn_state(NM_VPN_CONNECTION_GET_PRIVATE(self)->vpn_state);
}

const char *
nm_vpn_connection_get_banner(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->banner;
}

const NML3ConfigData *
nm_vpn_connection_get_l3cd(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->l3cds_changed) {
        nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
        L3CDType                                l3cd_type;
        int                                     ifindex;

        priv->l3cds_changed = FALSE;

        ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);

        if (ifindex > 0) {
            const int default_dns_priority_x[2] = {
                NM_DNS_PRIORITY_DEFAULT_VPN,
                NM_DNS_PRIORITY_DEFAULT_VPN,
            };
            guint32 default_route_table_x[2];
            guint32 default_route_metric_x[2];

            for (l3cd_type = 0; l3cd_type < _L3CD_TYPE_NUM; l3cd_type++) {
                if (l3cd_type == L3CD_TYPE_GW_EXTERN)
                    continue;
                if (!priv->l3cds[l3cd_type])
                    continue;

                if (!l3cd) {
                    default_route_table_x[0] = get_route_table(self, AF_INET6, TRUE);
                    default_route_table_x[1] = get_route_table(self, AF_INET, TRUE);
                    default_route_metric_x[0] =
                        nm_vpn_connection_get_ip_route_metric(self, AF_INET6);
                    default_route_metric_x[1] =
                        nm_vpn_connection_get_ip_route_metric(self, AF_INET);
                    l3cd = _l3cfg_l3cd_new(self, ifindex);
                }

                nm_l3_config_data_merge(l3cd,
                                        priv->l3cds[l3cd_type],
                                        _l3cfg_get_merge_flags(self, l3cd_type),
                                        default_route_table_x,
                                        default_route_metric_x,
                                        NULL,
                                        default_dns_priority_x,
                                        NULL,
                                        NULL);
            }
        }

        nm_l3_config_data_reset(&priv->l3cd_combined, l3cd);
    }

    return priv->l3cd_combined;
}

static int
_get_ifindex_for_device(NMVpnConnection *self)
{
    NMDevice *parent_dev;
    int       ifindex;

    nm_assert(NM_IS_VPN_CONNECTION(self));

    parent_dev = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    if (!parent_dev)
        return 0;
    ifindex = nm_device_get_ip_ifindex(parent_dev);
    if (ifindex <= 0)
        return 0;

    return ifindex;
}

const char *
nm_vpn_connection_get_ip_iface(NMVpnConnection *self, gboolean fallback_device)
{
    NMVpnConnectionPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->l3cfg_if)
        return nm_l3cfg_get_ifname(priv->l3cfg_if, TRUE);
    if (fallback_device && priv->l3cfg_dev)
        return nm_l3cfg_get_ifname(priv->l3cfg_dev, TRUE);
    return NULL;
}

int
nm_vpn_connection_get_ip_ifindex(NMVpnConnection *self, gboolean fallback_device)
{
    NMVpnConnectionPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->ifindex_if > 0)
        return priv->ifindex_if;
    if (fallback_device && priv->ifindex_dev > 0)
        return priv->ifindex_dev;
    return 0;
}

static guint32
_get_vpn_timeout(NMVpnConnection *self)
{
    guint32       timeout;
    NMSettingVpn *s_vpn;

    s_vpn = nm_connection_get_setting_vpn(_get_applied_connection(self));
    g_return_val_if_fail(s_vpn, 60);

    /* Timeout waiting for IP config signal from VPN service
     * It is a configured value or 60 seconds */
    timeout = nm_setting_vpn_get_timeout(s_vpn);
    if (timeout == 0) {
        timeout = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
                                                              NM_CON_DEFAULT("vpn.timeout"),
                                                              NULL,
                                                              1,
                                                              G_MAXUINT32,
                                                              60);
    }
    return timeout;
}

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

static gboolean
_l3cfg_l3cd_set(NMVpnConnection *self, L3CDType l3cd_type, const NML3ConfigData *l3cd)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (nm_l3_config_data_equal(l3cd, priv->l3cds[l3cd_type]))
        return FALSE;

    if (_LOGT_ENABLED()) {
        if (l3cd) {
            char s_name[150];

            /* Seal hear, so that we don't log about an unsealed instance.
             * nm_l3_config_data_reset() anyway seals the instance too. */
            nm_l3_config_data_seal(l3cd);

            _LOGT("l3cd[%s]: set " NM_HASH_OBFUSCATE_PTR_FMT,
                  _l3cd_type_to_string(l3cd_type),
                  NM_HASH_OBFUSCATE_PTR(l3cd));
            nm_l3_config_data_log(
                l3cd,
                nm_sprintf_buf(s_name, "l3cd[%s]", _l3cd_type_to_string(l3cd_type)),
                "vpn-config: ",
                LOGL_TRACE,
                _NMLOG_DOMAIN);
        } else
            _LOGT("l3cd[%s]: unset", _l3cd_type_to_string(l3cd_type));
    }

    nm_l3_config_data_reset(&priv->l3cds_[l3cd_type], l3cd);
    priv->l3cds_changed = TRUE;
    return TRUE;
}

static void
_l3cfg_l3cd_update(NMVpnConnection *self, L3CDType l3cd_type)
{
    NMVpnConnectionPrivate      *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NML3Cfg                     *l3cfg;
    const NML3ConfigData *const *p_l3cd;

    if (NM_IN_SET(l3cd_type, L3CD_TYPE_IP_4, L3CD_TYPE_IP_6, L3CD_TYPE_GENERIC, L3CD_TYPE_STATIC)) {
        l3cfg = priv->l3cfg_if;
        if (!l3cfg) {
            l3cfg = priv->l3cfg_dev;
        }
    } else {
        nm_assert(NM_IN_SET(l3cd_type, L3CD_TYPE_GW_EXTERN));
        l3cfg = priv->l3cfg_dev;
    }

    p_l3cd = &priv->l3cds[l3cd_type];

    if (!l3cfg)
        return;

    if (!*p_l3cd) {
        if (!nm_l3cfg_remove_config_all(l3cfg, p_l3cd))
            return;
        _LOGT("l3cd[%s]: remove-config " NM_HASH_OBFUSCATE_PTR_FMT,
              _l3cd_type_to_string(l3cd_type),
              NM_HASH_OBFUSCATE_PTR(*p_l3cd));
        goto handle_changed;
    }

    if (!nm_l3cfg_add_config(l3cfg,
                             p_l3cd,
                             TRUE,
                             *p_l3cd,
                             NM_L3CFG_CONFIG_PRIORITY_VPN,
                             get_route_table(self, AF_INET, TRUE),
                             get_route_table(self, AF_INET6, TRUE),
                             nm_vpn_connection_get_ip_route_metric(self, AF_INET),
                             nm_vpn_connection_get_ip_route_metric(self, AF_INET6),
                             0,
                             0,
                             NM_DNS_PRIORITY_DEFAULT_VPN,
                             NM_DNS_PRIORITY_DEFAULT_VPN,
                             NM_L3_ACD_DEFEND_TYPE_ONCE,
                             0,
                             NM_L3CFG_CONFIG_FLAGS_NONE,
                             _l3cfg_get_merge_flags(self, l3cd_type)))
        return;

    _LOGT("l3cd[%s]: add-config " NM_HASH_OBFUSCATE_PTR_FMT,
          _l3cd_type_to_string(l3cd_type),
          NM_HASH_OBFUSCATE_PTR(*p_l3cd));

handle_changed:
    nm_l3cfg_commit_on_idle_schedule(l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
}

static void
_l3cfg_l3cd_update_all(NMVpnConnection *self)
{
    L3CDType l3cd_type;

    for (l3cd_type = 0; l3cd_type < _L3CD_TYPE_NUM; l3cd_type++)
        _l3cfg_l3cd_update(self, l3cd_type);
}

static void
_l3cfg_l3cd_clear_all(NMVpnConnection *self)
{
    L3CDType l3cd_type;

    for (l3cd_type = 0; l3cd_type < _L3CD_TYPE_NUM; l3cd_type++)
        _l3cfg_l3cd_set(self, l3cd_type, NULL);

    _l3cfg_l3cd_update_all(self);
}

static void
_l3cfg_clear(NMVpnConnection *self, NML3Cfg *l3cfg)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    L3CDType                l3cd_type;
    gboolean                changed = FALSE;

    if (!l3cfg)
        return;

    g_signal_handlers_disconnect_by_func(l3cfg, G_CALLBACK(_l3cfg_notify_cb), self);

    for (l3cd_type = 0; l3cd_type < _L3CD_TYPE_NUM; l3cd_type++) {
        if (nm_l3cfg_remove_config_all(l3cfg, &priv->l3cds[l3cd_type]))
            changed = TRUE;
    }

    if (changed)
        nm_l3cfg_commit_on_idle_schedule(l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
}

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

static void
cancel_get_secrets(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->secrets_id) {
        _LOGT("secrets: cancel request");
        nm_settings_connection_cancel_secrets(_get_settings_connection(self, FALSE),
                                              priv->secrets_id);
        nm_assert(!priv->secrets_id);
    }
}

static void
_l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT) {
        if (l3cfg == (priv->l3cfg_if ?: priv->l3cfg_dev) && priv->wait_for_pre_up_state
            && priv->vpn_state < STATE_PRE_UP)
            _set_vpn_state(self, STATE_PRE_UP, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
    }
}

static gboolean
_set_ip_ifindex(NMVpnConnection *self, int ifindex, gboolean is_if)
{
    NMVpnConnectionPrivate   *priv      = NM_VPN_CONNECTION_GET_PRIVATE(self);
    int                      *p_ifindex = is_if ? &priv->ifindex_if : &priv->ifindex_dev;
    NML3Cfg                 **p_l3cfg   = is_if ? &priv->l3cfg_if : &priv->l3cfg_dev;
    NML3CfgCommitTypeHandle **p_l3cfg_commit_type =
        is_if ? &priv->l3cfg_commit_type_if : &priv->l3cfg_commit_type_dev;
    gs_unref_object NML3Cfg *l3cfg_old = NULL;

    if (ifindex < 0)
        ifindex = nm_assert_unreachable_val(0);

    if (*p_ifindex == ifindex)
        return FALSE;

    _LOGD("set ip-ifindex-%s %d", is_if ? "if" : "dev", ifindex);

    *p_ifindex = ifindex;

    l3cfg_old = g_steal_pointer(p_l3cfg);
    nm_l3cfg_commit_type_clear(l3cfg_old, p_l3cfg_commit_type);
    _l3cfg_clear(self, l3cfg_old);

    if (ifindex > 0) {
        *p_l3cfg = nm_netns_l3cfg_acquire(priv->netns, ifindex);
        g_signal_connect(*p_l3cfg, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(_l3cfg_notify_cb), self);
        *p_l3cfg_commit_type =
            nm_l3cfg_commit_type_register(*p_l3cfg, NM_L3_CFG_COMMIT_TYPE_UPDATE, NULL, "vpn");
    }

    return TRUE;
}

static void
disconnect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection           *self;
    gs_unref_variant GVariant *res   = NULL;
    gs_free_error GError      *error = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_VPN_CONNECTION(user_data);

    _LOGT("dbus: disconnected%s%s",
          NM_PRINT_FMT_QUOTED2(error, " failed: ", error->message, " with success"));
}

static void
fw_call_cleanup(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->fw_call) {
        nm_firewalld_manager_cancel_call(priv->fw_call);
        g_warn_if_fail(!priv->fw_call);
        priv->fw_call = NULL;
    }
}

static void
vpn_cleanup(NMVpnConnection *self, NMDevice *parent_dev)
{
    const char *iface;

    /* Remove zone from firewall */
    iface = nm_vpn_connection_get_ip_iface(self, FALSE);
    if (iface) {
        nm_firewalld_manager_remove_from_zone(nm_firewalld_manager_get(), iface, NULL, NULL, NULL);
    }

    /* Cancel pending firewall call */
    fw_call_cleanup(self);

    _l3cfg_l3cd_clear_all(self);
}

static void
dispatcher_pre_down_done(NMDispatcherCallId *call_id, gpointer user_data)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_assert(call_id);
    nm_assert(priv->dispatcher_id == call_id);

    priv->dispatcher_id = NULL;
    _set_vpn_state(self,
                   STATE_DISCONNECTED,
                   NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED,
                   FALSE);
}

static void
dispatcher_pre_up_done(NMDispatcherCallId *call_id, gpointer user_data)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_assert(call_id);
    nm_assert(priv->dispatcher_id == call_id);

    priv->dispatcher_id = NULL;
    _set_vpn_state(self, STATE_ACTIVATED, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
dispatcher_cleanup(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->dispatcher_id)
        nm_dispatcher_call_cancel(g_steal_pointer(&priv->dispatcher_id));
}

static void
_set_vpn_state(NMVpnConnection              *self,
               VpnState                      vpn_state,
               NMActiveConnectionStateReason reason,
               gboolean                      quitting)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    VpnState                old_vpn_state;
    NMVpnConnectionState    new_external_state;
    NMVpnConnectionState    old_external_state;
    NMDevice     *parent_dev = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    NMConnection *applied;

    if (vpn_state == priv->vpn_state)
        return;

    old_vpn_state   = priv->vpn_state;
    priv->vpn_state = vpn_state;

    _LOGD("set state: %s (was %s)",
          vpn_state_to_string_a(priv->vpn_state),
          vpn_state_to_string_a(old_vpn_state));

    /* The device gets destroyed by active connection when it enters
     * the deactivated state, so we need to ref it for usage below.
     */
    nm_g_object_ref(parent_dev);

    /* Update active connection base class state */
    nm_active_connection_set_state(NM_ACTIVE_CONNECTION(self),
                                   _state_to_ac_state(vpn_state),
                                   reason);

    /* Clear any in-progress secrets request */
    cancel_get_secrets(self);

    dispatcher_cleanup(self);

    /* The connection gets destroyed by the VPN manager when it enters the
     * disconnected/failed state, but we need to keep it around for a bit
     * to send out signals and handle the dispatcher.  So ref it.
     */
    g_object_ref(self);

    old_external_state = _state_to_nm_vpn_state(old_vpn_state);
    new_external_state = _state_to_nm_vpn_state(priv->vpn_state);
    if (new_external_state != old_external_state) {
        nm_dbus_object_emit_signal(NM_DBUS_OBJECT(self),
                                   &interface_info_vpn_connection,
                                   &signal_info_vpn_state_changed,
                                   "(uu)",
                                   (guint32) new_external_state,
                                   (guint32) reason);
        g_signal_emit(self,
                      signals[INTERNAL_STATE_CHANGED],
                      0,
                      (guint) new_external_state,
                      (guint) old_external_state,
                      (guint) reason);
        _notify(self, PROP_VPN_STATE);
    }

    switch (vpn_state) {
    case STATE_NEED_AUTH:
        /* Do nothing; not part of 'default' because we don't want to touch
         * priv->secrets_req as NEED_AUTH is re-entered during interactive
         * secrets.
         */
        break;
    case STATE_PRE_UP:
        if (!nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_PRE_UP,
                                    _get_settings_connection(self, FALSE),
                                    _get_applied_connection(self),
                                    parent_dev,
                                    nm_vpn_connection_get_ip_iface(self, FALSE),
                                    nm_vpn_connection_get_l3cd(self),
                                    dispatcher_pre_up_done,
                                    self,
                                    &priv->dispatcher_id)) {
            /* Just proceed on errors */
            dispatcher_pre_up_done(0, self);
        }
        break;
    case STATE_ACTIVATED:

        nm_clear_g_source_inst(&priv->start_timeout_source);

        applied = _get_applied_connection(self);

        /* Secrets no longer needed now that we're connected */
        nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(self));

        /* Let dispatcher scripts know we're up and running */
        nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_UP,
                               _get_settings_connection(self, FALSE),
                               applied,
                               parent_dev,
                               nm_vpn_connection_get_ip_iface(self, FALSE),
                               nm_vpn_connection_get_l3cd(self),
                               NULL,
                               NULL,
                               NULL);
        break;
    case STATE_DEACTIVATING:
        applied = _get_applied_connection(self);
        if (quitting) {
            nm_dispatcher_call_vpn_sync(NM_DISPATCHER_ACTION_VPN_PRE_DOWN,
                                        _get_settings_connection(self, FALSE),
                                        applied,
                                        parent_dev,
                                        nm_vpn_connection_get_ip_iface(self, FALSE),
                                        nm_vpn_connection_get_l3cd(self));
        } else {
            if (!nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_PRE_DOWN,
                                        _get_settings_connection(self, FALSE),
                                        applied,
                                        parent_dev,
                                        nm_vpn_connection_get_ip_iface(self, FALSE),
                                        nm_vpn_connection_get_l3cd(self),
                                        dispatcher_pre_down_done,
                                        self,
                                        &priv->dispatcher_id)) {
                /* Just proceed on errors */
                dispatcher_pre_down_done(0, self);
            }
        }

        nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
        break;
    case STATE_FAILED:
    case STATE_DISCONNECTED:
        if (old_vpn_state >= STATE_ACTIVATED && old_vpn_state <= STATE_DEACTIVATING) {
            /* Let dispatcher scripts know we're about to go down */
            if (quitting) {
                nm_dispatcher_call_vpn_sync(NM_DISPATCHER_ACTION_VPN_DOWN,
                                            _get_settings_connection(self, FALSE),
                                            _get_applied_connection(self),
                                            parent_dev,
                                            nm_vpn_connection_get_ip_iface(self, FALSE),
                                            NULL);
            } else {
                nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_DOWN,
                                       _get_settings_connection(self, FALSE),
                                       _get_applied_connection(self),
                                       parent_dev,
                                       nm_vpn_connection_get_ip_iface(self, FALSE),
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL);
            }
        }

        if (priv->dbus.bus_name)
            _dbus_connection_call(self, "Disconnect", NULL, G_VARIANT_TYPE("()"), disconnect_cb);

        vpn_cleanup(self, parent_dev);
        /* fall-through */
    default:
        priv->secrets_idx = SECRETS_REQ_SYSTEM;
        break;
    }

    g_object_unref(self);
    if (parent_dev)
        g_object_unref(parent_dev);
}

static gboolean
_service_and_connection_can_persist(NMVpnConnection *self)
{
    return NM_VPN_CONNECTION_GET_PRIVATE(self)->connection_can_persist
           && NM_VPN_CONNECTION_GET_PRIVATE(self)->service_can_persist;
}

static gboolean
_connection_only_can_persist(NMVpnConnection *self)
{
    return NM_VPN_CONNECTION_GET_PRIVATE(self)->connection_can_persist
           && !NM_VPN_CONNECTION_GET_PRIVATE(self)->service_can_persist;
}

static void
device_state_changed(NMActiveConnection *active,
                     NMDevice           *device,
                     NMDeviceState       new_state,
                     NMDeviceState       old_state,
                     NMDeviceStateReason reason)
{
    if (_service_and_connection_can_persist(NM_VPN_CONNECTION(active))) {
        if (new_state <= NM_DEVICE_STATE_DISCONNECTED || new_state == NM_DEVICE_STATE_FAILED) {
            nm_active_connection_set_device(active, NULL);
        }
        return;
    }

    if (new_state <= NM_DEVICE_STATE_DISCONNECTED) {
        _set_vpn_state(NM_VPN_CONNECTION(active),
                       STATE_DISCONNECTED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED,
                       FALSE);
    } else if (new_state == NM_DEVICE_STATE_FAILED) {
        _set_vpn_state(NM_VPN_CONNECTION(active),
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED,
                       FALSE);
    }

    /* FIXME: map device DEACTIVATING state to VPN DEACTIVATING state and
     * block device deactivation on VPN deactivation.
     */
}

static gboolean
_parent_device_l3cd_add_gateway_route(NML3ConfigData *l3cd,
                                      int             addr_family,
                                      NMDevice       *parent_device,
                                      const NMIPAddr *vpn_gw,
                                      NMPlatform     *platform)
{
    const int                       IS_IPv4        = NM_IS_IPv4(addr_family);
    NMIPAddr                        parent_gw      = NM_IP_ADDR_INIT;
    gboolean                        has_parent_gw  = FALSE;
    nm_auto_nmpobj const NMPObject *route_resolved = NULL;
    int                             ifindex;
    NMPlatformIPXRoute              route;
    int                             r;

    nm_assert(NM_IS_L3_CONFIG_DATA(l3cd));
    g_return_val_if_fail(vpn_gw, FALSE);

    if (nm_ip_addr_is_null(addr_family, vpn_gw))
        return FALSE;

    ifindex = nm_l3_config_data_get_ifindex(l3cd);

    nm_assert(ifindex > 0);

    if (parent_device && ifindex != nm_device_get_ip_ifindex(parent_device))
        parent_device = 0;

    /* Ask kernel how to reach @vpn_gw. We can only inject the route in
     * @parent_device, so whatever we resolve, it can only be on @ifindex. */
    r = nm_platform_ip_route_get(platform,
                                 addr_family,
                                 vpn_gw,
                                 ifindex,
                                 (NMPObject **) &route_resolved);
    if (r >= 0) {
        const NMPlatformIPXRoute *rx = NMP_OBJECT_CAST_IPX_ROUTE(route_resolved);
        const NMPObject          *obj;

        if (rx->rx.ifindex == ifindex && nm_platform_route_table_is_main(rx->rx.table_coerced)) {
            gconstpointer gw = nm_platform_ip_route_get_gateway(addr_family, &rx->rx);

            /* `ip route get` always resolves the route, even if the destination is unreachable.
             * In which case, it pretends the destination is directly reachable.
             *
             * So, only accept direct routes if @vpn_gw is a private network
             * or if the parent device also has a direct default route */
            if (!nm_ip_addr_is_null(addr_family, gw)) {
                nm_ip_addr_set(addr_family, &parent_gw, gw);
                has_parent_gw = TRUE;
            } else if (nm_ip_addr_is_site_local(addr_family, vpn_gw))
                has_parent_gw = TRUE;
            else if ((obj = nm_device_get_best_default_route(parent_device, addr_family))
                     && nm_ip_addr_is_null(
                         addr_family,
                         nm_platform_ip_route_get_gateway(addr_family,
                                                          NMP_OBJECT_CAST_IP_ROUTE(obj))))
                has_parent_gw = TRUE;
        }
    }

    if (!has_parent_gw)
        return FALSE;

    if (IS_IPv4) {
        route.r4 = (NMPlatformIP4Route){
            .ifindex    = ifindex,
            .network    = vpn_gw->addr4,
            .plen       = 32,
            .gateway    = parent_gw.addr4,
            .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
            .metric_any = TRUE,
        };
    } else {
        route.r6 = (NMPlatformIP6Route){
            .ifindex    = ifindex,
            .network    = vpn_gw->addr6,
            .plen       = 128,
            .gateway    = parent_gw.addr6,
            .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
            .metric_any = TRUE,
        };
    }
    nm_l3_config_data_add_route(l3cd, addr_family, NULL, &route.rx);

    if (!nm_ip_addr_is_null(addr_family, &parent_gw)) {
        /* Ensure there's a route to the parent device's gateway through the
         * parent device, since if the VPN claims the default route and the VPN
         * routes include a subnet that matches the parent device's subnet,
         * the parent device's gateway would get routed through the VPN and fail.
         */
        if (IS_IPv4) {
            route.r4 = (NMPlatformIP4Route){
                .network    = parent_gw.addr4,
                .plen       = 32,
                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
                .metric_any = TRUE,
            };
        } else {
            route.r6 = (NMPlatformIP6Route){
                .network    = parent_gw.addr6,
                .plen       = 128,
                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
                .metric_any = TRUE,
            };
        }
        nm_l3_config_data_add_route(l3cd, addr_family, NULL, &route.rx);
    }

    return TRUE;
}

static gboolean
_l3cfg_l3cd_gw_extern_update(NMVpnConnection *self)
{
    NMVpnConnectionPrivate                 *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
    int                                     ifindex;
    gboolean                                changed;
    int                                     IS_IPv4;

    ifindex = priv->ifindex_dev;
    if (ifindex <= 0)
        goto set;

    l3cd = _l3cfg_l3cd_new(self, ifindex);

    changed = FALSE;
    for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
        const int          addr_family = IS_IPv4 ? AF_INET : AF_INET6;
        NMSettingIPConfig *s_ip;

        s_ip = nm_connection_get_setting_ip_config(_get_applied_connection(self), addr_family);
        if (s_ip && nm_setting_ip_config_get_auto_route_ext_gw(s_ip) == NM_TERNARY_FALSE) {
            _LOGD("IPv%c route to the external gateway have been deactivated via auto-route-ext-gw "
                  "setting",
                  nm_utils_addr_family_to_char(addr_family));
            continue;
        }

        if (_parent_device_l3cd_add_gateway_route(
                l3cd,
                IS_IPv4 ? AF_INET : AF_INET6,
                nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self)),
                &priv->ip_data_x[IS_IPv4].gw_external,
                nm_netns_get_platform(priv->netns)))
            changed = TRUE;
    }
    if (!changed)
        nm_clear_pointer(&l3cd, nm_l3_config_data_unref);

set:
    if (!_l3cfg_l3cd_set(self, L3CD_TYPE_GW_EXTERN, l3cd))
        return FALSE;

    return TRUE;
}

const char *
nm_vpn_connection_get_service(NMVpnConnection *self)
{
    NMSettingVpn *s_vpn;

    s_vpn = nm_connection_get_setting_vpn(_get_applied_connection(self));
    return nm_setting_vpn_get_service_type(s_vpn);
}

static void
_apply_config(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    _LOGT("apply-config");

    if (priv->ifindex_if > 0) {
        nm_platform_link_change_flags(nm_netns_get_platform(priv->netns),
                                      priv->ifindex_if,
                                      IFF_UP,
                                      TRUE);
    }

    if (priv->ifindex_dev > 0) {
        nm_platform_link_change_flags(nm_netns_get_platform(priv->netns),
                                      priv->ifindex_dev,
                                      IFF_UP,
                                      TRUE);
    }

    if (priv->ifindex_if > 0 && priv->ifindex_if != priv->ifindex_dev) {
        if (priv->mtu
            && priv->mtu
                   != nm_platform_link_get_mtu(nm_netns_get_platform(priv->netns),
                                               priv->ifindex_if))
            nm_platform_link_set_mtu(nm_netns_get_platform(priv->netns),
                                     priv->ifindex_if,
                                     priv->mtu);
    }

    priv->wait_for_pre_up_state = TRUE;

    _l3cfg_l3cd_update_all(self);
}

static void
fw_change_zone_cb(NMFirewalldManager       *firewalld_manager,
                  NMFirewalldManagerCallId *call_id,
                  GError                   *error,
                  gpointer                  user_data)
{
    NMVpnConnection        *self = user_data;
    NMVpnConnectionPrivate *priv;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(priv->fw_call == call_id);

    priv->fw_call = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    _apply_config(self);
}

static void
_check_complete(NMVpnConnection *self, gboolean success)
{
    NMVpnConnectionPrivate                 *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
    NMConnection                           *connection;
    NMSettingConnection                    *s_con;
    const char                             *zone;
    const char                             *iface;

    if (priv->vpn_state < STATE_IP_CONFIG_GET || priv->vpn_state > STATE_ACTIVATED)
        return;

    if (success) {
        if (!priv->generic_config_received
            || (priv->ip_data_4.enabled && !priv->l3cds[L3CD_TYPE_IP_4])
            || (priv->ip_data_6.enabled && !priv->l3cds[L3CD_TYPE_IP_6])) {
            /* Need to wait more config. */
            return;
        }
    }

    nm_clear_g_source_inst(&priv->connect_timeout_source);

    if (!success) {
        _LOGW("did not receive valid IP config information");
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID,
                       FALSE);
        return;
    }

    connection = _get_applied_connection(self);

    l3cd = nm_l3_config_data_new_from_connection(nm_netns_get_multi_idx(priv->netns),
                                                 nm_vpn_connection_get_ip_ifindex(self, TRUE),
                                                 connection);

    nm_l3_config_data_set_allow_routes_without_address(l3cd, AF_INET, TRUE);
    nm_l3_config_data_set_allow_routes_without_address(l3cd, AF_INET6, TRUE);

    _l3cfg_l3cd_set(self, L3CD_TYPE_STATIC, l3cd);

    _l3cfg_l3cd_gw_extern_update(self);

    iface = nm_vpn_connection_get_ip_iface(self, FALSE);

    /* Add the tunnel interface to the specified firewall zone */
    if (iface) {
        s_con = nm_connection_get_setting_connection(connection);
        zone  = nm_setting_connection_get_zone(s_con);

        fw_call_cleanup(self);
        priv->fw_call = nm_firewalld_manager_add_or_change_zone(nm_firewalld_manager_get(),
                                                                iface,
                                                                zone,
                                                                FALSE,
                                                                fw_change_zone_cb,
                                                                self);
        return;
    }

    _apply_config(self);
}

static gboolean
_vardict_to_addr(int addr_family, GVariant *dict, const char *key, gpointer dst)
{
    guint32 u32;

    if (!NM_IS_IPv4(addr_family)) {
        gs_unref_variant GVariant *v = NULL;

        if (g_variant_lookup(dict, key, "@ay", &v)) {
            if (nm_ip_addr_set_from_variant(AF_INET6, dst, v, NULL))
                return TRUE;
        }
        nm_ip_addr_set(AF_INET6, dst, &nm_ip_addr_zero.addr6);
        return FALSE;
    }

    /* The way we encode IPv4 addresses is not endianness safe. It works well enough
     * on the same host and as we know that the VPN plugin sends the address in the
     * same endianness that we expect.
     *
     * But we read a u32 (natively), and that happens to be already in the right
     * endianness to be used directly as IPv4 address. */
    if (g_variant_lookup(dict, key, "u", &u32)) {
        unaligned_write_ne32(dst, u32);
        return TRUE;
    }
    unaligned_write_ne32(dst, 0);
    return FALSE;
}

guint32
nm_vpn_connection_get_ip_route_metric(NMVpnConnection *self, int addr_family)
{
    gint64        route_metric = -1;
    NMConnection *applied;

    applied = _get_applied_connection(self);
    if (!applied)
        g_return_val_if_reached(NM_VPN_ROUTE_METRIC_DEFAULT);

    route_metric = nm_setting_ip_config_get_route_metric(
        nm_connection_get_setting_ip_config(applied, addr_family));
    return (route_metric >= 0) ? route_metric : NM_VPN_ROUTE_METRIC_DEFAULT;
}

static guint32
get_route_table(NMVpnConnection *self, int addr_family, gboolean fallback_main)
{
    NMConnection      *connection;
    NMSettingIPConfig *s_ip;
    guint32            route_table = 0;

    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));

    connection = _get_applied_connection(self);
    if (connection) {
        s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
        if (s_ip)
            route_table = nm_setting_ip_config_get_route_table(s_ip);
    }

    return route_table ?: (fallback_main ? RT_TABLE_MAIN : 0);
}

static gboolean
connect_timeout_cb(gpointer user_data)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_clear_g_source_inst(&priv->connect_timeout_source);

    /* Cancel activation if it's taken too long */
    if (priv->vpn_state == STATE_CONNECT || priv->vpn_state == STATE_IP_CONFIG_GET) {
        _LOGW("connect timeout exceeded");
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT,
                       FALSE);
    }

    return G_SOURCE_CONTINUE;
}

static void
connect_success(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    priv->connect_timeout_source =
        nm_g_timeout_add_seconds_source(_get_vpn_timeout(self), connect_timeout_cb, self);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);
}

static void
connect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection           *self;
    gs_unref_variant GVariant *res   = NULL;
    gs_free_error GError      *error = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_VPN_CONNECTION(user_data);

    if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGW("failed to connect: '%s'", error->message);
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                       FALSE);
    } else
        connect_success(self);
}

static void
connect_interactive_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection           *self;
    NMVpnConnectionPrivate    *priv;
    gs_unref_variant GVariant *res   = NULL;
    gs_free_error GError      *error = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_VPN_CONNECTION(user_data);
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (g_error_matches(error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED)
        && priv->dbus.bus_name) {
        _LOGD("connect: falling back to non-interactive connect");
        _dbus_connection_call(self,
                              "Connect",
                              g_variant_new("(@a{sa{sv}})", priv->connect_hash),
                              G_VARIANT_TYPE("()"),
                              connect_cb);
        return;
    }

    if (error) {
        _LOGW("connect: failed to connect interactively: '%s'", error->message);
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                       FALSE);
        return;
    }

    _LOGD("connect: success from ConnectInteractive");
    connect_success(self);
}

/* Add a username to a hashed connection */
static GVariant *
_hash_with_username(NMConnection *connection, const char *username)
{
    gs_unref_object NMConnection *dup = NULL;
    NMSettingVpn                 *s_vpn;

    /* Shortcut if we weren't given a username or if there already was one in
     * the VPN setting; don't bother duplicating the connection and everything.
     */
    s_vpn = nm_connection_get_setting_vpn(connection);
    g_return_val_if_fail(s_vpn, NULL);

    if (!username || nm_setting_vpn_get_user_name(s_vpn))
        return nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL);

    dup = nm_simple_connection_new_clone(connection);
    nm_assert(dup);

    s_vpn = nm_connection_get_setting_vpn(dup);
    g_return_val_if_fail(s_vpn, NULL);

    g_object_set(s_vpn, NM_SETTING_VPN_USER_NAME, username, NULL);

    return nm_connection_to_dbus(dup, NM_CONNECTION_SERIALIZE_ALL);
}

static void
really_activate(NMVpnConnection *self, const char *username)
{
    NMVpnConnectionPrivate *priv;
    GVariantBuilder         details;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(priv->vpn_state == STATE_NEED_AUTH);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);
    priv->connect_hash = _hash_with_username(_get_applied_connection(self), username);
    g_variant_ref_sink(priv->connect_hash);

    if (!priv->dbus.bus_name) {
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED,
                       FALSE);
        return;
    }

    /* If at least one agent doesn't support VPN hints, then we can't use
     * ConnectInteractive(), because that agent won't be able to pass hints
     * from the VPN plugin's interactive secrets requests to the VPN authentication
     * dialog and we won't get the secrets we need.  In this case fall back to
     * the old Connect() call.
     */
    if (nm_agent_manager_all_agents_have_capability(
            nm_agent_manager_get(),
            nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(self)),
            NM_SECRET_AGENT_CAPABILITY_VPN_HINTS)) {
        _LOGD("connect: allowing interactive secrets as all agents have that capability");
        g_variant_builder_init(&details, G_VARIANT_TYPE_VARDICT);
        _dbus_connection_call(self,
                              "ConnectInteractive",
                              g_variant_new("(@a{sa{sv}}a{sv})", priv->connect_hash, &details),
                              G_VARIANT_TYPE("()"),
                              connect_interactive_cb);
    } else {
        _LOGD(
            "connect: calling old Connect function as not all agents support interactive secrets");
        _dbus_connection_call(self,
                              "Connect",
                              g_variant_new("(@a{sa{sv}})", priv->connect_hash),
                              G_VARIANT_TYPE("()"),
                              connect_cb);
    }

    _set_vpn_state(self, STATE_CONNECT, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
_dbus_signal_failure_cb(NMVpnConnection *self, guint32 reason)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    _LOGW("dbus: failure: %s (%d)", vpn_plugin_failure_to_string_a(reason), reason);

    switch (reason) {
    case NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED;
        break;
    case NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID;
        break;
    default:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN;
        break;
    }
}

static void
_dbus_signal_state_changed_cb(NMVpnConnection *self, guint32 new_service_state)
{
    NMVpnConnectionPrivate *priv              = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMVpnServiceState       old_service_state = priv->service_state;

    _LOGD("dbus: state changed: %s (%d)",
          vpn_service_state_to_string_a(new_service_state),
          new_service_state);
    priv->service_state = new_service_state;

    if (new_service_state == NM_VPN_SERVICE_STATE_STOPPED) {
        if ((priv->vpn_state >= STATE_WAITING) && (priv->vpn_state <= STATE_ACTIVATED)) {
            VpnState old_state = priv->vpn_state;

            _set_vpn_state(self, STATE_FAILED, priv->failure_reason, FALSE);

            /* Reset the failure reason */
            priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN;

            /* If the connection failed, the service cannot persist, but the
             * connection can persist, ask listeners to re-activate the connection.
             */
            if (old_state == STATE_ACTIVATED && priv->vpn_state == STATE_FAILED
                && _connection_only_can_persist(self))
                g_signal_emit(self, signals[INTERNAL_RETRY_AFTER_FAILURE], 0);
        }
    } else if (new_service_state == NM_VPN_SERVICE_STATE_STARTING
               && old_service_state == NM_VPN_SERVICE_STATE_STARTED) {
        /* The VPN service got disconnected and is attempting to reconnect */
        _set_vpn_state(self,
                       STATE_CONNECT,
                       NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT,
                       FALSE);
    }
}

static gboolean
_config_process_generic(NMVpnConnection *self, GVariant *dict)
{
    nm_auto_g_object_thaw_notify GObject   *self_thaw = NULL;
    NMVpnConnectionPrivate                 *priv      = NM_VPN_CONNECTION_GET_PRIVATE(self);
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd      = NULL;
    int                                     IS_IPv4;
    int                                     ip_ifindex = 0;
    const char                             *v_str;
    guint32                                 v_u32;
    gboolean                                v_b;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_TUNDEV, "&s", &v_str)) {
        const char *iface = NULL;

        /* Backwards compat with NM-openswan/libreswan */
        if (!NM_IN_STRSET(v_str, "", "_none_"))
            iface = v_str;

        if (iface) {
            NMPlatform *platform = nm_netns_get_platform(priv->netns);

            ip_ifindex = nm_platform_link_get_ifindex(platform, iface);
            if (ip_ifindex <= 0) {
                nm_platform_process_events(platform);
                ip_ifindex = nm_platform_link_get_ifindex(platform, iface);
            }
            if (ip_ifindex <= 0) {
                _LOGW("config: failed to look up VPN interface index for \"%s\"", iface);
                return FALSE;
            }
        }
    }

    self_thaw = nm_g_object_freeze_notify(self);

    _set_ip_ifindex(self, ip_ifindex, TRUE);
    _set_ip_ifindex(self, _get_ifindex_for_device(self), FALSE);

    ip_ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);
    if (ip_ifindex <= 0) {
        _LOGW("config: no ip-ifindex for the VPN");
        return FALSE;
    }

    for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
        NML3Cfg                    *l3cfg        = priv->l3cfg_if ?: priv->l3cfg_dev;
        gs_unref_object NMIPConfig *ipconfig_old = NULL;

        ipconfig_old = g_steal_pointer(&priv->ip_data_x[IS_IPv4].ip_config);
        if (l3cfg) {
            priv->ip_data_x[IS_IPv4].ip_config =
                nm_l3cfg_ipconfig_acquire(l3cfg, IS_IPv4 ? AF_INET : AF_INET6);
        }
        g_object_notify(G_OBJECT(self),
                        IS_IPv4 ? NM_ACTIVE_CONNECTION_IP4_CONFIG
                                : NM_ACTIVE_CONNECTION_IP6_CONFIG);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CAN_PERSIST, "b", &v_b) && v_b) {
        /* Defaults to FALSE, so only let service indicate TRUE */
        priv->service_can_persist = TRUE;
    }

    if (!g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_BANNER, "&s", &v_str))
        v_str = NULL;
    if (nm_strdup_reset(&priv->banner, v_str))
        _notify(self, PROP_BANNER);

    _vardict_to_addr(AF_INET, dict, NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY, &priv->ip_data_4.gw_external);
    _vardict_to_addr(AF_INET6,
                     dict,
                     NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY,
                     &priv->ip_data_6.gw_external);

    if (nm_ip_addr_is_null(AF_INET, &priv->ip_data_4.gw_external)
        && nm_ip_addr_is_null(AF_INET6, &priv->ip_data_6.gw_external)) {
        _LOGW("config: no VPN gateway address received");
        return FALSE;
    }

    l3cd = _l3cfg_l3cd_new(self, ip_ifindex);

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_PROXY_PAC, "&s", &v_str)) {
        nm_l3_config_data_set_proxy_method(l3cd, NM_PROXY_CONFIG_METHOD_AUTO);
        nm_l3_config_data_set_proxy_pac_url(l3cd, v_str);
    } else
        nm_l3_config_data_set_proxy_method(l3cd, NM_PROXY_CONFIG_METHOD_NONE);

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_MTU, "u", &v_u32))
        priv->mtu = v_u32;
    else
        priv->mtu = 0;

    priv->generic_config_received = TRUE;

    nm_g_object_thaw_notify_clear(&self_thaw);

    _l3cfg_l3cd_set(self, L3CD_TYPE_GENERIC, l3cd);

    return TRUE;
}

static void
_dbus_signal_config_cb(NMVpnConnection *self, GVariant *dict)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    gboolean                v_b;

    g_return_if_fail(dict);

    if (!g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT)) {
        _LOGD("config: ignore invalid configuration type");
        return;
    }

    if (priv->vpn_state < STATE_NEED_AUTH) {
        /* Only list to this signals during and after connection */
        _LOGD("config: ignore configuration before need-auth state");
        return;
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_HAS_IP4, "b", &v_b))
        priv->ip_data_4.enabled = v_b;
    else
        priv->ip_data_4.enabled = FALSE;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_HAS_IP6, "b", &v_b))
        priv->ip_data_6.enabled = v_b;
    else
        priv->ip_data_6.enabled = FALSE;

    _LOGD("config: reply received (IPv4:%s(%s), IPv6:%s(%s))",
          priv->ip_data_4.enabled ? "on" : "off",
          priv->ip_data_4.method_auto ? "auto" : "disabled",
          priv->ip_data_4.enabled ? "on" : "off",
          priv->ip_data_6.method_auto ? "auto" : "disabled");

    if (!priv->ip_data_4.method_auto)
        priv->ip_data_4.enabled = FALSE;
    if (!priv->ip_data_6.method_auto)
        priv->ip_data_6.enabled = FALSE;

    if (priv->vpn_state == STATE_CONNECT)
        _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    if (!_config_process_generic(self, dict)) {
        _check_complete(self, FALSE);
        return;
    }

    _check_complete(self, TRUE);
}

static void
_dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict)
{
    const int                               IS_IPv4 = NM_IS_IPv4(addr_family);
    NMVpnConnectionPrivate                 *priv    = NM_VPN_CONNECTION_GET_PRIVATE(self);
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
    GVariantIter                           *var_iter;
    guint32                                 u32;
    const char                             *v_str;
    NMIPAddr                                v_addr;
    GVariant                               *v;
    gboolean                                v_b;
    int                                     ip_ifindex;
    guint32                                 mss = 0;
    gboolean                                never_default;
    NMPlatformIPXAddress                    address;

    g_return_if_fail(dict && g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT));

    if (priv->vpn_state < STATE_NEED_AUTH) {
        _LOGD("config%c: ignoring, the connection is not in need-auth state",
              nm_utils_addr_family_to_char(addr_family));
        return;
    }

    if (priv->vpn_state > STATE_ACTIVATED) {
        _LOGD("config%c: ignoring, the connection is no longer active",
              nm_utils_addr_family_to_char(addr_family));
        return;
    }

    if (IS_IPv4) {
        if (priv->generic_config_received) {
            _LOGD("config4: reply received");

            if (g_variant_n_children(dict) == 0) {
                priv->ip_data_4.enabled = FALSE;
                _check_complete(self, TRUE);
                return;
            }
        } else {
            _LOGD("config4: reply received (old style)");

            /* In the old API, the generic and IPv4 configuration items
             * were mixed together.
             */
            if (!_config_process_generic(self, dict)) {
                _check_complete(self, FALSE);
                return;
            }

            if (priv->ip_data_4.method_auto)
                priv->ip_data_4.enabled = TRUE;
            priv->ip_data_6.enabled = FALSE;
        }
    } else {
        _LOGD("config6: reply received");

        if (g_variant_n_children(dict) == 0) {
            priv->ip_data_6.enabled = FALSE;
            _check_complete(self, TRUE);
            return;
        }
    }

    if (priv->vpn_state == STATE_CONNECT) {
        _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
    }

    if (!priv->ip_data_x[IS_IPv4].enabled) {
        _check_complete(self, TRUE);
        return;
    }

    ip_ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);
    if (ip_ifindex <= 0)
        g_return_if_reached();

    l3cd = _l3cfg_l3cd_new(self, ip_ifindex);

    nm_l3_config_data_set_dns_priority(l3cd, AF_INET, NM_DNS_PRIORITY_DEFAULT_VPN);

    _vardict_to_addr(addr_family,
                     dict,
                     IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_INT_GATEWAY
                             : NM_VPN_PLUGIN_IP6_CONFIG_INT_GATEWAY,
                     &priv->ip_data_x[IS_IPv4].gw_internal);

    if (IS_IPv4) {
        address.a4 = (NMPlatformIP4Address){
            .plen = 24,
        };
    } else {
        address.a6 = (NMPlatformIP6Address){
            .plen = 128,
        };
    }

    if (_vardict_to_addr(addr_family,
                         dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_ADDRESS
                                 : NM_VPN_PLUGIN_IP6_CONFIG_ADDRESS,
                         address.ax.address_ptr)
        && nm_ip_addr_is_null(addr_family, &address.ax.address_ptr)) {
        _LOGW("invalid IP%c config received: address is zero",
              nm_utils_addr_family_to_char(addr_family));
        _check_complete(self, FALSE);
        return;
    }

    if (!_vardict_to_addr(addr_family,
                          dict,
                          IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_PTP : NM_VPN_PLUGIN_IP6_CONFIG_PTP,
                          nm_platform_ip_address_get_peer_address(addr_family, &address.ax))) {
        if (IS_IPv4)
            address.a4.peer_address = address.a4.address;
    }

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_PREFIX
                                 : NM_VPN_PLUGIN_IP6_CONFIG_PREFIX,
                         "u",
                         &u32))
        address.ax.plen = u32;

    if (!nm_ip_addr_is_null(addr_family, &address.ax.address_ptr)
        && (address.ax.plen == 0 || address.ax.plen > (IS_IPv4 ? 32 : 128))) {
        _LOGW("invalid IP%c config received: invalid prefix %u",
              nm_utils_addr_family_to_char(addr_family),
              address.ax.plen);
        _check_complete(self, FALSE);
        return;
    }

    if (!nm_ip_addr_is_null(addr_family, &address.ax.address_ptr)) {
        address.ax.addr_source = NM_IP_CONFIG_SOURCE_VPN;
        nm_l3_config_data_add_address(l3cd, addr_family, NULL, &address.ax);
    }

    if (IS_IPv4) {
        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_DNS, "au", &var_iter)) {
            while (g_variant_iter_next(var_iter, "u", &u32))
                nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &u32, NULL);
            g_variant_iter_free(var_iter);
        }
    } else {
        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_DNS, "aay", &var_iter)) {
            while (g_variant_iter_next(var_iter, "@ay", &v)) {
                if (nm_ip_addr_set_from_variant(AF_INET6, &v_addr, v, NULL))
                    nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &v_addr, NULL);
                g_variant_unref(v);
            }
            g_variant_iter_free(var_iter);
        }
    }

    if (IS_IPv4) {
        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_NBNS, "au", &var_iter)) {
            while (g_variant_iter_next(var_iter, "u", &u32))
                nm_l3_config_data_add_wins(l3cd, u32);
            g_variant_iter_free(var_iter);
        }
    }

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_MSS : NM_VPN_PLUGIN_IP6_CONFIG_MSS,
                         "u",
                         &u32))
        mss = u32;

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_DOMAIN
                                 : NM_VPN_PLUGIN_IP6_CONFIG_DOMAIN,
                         "&s",
                         &v_str))
        nm_l3_config_data_add_domain(l3cd, addr_family, v_str);

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_DOMAINS
                                 : NM_VPN_PLUGIN_IP6_CONFIG_DOMAINS,
                         "as",
                         &var_iter)) {
        while (g_variant_iter_next(var_iter, "&s", &v_str))
            nm_l3_config_data_add_domain(l3cd, addr_family, v_str);
        g_variant_iter_free(var_iter);
    }

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_PRESERVE_ROUTES
                                 : NM_VPN_PLUGIN_IP6_CONFIG_PRESERVE_ROUTES,
                         "b",
                         &v_b)
        && v_b) {
        if (priv->l3cds[L3CD_TYPE_IP_X(IS_IPv4)]) {
            NMDedupMultiIter ipconf_iter;
            const NMPObject *route;

            nm_l3_config_data_iter_obj_for_each (&ipconf_iter,
                                                 priv->l3cds[L3CD_TYPE_IP_X(IS_IPv4)],
                                                 &route,
                                                 NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))
                nm_l3_config_data_add_route(l3cd, addr_family, route, NULL);
        }
    } else if (IS_IPv4) {
        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_ROUTES, "aau", &var_iter)) {
            _nm_unused nm_auto_free_variant_iter GVariantIter *var_iter_ref_owner = var_iter;
            NMPlatformIPXRoute                                 route              = {};
            guint32                                            plen;

            while (g_variant_iter_next(var_iter, "@au", &v)) {
                _nm_unused gs_unref_variant GVariant *v_ref_owner = v;

                switch (g_variant_n_children(v)) {
                case 5:
                    g_variant_get_child(v, 4, "u", &route.r4.pref_src);
                    /* fall-through */
                case 4:
                    g_variant_get_child(v, 0, "u", &route.r4.network);
                    g_variant_get_child(v, 1, "u", &plen);
                    g_variant_get_child(v, 2, "u", &route.r4.gateway);
                    /* 4th item is unused route metric */
                    route.r4.table_any  = TRUE;
                    route.r4.metric_any = TRUE;
                    route.r4.rt_source  = NM_IP_CONFIG_SOURCE_VPN;

                    if (plen > 32)
                        break;
                    route.r4.plen    = plen;
                    route.r4.network = nm_ip4_addr_clear_host_address(route.r4.network, plen);

                    if (priv->ip_data_4.gw_external.addr4
                        && route.r4.network == priv->ip_data_4.gw_external.addr4
                        && route.r4.plen == 32) {
                        /* Ignore host routes to the VPN gateway since NM adds one itself
                         * below.  Since NM knows more about the routing situation than
                         * the VPN server, we want to use the NM created route instead of
                         * whatever the server provides.
                         */
                        break;
                    }

                    nm_l3_config_data_add_route_4(l3cd, &route.r4);
                    break;
                default:
                    break;
                }
            }
        }
    } else {
        _nm_unused nm_auto_free_variant_iter GVariantIter *var_iter_ref_owner = NULL;
        NMPlatformIPXRoute                                 route              = {};
        guint32                                            prefix;
        guint32                                            metric;
        NMOptionBool                                       new_signature = NM_OPTION_BOOL_DEFAULT;

        /* IPv6 and no "preserve-routes" */

        if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ROUTES, "a(ayuayu)", &var_iter))
            new_signature = FALSE;
        else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ROUTES, "a(ayuayuay)", &var_iter))
            new_signature = TRUE;
        else
            var_iter = NULL;

        var_iter_ref_owner = var_iter;

        while (TRUE) {
            gs_unref_variant GVariant *next_hop = NULL;
            gs_unref_variant GVariant *dest     = NULL;
            gs_unref_variant GVariant *pref_src = NULL;

            if (new_signature == NM_OPTION_BOOL_DEFAULT) {
                break;
            } else if (new_signature) {
                if (!g_variant_iter_next(var_iter,
                                         "(@ayu@ayu@ay)",
                                         &dest,
                                         &prefix,
                                         &next_hop,
                                         &metric,
                                         &pref_src))
                    break;
            } else {
                if (!g_variant_iter_next(var_iter,
                                         "(@ayu@ayu)",
                                         &dest,
                                         &prefix,
                                         &next_hop,
                                         &metric))
                    break;
            }

            if (prefix > 128)
                continue;

            route.r6 = (NMPlatformIP6Route){
                .plen       = prefix,
                .table_any  = TRUE,
                .metric_any = TRUE,
                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
            };

            if (!nm_ip_addr_set_from_variant(AF_INET6, &route.r6.network, dest, NULL))
                continue;

            if (pref_src
                && !nm_ip_addr_set_from_variant(AF_INET6, &route.r6.pref_src, pref_src, NULL))
                continue;

            nm_ip_addr_set_from_variant(AF_INET6, &route.r6.gateway, next_hop, NULL);

            nm_ip6_addr_clear_host_address(&route.r6.network, &route.r6.network, route.r6.plen);

            if (!IN6_IS_ADDR_UNSPECIFIED(&priv->ip_data_6.gw_external.addr6)
                && IN6_ARE_ADDR_EQUAL(&route.r6.network, &priv->ip_data_6.gw_external.addr6)
                && route.r6.plen == 128) {
                /* Ignore host routes to the VPN gateway since NM adds one itself.
                 * Since NM knows more about the routing situation than the VPN
                 * server, we want to use the NM created route instead of whatever
                 * the server provides.
                 */
                continue;
            }

            nm_l3_config_data_add_route_6(l3cd, &route.r6);
        }
    }

    if (g_variant_lookup(dict,
                         IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_NEVER_DEFAULT
                                 : NM_VPN_PLUGIN_IP6_CONFIG_NEVER_DEFAULT,
                         "b",
                         &v_b))
        never_default = v_b;
    else
        never_default = FALSE;

    if (!never_default) {
        NMPlatformIPXRoute route;

        if (IS_IPv4) {
            route.r4 = (NMPlatformIP4Route){
                .ifindex    = ip_ifindex,
                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
                .gateway    = priv->ip_data_4.gw_internal.addr4,
                .table_any  = TRUE,
                .metric_any = TRUE,
                .mss        = mss,
            };
        } else {
            route.r6 = (NMPlatformIP6Route){
                .ifindex    = ip_ifindex,
                .rt_source  = NM_IP_CONFIG_SOURCE_VPN,
                .gateway    = priv->ip_data_6.gw_internal.addr6,
                .table_any  = TRUE,
                .metric_any = TRUE,
                .mss        = mss,
            };
        }
        nm_l3_config_data_add_route(l3cd, addr_family, NULL, &route.rx);
    }

    _l3cfg_l3cd_set(self, L3CD_TYPE_IP_X(IS_IPv4), l3cd);

    _check_complete(self, TRUE);
}

void
nm_vpn_connection_disconnect(NMVpnConnection              *self,
                             NMActiveConnectionStateReason reason,
                             gboolean                      quitting)
{
    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    _set_vpn_state(self, STATE_DISCONNECTED, reason, quitting);
}

gboolean
nm_vpn_connection_deactivate(NMVpnConnection              *self,
                             NMActiveConnectionStateReason reason,
                             gboolean                      quitting)
{
    NMVpnConnectionPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), FALSE);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->vpn_state <= STATE_UNKNOWN || priv->vpn_state > STATE_DEACTIVATING)
        return FALSE;

    _set_vpn_state(self, STATE_DEACTIVATING, reason, quitting);
    return TRUE;
}

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

static void
_secrets_dbus_need_secrets_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection           *self;
    NMVpnConnectionPrivate    *priv;
    gs_unref_variant GVariant *res   = NULL;
    gs_free_error GError      *error = NULL;
    const char                *setting_name;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_VPN_CONNECTION(user_data);
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (error) {
        _LOGW("plugin NeedSecrets request #%d failed: %s", priv->secrets_idx + 1, error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    g_variant_get(res, "(&s)", &setting_name);
    if (nm_str_is_empty(setting_name)) {
        /* No secrets required; we can start the VPN */
        _LOGD("service indicated no additional secrets required");
        really_activate(self, priv->username);
        return;
    }

    /* More secrets required */
    if (priv->secrets_idx == SECRETS_REQ_NEW) {
        _LOGW("final secrets request failed to provide sufficient secrets");
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    _LOGD("service indicated additional secrets required");
    _secrets_get(self, priv->secrets_idx + 1, NULL);
}

static void
_secrets_dbus_new_secrets_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection           *self;
    gs_unref_variant GVariant *res   = NULL;
    gs_free_error GError      *error = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_VPN_CONNECTION(user_data);

    if (error) {
        _LOGW("sending new secrets to the plugin failed: %s", error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    _set_vpn_state(self, STATE_CONNECT, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
_secrets_get_secrets_cb(NMSettingsConnection       *connection,
                        NMSettingsConnectionCallId *call_id,
                        const char                 *agent_username,
                        const char                 *setting_name,
                        GError                     *error,
                        gpointer                    user_data)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv;
    GVariant               *dict;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    g_return_if_fail(connection && connection == _get_settings_connection(self, FALSE));
    g_return_if_fail(call_id == priv->secrets_id);

    priv->secrets_id = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    if (error && priv->secrets_idx >= SECRETS_REQ_NEW) {
        _LOGW("secrets: failed to request VPN secrets #%d: %s",
              priv->secrets_idx + 1,
              error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    if (!priv->dbus.bus_name) {
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED,
                       FALSE);
        return;
    }

    /* Cache the username for later */
    if (agent_username)
        nm_strdup_reset(&priv->username, agent_username);

    dict = _hash_with_username(_get_applied_connection(self), priv->username);

    if (priv->secrets_idx == SECRETS_REQ_INTERACTIVE) {
        _LOGD("secrets: sending secrets to the plugin");
        _dbus_connection_call(self,
                              "NewSecrets",
                              g_variant_new("(@a{sa{sv}})", dict),
                              G_VARIANT_TYPE("()"),
                              _secrets_dbus_new_secrets_cb);
        return;
    }

    _LOGD("secrets: asking service if additional secrets are required");
    _dbus_connection_call(self,
                          "NeedSecrets",
                          g_variant_new("(@a{sa{sv}})", dict),
                          G_VARIANT_TYPE("(s)"),
                          _secrets_dbus_need_secrets_cb);
}

static void
_secrets_get(NMVpnConnection *self, SecretsReq secrets_idx, const char *const *hints)
{
    NMVpnConnectionPrivate      *priv  = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE;

    g_return_if_fail(secrets_idx < SECRETS_REQ_LAST);

    priv->secrets_idx = secrets_idx;

    cancel_get_secrets(self);

    _LOGD("secrets: requesting VPN secrets pass #%d", priv->secrets_idx + 1);

    switch (priv->secrets_idx) {
    case SECRETS_REQ_SYSTEM:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM;
        break;
    case SECRETS_REQ_EXISTING:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE;
        break;
    case SECRETS_REQ_NEW:
    case SECRETS_REQ_INTERACTIVE:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
        break;
    default:
        g_return_if_reached();
    }

    if (nm_active_connection_get_user_requested(NM_ACTIVE_CONNECTION(self)))
        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED;

    priv->secrets_id = nm_settings_connection_get_secrets(
        _get_settings_connection(self, FALSE),
        _get_applied_connection(self),
        nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(self)),
        NM_SETTING_VPN_SETTING_NAME,
        flags,
        hints,
        _secrets_get_secrets_cb,
        self);

    g_return_if_fail(priv->secrets_id);
}

static void
_dbus_signal_secrets_required_cb(NMVpnConnection   *self,
                                 const char        *message,
                                 const char *const *secrets)
{
    NMVpnConnectionPrivate *priv        = NM_VPN_CONNECTION_GET_PRIVATE(self);
    const gsize             secrets_len = NM_PTRARRAY_LEN(secrets);
    gsize                   i;
    gs_free const char    **hints        = NULL;
    gs_free char           *message_hint = NULL;

    if (!NM_IN_SET(priv->vpn_state, STATE_CONNECT, STATE_NEED_AUTH)) {
        _LOGD("secrets: request ignored in current state %s",
              vpn_state_to_string_a(priv->vpn_state));
        return;
    }

    _LOGD("secrets: request (state %s)", vpn_state_to_string_a(priv->vpn_state));

    priv->secrets_idx = SECRETS_REQ_INTERACTIVE;
    _set_vpn_state(self, STATE_NEED_AUTH, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    /* Copy hints and add message to the end */
    hints = g_new(const char *, secrets_len + 2);
    for (i = 0; i < secrets_len; i++)
        hints[i] = secrets[i];
    if (message) {
        message_hint = g_strdup_printf("x-vpn-message:%s", message);
        hints[i++]   = message_hint;
    }
    hints[i] = NULL;
    nm_assert(i < secrets_len + 2);

    _secrets_get(self, SECRETS_REQ_INTERACTIVE, hints);
}

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

static int
_get_log_level(void)
{
    NMLogLevel level;

    /* curiously enough, nm-logging also uses syslog. But it
     * maps NMLogLevel differently to the syslog levels then we
     * do here.
     *
     * The reason is, that LOG_NOTICE is already something worth
     * highlighting in the journal, but we have 3 levels that are
     * lower then LOG_NOTICE (LOGL_TRACE, LOGL_DEBUG, LOGL_INFO),
     * On the other hand, syslog only defines LOG_DEBUG and LOG_INFO.
     * Thus, we must map them differently.
     *
     * Inside the VPN plugin, you might want to treat LOG_NOTICE as
     * as low severity, not worthy to be highlighted (like NM does). */

    level = nm_logging_get_level(LOGD_VPN_PLUGIN);
    if (level != _LOGL_OFF) {
        if (level <= LOGL_TRACE)
            return LOG_DEBUG;
        if (level <= LOGL_DEBUG)
            return LOG_INFO;
        if (level <= LOGL_INFO)
            return LOG_NOTICE;
        if (level <= LOGL_WARN)
            return LOG_WARNING;
        if (level <= LOGL_ERR)
            return LOG_ERR;
    }

    return LOG_EMERG;
}

static gboolean
nm_vpn_service_daemon_exec(NMVpnConnection *self, GError **error)
{
    NMVpnConnectionPrivate *priv;
    GPid                    pid;
    char                   *vpn_argv[4];
    gs_free char          **envp = NULL;
    char                    env_log_level[NM_STRLEN("NM_VPN_LOG_LEVEL=") + 100];
    char                    env_log_syslog[NM_STRLEN("NM_VPN_LOG_SYSLOG=") + 10];
    const gsize             N_ENVIRON_EXTRA = 3;
    char                  **p_environ;
    gsize                   n_environ;
    gsize                   i;
    gsize                   j;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), FALSE);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    i             = 0;
    vpn_argv[i++] = (char *) nm_vpn_plugin_info_get_program(priv->plugin_info);
    g_return_val_if_fail(vpn_argv[0], FALSE);
    if (nm_vpn_plugin_info_supports_multiple(priv->plugin_info)) {
        vpn_argv[i++] = "--bus-name";
        vpn_argv[i++] = priv->dbus.bus_name;
    }
    vpn_argv[i++] = NULL;

    /* we include <unistd.h> and "config.h" defines _GNU_SOURCE for us. So, we have @environ. */
    p_environ = environ;
    n_environ = NM_PTRARRAY_LEN(p_environ);
    envp      = g_new(char *, n_environ + N_ENVIRON_EXTRA);
    for (i = 0, j = 0; j < n_environ; j++) {
        if (NM_STR_HAS_PREFIX(p_environ[j], "NM_VPN_LOG_LEVEL=")
            || NM_STR_HAS_PREFIX(p_environ[j], "NM_VPN_LOG_SYSLOG="))
            continue;
        envp[i++] = p_environ[j];
    }

    /* NM_VPN_LOG_LEVEL: the syslog logging level for the plugin. */
    envp[i++] = nm_sprintf_buf(env_log_level, "NM_VPN_LOG_LEVEL=%d", _get_log_level());

    /* NM_VPN_LOG_SYSLOG: whether to log to stdout or syslog. If NetworkManager itself runs in
     * foreground, we also want the plugin to log to stdout.
     * If the plugin runs in background, the plugin should prefer logging to syslog. Otherwise
     * logging messages will be lost (unless using journald, in which case it wouldn't matter). */
    envp[i++] = nm_sprintf_buf(env_log_syslog,
                               "NM_VPN_LOG_SYSLOG=%c",
                               nm_logging_syslog_enabled() ? '1' : '0');

    envp[i++] = NULL;
    nm_assert(i <= n_environ + N_ENVIRON_EXTRA);

    if (!g_spawn_async(NULL, vpn_argv, envp, 0, nm_utils_setpgid, NULL, &pid, error))
        return FALSE;

    _LOGD("starting: VPN service has PID %lld", (long long) pid);
    return TRUE;
}

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

static gboolean
_start_timeout_cb(gpointer data)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_clear_g_source_inst(&priv->start_timeout_source);

    if (priv->dbus_service_started)
        _LOGW("starting: timed out waiting for the service to start");
    else
        _LOGW("starting: timed out waiting for the VPN to activate");
    nm_vpn_connection_disconnect(self,
                                 NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT,
                                 FALSE);
    return G_SOURCE_CONTINUE;
}

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

static void
_dbus_dispatch_cb(GDBusConnection *connection,
                  const char      *sender_name,
                  const char      *object_path,
                  const char      *interface_name,
                  const char      *signal_name,
                  GVariant        *parameters,
                  gpointer         user_data)
{
    NMVpnConnection        *self = user_data;
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    guint32                 v_u;

    nm_assert(nm_streq0(object_path, NM_VPN_DBUS_PLUGIN_PATH));
    nm_assert(nm_streq0(interface_name, NM_VPN_DBUS_PLUGIN_INTERFACE));
    nm_assert(signal_name);

    if (!nm_streq0(priv->dbus.owner, sender_name))
        return;

    if (nm_streq(signal_name, "Failure")) {
        if (nm_g_variant_tuple_get_u(parameters, &v_u))
            _dbus_signal_failure_cb(self, v_u);
    } else if (nm_streq(signal_name, "StateChanged")) {
        if (nm_g_variant_tuple_get_u(parameters, &v_u))
            _dbus_signal_state_changed_cb(self, v_u);
    } else if (nm_streq(signal_name, "SecretsRequired")) {
        if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sas)"))) {
            const char          *v_s;
            gs_free const char **v_strv = NULL;

            g_variant_get(parameters, "(&s^a&s)", &v_s, &v_strv);
            _dbus_signal_secrets_required_cb(self, v_s, v_strv);
        }
    } else if (NM_IN_STRSET(signal_name, "Config", "Ip4Config", "Ip6Config")) {
        if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(a{sv})"))) {
            gs_unref_variant GVariant *v_var = NULL;

            g_variant_get(parameters, "(@a{sv})", &v_var);
            if (signal_name[0] == 'C')
                _dbus_signal_config_cb(self, v_var);
            else if (signal_name[2] == '4')
                _dbus_signal_ip_config_cb(self, AF_INET, v_var);
            else
                _dbus_signal_ip_config_cb(self, AF_INET6, v_var);
        }
    }
}

static void
_name_owner_changed(NMVpnConnection *self, const char *owner, gboolean initializing)
{
    _nm_unused gs_unref_object NMVpnConnection *self_keep_alive = g_object_ref(self);
    NMVpnConnectionPrivate                     *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    owner = nm_str_not_empty(owner);

    if (!owner && initializing) {
        gs_free_error GError *error = NULL;

        nm_assert(!priv->dbus.owner);
        _LOGT("dbus: no name owner for %s (start VPN service)", priv->dbus.bus_name);

        if (!nm_vpn_service_daemon_exec(self, &error)) {
            _LOGW("starting: failure to start VPN service: %s", error->message);
            nm_vpn_connection_disconnect(self,
                                         NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                                         FALSE);
        }
        priv->start_timeout_source = nm_g_timeout_add_seconds_source(5, _start_timeout_cb, self);
        return;
    }

    if (!nm_strdup_reset(&priv->dbus.owner, owner))
        return;

    if (!priv->dbus.owner) {
        _LOGT("dbus: name owner for %s disappeared", priv->dbus.bus_name);

        /* We don't want to restart if the service re-appears. Disconnect the signal
         * so that cannot happen and we don't disconnect the VPN again. */
        nm_clear_g_dbus_connection_signal(priv->dbus.connection,
                                          &priv->dbus.signal_id_name_changed);

        nm_vpn_connection_disconnect(self,
                                     NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED,
                                     FALSE);
        return;
    }

    _LOGT("dbus: name owner %s for %s", priv->dbus.owner, priv->dbus.bus_name);

    priv->dbus_service_started = TRUE;
    nm_clear_g_source_inst(&priv->start_timeout_source);
    priv->start_timeout_source =
        nm_g_timeout_add_seconds_source(_get_vpn_timeout(self) + 180, _start_timeout_cb, self);

    _set_vpn_state(self, STATE_NEED_AUTH, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    /* Kick off the secrets requests; first we get existing system secrets
     * and ask the plugin if these are sufficient, next we get all existing
     * secrets from system and from user agents and ask the plugin again,
     * and last we ask the user for new secrets if required.
     */
    _secrets_get(self, SECRETS_REQ_SYSTEM, NULL);
}

static void
_name_owner_changed_cb(GDBusConnection *connection,
                       const char      *sender_name,
                       const char      *object_path,
                       const char      *interface_name,
                       const char      *signal_name,
                       GVariant        *parameters,
                       gpointer         user_data)
{
    NMVpnConnection        *self = user_data;
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    const char             *new_owner;

    if (!priv->dbus.name_owner_initialized)
        return;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
        return;

    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);

    _name_owner_changed(self, new_owner, FALSE);
}

static void
_name_owner_get_cb(const char *name_owner, GError *error, gpointer user_data)
{
    NMVpnConnection        *self;
    NMVpnConnectionPrivate *priv;

    if (nm_utils_error_is_cancelled(error))
        return;

    self = user_data;
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    priv->dbus.name_owner_initialized = TRUE;
    _name_owner_changed(self, name_owner, TRUE);
}

static gboolean
_init_fail_on_idle_cb(gpointer user_data)
{
    NMVpnConnection        *self = user_data;
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_clear_g_source_inst(&priv->init_fail_on_idle_source);
    _set_vpn_state(self,
                   STATE_FAILED,
                   NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                   FALSE);
    return G_SOURCE_CONTINUE;
}

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

void
nm_vpn_connection_activate(NMVpnConnection *self, NMVpnPluginInfo *plugin_info)
{
    NMVpnConnectionPrivate *priv;
    NMConnection           *connection;
    NMSettingVpn           *s_vpn;
    const char             *service;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));
    g_return_if_fail(NM_IS_VPN_PLUGIN_INFO(plugin_info));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(!priv->plugin_info);

    connection = _get_applied_connection(self);

    s_vpn = nm_connection_get_setting_vpn(connection);
    g_return_if_fail(s_vpn);

    service = nm_vpn_plugin_info_get_service(plugin_info);
    nm_assert(service);

    if (nm_vpn_plugin_info_supports_multiple(plugin_info)) {
        const char *path;

        path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self));
        if (path)
            path = strrchr(path, '/');
        g_return_if_fail(path);

        priv->dbus.bus_name = g_strdup_printf("%s.Connection_%s", service, &path[1]);
    } else
        priv->dbus.bus_name = g_strdup(service);

    _LOGI("starting %s", nm_vpn_plugin_info_get_name(plugin_info));

    priv->ip_data_4.method_auto = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET),
                                            NM_SETTING_IP4_CONFIG_METHOD_AUTO);
    priv->ip_data_6.method_auto = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET6),
                                            NM_SETTING_IP6_CONFIG_METHOD_AUTO);

    priv->connection_can_persist = nm_setting_vpn_get_persistent(s_vpn);
    priv->plugin_info            = g_object_ref(plugin_info);

    priv->main_cancellable = g_cancellable_new();

    priv->dbus.connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);

    if (!priv->dbus.connection) {
        _LOGD("starting: no D-Bus connection (will fail)");
        priv->init_fail_on_idle_source = nm_g_idle_add_source(_init_fail_on_idle_cb, self);
        goto out;
    }

    _LOGD("starting: watch D-Bus service %s", priv->dbus.bus_name);
    priv->dbus.signal_id_name_changed =
        nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus.connection,
                                                               priv->dbus.bus_name,
                                                               _name_owner_changed_cb,
                                                               self,
                                                               NULL);

    priv->dbus.signal_id_vpn = g_dbus_connection_signal_subscribe(priv->dbus.connection,
                                                                  priv->dbus.bus_name,
                                                                  NM_VPN_DBUS_PLUGIN_INTERFACE,
                                                                  NULL,
                                                                  NM_VPN_DBUS_PLUGIN_PATH,
                                                                  NULL,
                                                                  G_DBUS_SIGNAL_FLAGS_NONE,
                                                                  _dbus_dispatch_cb,
                                                                  self,
                                                                  NULL);

    nm_dbus_connection_call_get_name_owner(priv->dbus.connection,
                                           priv->dbus.bus_name,
                                           3000,
                                           priv->main_cancellable,
                                           _name_owner_get_cb,
                                           self);

out:
    _set_vpn_state(self, STATE_PREPARE, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

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

static void
device_changed(NMActiveConnection *active, NMDevice *new_device, NMDevice *old_device)
{
    NMVpnConnection         *self      = NM_VPN_CONNECTION(active);
    NMVpnConnectionPrivate  *priv      = NM_VPN_CONNECTION_GET_PRIVATE(active);
    gs_unref_object NML3Cfg *l3cfg_old = NULL;
    int                      ifindex;

    if (!priv->generic_config_received)
        return;
    if (priv->vpn_state > STATE_ACTIVATED)
        return;
    if (!_service_and_connection_can_persist(self))
        return;

    if (priv->ifindex_if <= 0) {
        /* Route-based VPNs must updvate their routing and send a new IP config
         * since all their routes need to be adjusted for new_device.
         */
        return;
    }

    ifindex = _get_ifindex_for_device(self);
    if (ifindex <= 0)
        return;
    if (priv->ifindex_dev == ifindex)
        return;

    _LOGD("set ip-ifindex-dev %d (was %d)", ifindex, priv->ifindex_dev);

    l3cfg_old = g_steal_pointer(&priv->l3cfg_dev);
    nm_l3cfg_commit_type_clear(l3cfg_old, &priv->l3cfg_commit_type_dev);
    _l3cfg_clear(self, l3cfg_old);

    priv->ifindex_dev = ifindex;

    priv->l3cfg_dev = nm_netns_l3cfg_acquire(priv->netns, ifindex);
    g_signal_connect(priv->l3cfg_dev, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(_l3cfg_notify_cb), self);
    priv->l3cfg_commit_type_dev =
        nm_l3cfg_commit_type_register(priv->l3cfg_dev, NM_L3_CFG_COMMIT_TYPE_UPDATE, NULL, "vpn");

    if (_l3cfg_l3cd_gw_extern_update(self))
        nm_l3cfg_commit_on_idle_schedule(priv->l3cfg_dev, NM_L3_CFG_COMMIT_TYPE_AUTO);
}

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

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(object);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_VPN_STATE:
        g_value_set_uint(value, _state_to_nm_vpn_state(priv->vpn_state));
        break;
    case PROP_BANNER:
        g_value_set_string(value, priv->banner ?: "");
        break;
    case PROP_IP4_CONFIG:
        nm_dbus_utils_g_value_set_object_path(value, priv->ip_data_4.ip_config);
        break;
    case PROP_IP6_CONFIG:
        nm_dbus_utils_g_value_set_object_path(value, priv->ip_data_6.ip_config);
        break;
    case PROP_CONTROLLER:
    case PROP_MASTER:
        nm_dbus_utils_g_value_set_object_path(
            value,
            nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self)));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_vpn_connection_init(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    priv->vpn_state   = STATE_WAITING;
    priv->secrets_idx = SECRETS_REQ_SYSTEM;
    priv->netns       = g_object_ref(nm_netns_get());
}

NMVpnConnection *
nm_vpn_connection_new(NMSettingsConnection  *settings_connection,
                      NMDevice              *parent_device,
                      const char            *specific_object,
                      NMActivationReason     activation_reason,
                      NMActivationStateFlags initial_state_flags,
                      NMAuthSubject         *subject)
{
    g_return_val_if_fail(!settings_connection || NM_IS_SETTINGS_CONNECTION(settings_connection),
                         NULL);
    g_return_val_if_fail(NM_IS_DEVICE(parent_device), NULL);
    g_return_val_if_fail(specific_object, NULL);

    return g_object_new(NM_TYPE_VPN_CONNECTION,
                        NM_ACTIVE_CONNECTION_INT_SETTINGS_CONNECTION,
                        settings_connection,
                        NM_ACTIVE_CONNECTION_INT_DEVICE,
                        parent_device,
                        NM_ACTIVE_CONNECTION_SPECIFIC_OBJECT,
                        specific_object,
                        NM_ACTIVE_CONNECTION_INT_SUBJECT,
                        subject,
                        NM_ACTIVE_CONNECTION_INT_ACTIVATION_REASON,
                        activation_reason,
                        NM_ACTIVE_CONNECTION_VPN,
                        TRUE,
                        NM_ACTIVE_CONNECTION_STATE_FLAGS,
                        (guint) initial_state_flags,
                        NULL);
}

static void
dispose(GObject *object)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(object);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_clear_g_dbus_connection_signal(priv->dbus.connection, &priv->dbus.signal_id_vpn);
    nm_clear_g_dbus_connection_signal(priv->dbus.connection, &priv->dbus.signal_id_name_changed);

    nm_clear_g_source_inst(&priv->init_fail_on_idle_source);

    nm_clear_g_cancellable(&priv->main_cancellable);

    nm_clear_g_source_inst(&priv->start_timeout_source);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);

    nm_clear_g_source_inst(&priv->connect_timeout_source);

    if (nm_l3cfg_commit_type_clear(priv->l3cfg_if, &priv->l3cfg_commit_type_if))
        nm_l3cfg_commit_on_idle_schedule(priv->l3cfg_if, NM_L3_CFG_COMMIT_TYPE_AUTO);

    if (nm_l3cfg_commit_type_clear(priv->l3cfg_dev, &priv->l3cfg_commit_type_dev))
        nm_l3cfg_commit_on_idle_schedule(priv->l3cfg_dev, NM_L3_CFG_COMMIT_TYPE_AUTO);

    g_clear_object(&priv->ip_data_4.ip_config);
    g_clear_object(&priv->ip_data_6.ip_config);

    dispatcher_cleanup(self);

    cancel_get_secrets(self);

    fw_call_cleanup(self);

    nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);

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

static void
finalize(GObject *object)
{
    NMVpnConnection        *self = NM_VPN_CONNECTION(object);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    G_OBJECT_CLASS(nm_vpn_connection_parent_class)->finalize(object);

    g_free(priv->banner);
    g_free(priv->username);
    g_free(priv->dbus.bus_name);

    _l3cfg_l3cd_clear_all(self);
    _l3cfg_clear(self, priv->l3cfg_if);
    _l3cfg_clear(self, priv->l3cfg_dev);

    g_clear_object(&priv->plugin_info);
    g_clear_object(&priv->l3cfg_if);
    g_clear_object(&priv->l3cfg_dev);
    g_clear_object(&priv->netns);
    g_clear_object(&priv->dbus.connection);
}

static const GDBusSignalInfo signal_info_vpn_state_changed = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
    "VpnStateChanged",
    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("state", "u"),
                                      NM_DEFINE_GDBUS_ARG_INFO("reason", "u"), ), );

static const NMDBusInterfaceInfoExtended interface_info_vpn_connection = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_VPN_CONNECTION,
        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&signal_info_vpn_state_changed, ),
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("VpnState",
                                                           "u",
                                                           NM_VPN_CONNECTION_VPN_STATE),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Banner",
                                                           "s",
                                                           NM_VPN_CONNECTION_BANNER), ), ),
};

static void
nm_vpn_connection_class_init(NMVpnConnectionClass *klass)
{
    GObjectClass            *object_class      = G_OBJECT_CLASS(klass);
    NMActiveConnectionClass *active_class      = NM_ACTIVE_CONNECTION_CLASS(klass);
    NMDBusObjectClass       *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);

    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_vpn_connection);

    object_class->get_property = get_property;
    object_class->dispose      = dispose;
    object_class->finalize     = finalize;

    active_class->device_state_changed = device_state_changed;
    active_class->device_changed       = device_changed;

    obj_properties[PROP_VPN_STATE] = g_param_spec_uint(NM_VPN_CONNECTION_VPN_STATE,
                                                       "",
                                                       "",
                                                       NM_VPN_CONNECTION_STATE_UNKNOWN,
                                                       NM_VPN_CONNECTION_STATE_DISCONNECTED,
                                                       NM_VPN_CONNECTION_STATE_UNKNOWN,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_BANNER] = g_param_spec_string(NM_VPN_CONNECTION_BANNER,
                                                      "",
                                                      "",
                                                      NULL,
                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    g_object_class_override_property(object_class,
                                     PROP_CONTROLLER,
                                     NM_ACTIVE_CONNECTION_CONTROLLER);
    g_object_class_override_property(object_class, PROP_MASTER, NM_ACTIVE_CONNECTION_MASTER);
    g_object_class_override_property(object_class,
                                     PROP_IP4_CONFIG,
                                     NM_ACTIVE_CONNECTION_IP4_CONFIG);
    g_object_class_override_property(object_class,
                                     PROP_IP6_CONFIG,
                                     NM_ACTIVE_CONNECTION_IP6_CONFIG);

    signals[INTERNAL_STATE_CHANGED] =
        g_signal_new(NM_VPN_CONNECTION_INTERNAL_STATE_CHANGED,
                     G_OBJECT_CLASS_TYPE(object_class),
                     G_SIGNAL_RUN_FIRST,
                     0,
                     NULL,
                     NULL,
                     NULL,
                     G_TYPE_NONE,
                     3,
                     G_TYPE_UINT, /* NMVpnConnectionState new_external_state */
                     G_TYPE_UINT, /* NMVpnConnectionState old_external_state */
                     G_TYPE_UINT /* NMActiveConnectionStateReason reason */);

    signals[INTERNAL_RETRY_AFTER_FAILURE] =
        g_signal_new(NM_VPN_CONNECTION_INTERNAL_RETRY_AFTER_FAILURE,
                     G_OBJECT_CLASS_TYPE(object_class),
                     G_SIGNAL_RUN_FIRST,
                     0,
                     NULL,
                     NULL,
                     NULL,
                     G_TYPE_NONE,
                     0);
}
