summary refs log tree commit diff
path: root/src/libnmc-base
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/libnmc-base
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/libnmc-base')
-rw-r--r--src/libnmc-base/meson.build15
-rw-r--r--src/libnmc-base/nm-client-utils.c912
-rw-r--r--src/libnmc-base/nm-client-utils.h70
-rw-r--r--src/libnmc-base/nm-polkit-listener.c910
-rw-r--r--src/libnmc-base/nm-polkit-listener.h33
-rw-r--r--src/libnmc-base/nm-secret-agent-simple.c1405
-rw-r--r--src/libnmc-base/nm-secret-agent-simple.h61
-rw-r--r--src/libnmc-base/nm-vpn-helpers.c821
-rw-r--r--src/libnmc-base/nm-vpn-helpers.h31
-rw-r--r--src/libnmc-base/qrcodegen.c1141
-rw-r--r--src/libnmc-base/qrcodegen.h312
11 files changed, 5711 insertions, 0 deletions
diff --git a/src/libnmc-base/meson.build b/src/libnmc-base/meson.build
new file mode 100644
index 00000000..adb71531
--- /dev/null
+++ b/src/libnmc-base/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnmc_base = static_library(
+  'nmc-base',
+  sources: files(
+    'nm-client-utils.c',
+    'nm-secret-agent-simple.c',
+    'nm-vpn-helpers.c',
+    'nm-polkit-listener.c',
+  ),
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+  ],
+)
diff --git a/src/libnmc-base/nm-client-utils.c b/src/libnmc-base/nm-client-utils.c
new file mode 100644
index 00000000..49234d97
--- /dev/null
+++ b/src/libnmc-base/nm-client-utils.c
@@ -0,0 +1,912 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2010 - 2017 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-client-utils.h"
+
+#include "libnm-glib-aux/nm-secret-utils.h"
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "nm-utils.h"
+#include "nm-device-bond.h"
+#include "nm-device-bridge.h"
+#include "nm-device-team.h"
+
+/*****************************************************************************/
+
+static int
+_nmc_objects_sort_by_path_cmp(gconstpointer pa, gconstpointer pb, gpointer user_data)
+{
+    NMObject *a = *((NMObject **) pa);
+    NMObject *b = *((NMObject **) pb);
+
+    NM_CMP_SELF(a, b);
+    NM_CMP_RETURN(nm_utils_dbus_path_cmp(nm_object_get_path(a), nm_object_get_path(b)));
+    return 0;
+}
+
+const NMObject **
+nmc_objects_sort_by_path(const NMObject *const *objs, gssize len)
+{
+    const NMObject **arr;
+    gsize            i, l;
+
+    if (len < 0)
+        l = NM_PTRARRAY_LEN(objs);
+    else
+        l = len;
+
+    arr = g_new(const NMObject *, l + 1);
+    for (i = 0; i < l; i++)
+        arr[i] = objs[i];
+    arr[l] = NULL;
+
+    if (l > 1) {
+        g_qsort_with_data(arr, l, sizeof(gpointer), _nmc_objects_sort_by_path_cmp, NULL);
+    }
+    return arr;
+}
+
+/*****************************************************************************/
+/*
+ * Convert string to unsigned integer.
+ * If required, the resulting number is checked to be in the <min,max> range.
+ */
+static gboolean
+nmc_string_to_uint_base(const char *       str,
+                        int                base,
+                        gboolean           range_check,
+                        unsigned long int  min,
+                        unsigned long int  max,
+                        unsigned long int *value)
+{
+    char *            end;
+    unsigned long int tmp;
+
+    if (!str || !str[0])
+        return FALSE;
+
+    /* FIXME: don't use this function, replace by _nm_utils_ascii_str_to_int64() */
+    errno = 0;
+    tmp   = strtoul(str, &end, base);
+    if (errno || *end != '\0' || (range_check && (tmp < min || tmp > max))) {
+        return FALSE;
+    }
+    *value = tmp;
+    return TRUE;
+}
+
+gboolean
+nmc_string_to_uint(const char *       str,
+                   gboolean           range_check,
+                   unsigned long int  min,
+                   unsigned long int  max,
+                   unsigned long int *value)
+{
+    return nmc_string_to_uint_base(str, 10, range_check, min, max, value);
+}
+
+gboolean
+nmc_string_to_bool(const char *str, gboolean *val_bool, GError **error)
+{
+    gs_free char *str_to_free = NULL;
+    int           i;
+
+    nm_assert(!error || !*error);
+    nm_assert(val_bool);
+
+    str = nm_strstrip_avoid_copy_a(300, str, &str_to_free);
+
+    if (nm_streq0(str, "o")) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           /* TRANSLATORS: the first %s is the partial value entered by
+                            * the user, the second %s a list of compatible values.
+                            */
+                           _("'%s' is ambiguous (%s)"),
+                           str,
+                           "on, off");
+        return FALSE;
+    }
+
+    if (nmc_string_is_valid(str, NM_MAKE_STRV("true", "yes", "on"), NULL))
+        *val_bool = TRUE;
+    else if (nmc_string_is_valid(str, NM_MAKE_STRV("false", "no", "off"), NULL))
+        *val_bool = FALSE;
+    else if ((i = _nm_utils_ascii_str_to_int64(str, 0, 0, 1, -1)) >= 0) {
+        *val_bool = !!i;
+    } else {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           _("'%s' is not valid; use [%s] or [%s]"),
+                           str,
+                           "true, yes, on",
+                           "false, no, off");
+        return FALSE;
+    }
+    return TRUE;
+}
+
+gboolean
+nmc_string_to_ternary_full(const char *            str,
+                           NMCStringToTernaryFlags flags,
+                           NMTernary *             val,
+                           GError **               error)
+{
+    gs_free char *str_to_free = NULL;
+    int           i;
+
+    nm_assert(!error || !*error);
+    nm_assert(val);
+
+    str = nm_strstrip_avoid_copy_a(300, str, &str_to_free);
+
+    if (nm_streq0(str, "o")) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           /* TRANSLATORS: the first %s is the partial value entered by
+                            * the user, the second %s a list of compatible values.
+                            */
+                           _("'%s' is ambiguous (%s)"),
+                           str,
+                           "on, off");
+        return FALSE;
+    }
+
+    if (nmc_string_is_valid(str, NM_MAKE_STRV("true", "yes", "on"), NULL))
+        *val = NM_TERNARY_TRUE;
+    else if (nmc_string_is_valid(str, NM_MAKE_STRV("false", "no", "off"), NULL))
+        *val = NM_TERNARY_FALSE;
+    else if (nmc_string_is_valid(
+                 str,
+                 NM_MAKE_STRV("unknown",
+                              "default",
+                              NM_FLAGS_HAS(flags, NMC_STRING_TO_TERNARY_FLAGS_IGNORE_FOR_DEFAULT)
+                                  ? "ignore"
+                                  : NULL),
+                 NULL))
+        *val = NM_TERNARY_DEFAULT;
+    else if ((i = _nm_utils_ascii_str_to_int64(str, 0, -1, 1, -2)) != -2)
+        *val = (NMTernary) i;
+    else {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           _("'%s' is not valid; use [%s], [%s] or [%s]"),
+                           str,
+                           "true, yes, on",
+                           "false, no, off",
+                           "unknown");
+        return FALSE;
+    }
+    return TRUE;
+}
+
+/*
+ * Check whether 'input' is contained in 'allowed' array. It performs case
+ * insensitive comparison and supports shortcut strings if they are unique.
+ * Returns: a pointer to found string in allowed array on success or NULL.
+ * On failure: error->code : 0 - string not found; 1 - string is ambiguous
+ */
+const char *
+_nmc_string_is_valid(const char *input, const char *const *allowed, GError **error)
+{
+    const char *const *p;
+    size_t             input_ln, p_len;
+    const char *       partial_match = NULL;
+    gboolean           ambiguous     = FALSE;
+
+    g_return_val_if_fail(!error || !*error, NULL);
+
+    if (!input || !*input)
+        goto finish;
+
+    input_ln = strlen(input);
+    for (p = allowed; p && *p; p++) {
+        p_len = strlen(*p);
+        if (g_ascii_strncasecmp(input, *p, input_ln) == 0) {
+            if (input_ln == p_len)
+                return *p;
+            if (!partial_match)
+                partial_match = *p;
+            else
+                ambiguous = TRUE;
+        }
+    }
+
+    if (ambiguous) {
+        GString *candidates = g_string_new("");
+
+        for (p = allowed; *p; p++) {
+            if (g_ascii_strncasecmp(input, *p, input_ln) == 0) {
+                if (candidates->len > 0)
+                    g_string_append(candidates, ", ");
+                g_string_append(candidates, *p);
+            }
+        }
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_AMBIGUOUS,
+                           _("'%s' is ambiguous: %s"),
+                           input,
+                           candidates->str);
+        g_string_free(candidates, TRUE);
+        return NULL;
+    }
+finish:
+    if (!partial_match) {
+        char *valid_vals = g_strjoinv(", ", (char **) allowed);
+
+        if (!input || !*input)
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("missing name, try one of [%s]"),
+                               valid_vals);
+        else
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("'%s' not among [%s]"),
+                               input,
+                               valid_vals);
+
+        g_free(valid_vals);
+    }
+
+    return partial_match;
+}
+
+gboolean
+matches(const char *cmd, const char *pattern)
+{
+    size_t len = strlen(cmd);
+    if (!len || len > strlen(pattern))
+        return FALSE;
+    return memcmp(pattern, cmd, len) == 0;
+}
+
+const char *
+nmc_bond_validate_mode(const char *mode, GError **error)
+{
+    unsigned long      mode_int;
+    static const char *valid_modes[] = {"balance-rr",
+                                        "active-backup",
+                                        "balance-xor",
+                                        "broadcast",
+                                        "802.3ad",
+                                        "balance-tlb",
+                                        "balance-alb",
+                                        NULL};
+    if (nmc_string_to_uint(mode, TRUE, 0, 6, &mode_int)) {
+        /* Translate bonding mode numbers to mode names:
+         * https://www.kernel.org/doc/Documentation/networking/bonding.txt
+         */
+        return valid_modes[mode_int];
+    } else
+        return nmc_string_is_valid(mode, valid_modes, error);
+}
+
+NM_UTILS_LOOKUP_STR_DEFINE(
+    nmc_device_state_to_string,
+    NMDeviceState,
+    NM_UTILS_LOOKUP_DEFAULT(N_("unknown")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNMANAGED, N_("unmanaged")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNAVAILABLE, N_("unavailable")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DISCONNECTED, N_("disconnected")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_PREPARE, N_("connecting (prepare)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_CONFIG, N_("connecting (configuring)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_NEED_AUTH, N_("connecting (need authentication)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CONFIG, N_("connecting (getting IP configuration)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CHECK, N_("connecting (checking IP connectivity)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_SECONDARIES,
+                         N_("connecting (starting secondary connections)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_ACTIVATED, N_("connected")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DEACTIVATING, N_("deactivating")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_FAILED, N_("connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_UNKNOWN, N_("unknown")), );
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    _device_state_to_string,
+    NMDeviceState,
+    NM_UTILS_LOOKUP_DEFAULT(NULL),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_PREPARE, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_CONFIG, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_NEED_AUTH, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CONFIG, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_IP_CHECK, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_SECONDARIES, N_("connecting (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_ACTIVATED, N_("connected (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_DEACTIVATING, N_("deactivating (externally)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_FAILED, N_("deactivating (externally)")),
+    NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(), );
+
+const char *
+nmc_device_state_to_string_with_external(NMDevice *device)
+{
+    NMActiveConnection *ac;
+    NMDeviceState       state;
+    const char *        s;
+
+    state = nm_device_get_state(device);
+
+    if ((ac = nm_device_get_active_connection(device))
+        && NM_FLAGS_HAS(nm_active_connection_get_state_flags(ac), NM_ACTIVATION_STATE_FLAG_EXTERNAL)
+        && (s = _device_state_to_string(state)))
+        return s;
+
+    return nmc_device_state_to_string(state);
+}
+
+NM_UTILS_LOOKUP_STR_DEFINE(nmc_device_metered_to_string,
+                           NMMetered,
+                           NM_UTILS_LOOKUP_DEFAULT(N_("unknown")),
+                           NM_UTILS_LOOKUP_ITEM(NM_METERED_YES, N_("yes")),
+                           NM_UTILS_LOOKUP_ITEM(NM_METERED_NO, N_("no")),
+                           NM_UTILS_LOOKUP_ITEM(NM_METERED_GUESS_YES, N_("yes (guessed)")),
+                           NM_UTILS_LOOKUP_ITEM(NM_METERED_GUESS_NO, N_("no (guessed)")),
+                           NM_UTILS_LOOKUP_ITEM(NM_METERED_UNKNOWN, N_("unknown")), );
+
+NM_UTILS_LOOKUP_STR_DEFINE(
+    nmc_device_reason_to_string,
+    NMDeviceStateReason,
+    /* TRANSLATORS: Unknown reason for a device state change (NMDeviceStateReason) */
+    NM_UTILS_LOOKUP_DEFAULT(N_("Unknown")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NONE, N_("No reason given")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_UNKNOWN, N_("Unknown error")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NOW_MANAGED, N_("Device is now managed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NOW_UNMANAGED, N_("Device is now unmanaged")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONFIG_FAILED,
+                         N_("The device could not be readied for configuration")),
+    NM_UTILS_LOOKUP_ITEM(
+        NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE,
+        N_("IP configuration could not be reserved (no available address, timeout, etc.)")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED,
+                         N_("The IP configuration is no longer valid")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NO_SECRETS,
+                         N_("Secrets were required, but not provided")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT,
+                         N_("802.1X supplicant disconnected")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED,
+                         N_("802.1X supplicant configuration failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED, N_("802.1X supplicant failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT,
+                         N_("802.1X supplicant took too long to authenticate")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_START_FAILED,
+                         N_("PPP service failed to start")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_DISCONNECT, N_("PPP service disconnected")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PPP_FAILED, N_("PPP failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_START_FAILED,
+                         N_("DHCP client failed to start")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_ERROR, N_("DHCP client error")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DHCP_FAILED, N_("DHCP client failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SHARED_START_FAILED,
+                         N_("Shared connection service failed to start")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SHARED_FAILED,
+                         N_("Shared connection service failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED,
+                         N_("AutoIP service failed to start")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_ERROR, N_("AutoIP service error")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_FAILED, N_("AutoIP service failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_BUSY, N_("The line is busy")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE, N_("No dial tone")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER,
+                         N_("No carrier could be established")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT,
+                         N_("The dialing request timed out")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED,
+                         N_("The dialing attempt failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED,
+                         N_("Modem initialization failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_APN_FAILED,
+                         N_("Failed to select the specified APN")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING,
+                         N_("Not searching for networks")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED,
+                         N_("Network registration denied")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT,
+                         N_("Network registration timed out")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED,
+                         N_("Failed to register with the requested network")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED, N_("PIN check failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_FIRMWARE_MISSING,
+                         N_("Necessary firmware for the device may be missing")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_REMOVED, N_("The device was removed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SLEEPING, N_("NetworkManager went to sleep")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_REMOVED,
+                         N_("The device's active connection disappeared")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_USER_REQUESTED,
+                         N_("Device disconnected by user or client")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CARRIER, N_("Carrier/link changed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED,
+                         N_("The device's existing connection was assumed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                         N_("The supplicant is now available")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND,
+                         N_("The modem could not be found")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_BT_FAILED,
+                         N_("The Bluetooth connection failed or timed out")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED,
+                         N_("GSM Modem's SIM card not inserted")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED,
+                         N_("GSM Modem's SIM PIN required")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED,
+                         N_("GSM Modem's SIM PUK required")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_WRONG, N_("GSM Modem's SIM wrong")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_INFINIBAND_MODE,
+                         N_("InfiniBand device does not support connected mode")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED,
+                         N_("A dependency of the connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_BR2684_FAILED,
+                         N_("A problem with the RFC 2684 Ethernet over ADSL bridge")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE,
+                         N_("ModemManager is unavailable")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SSID_NOT_FOUND,
+                         N_("The Wi-Fi network could not be found")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED,
+                         N_("A secondary connection of the base connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED, N_("DCB or FCoE setup failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED, N_("teamd control failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_FAILED,
+                         N_("Modem failed or no longer available")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_MODEM_AVAILABLE,
+                         N_("Modem now ready and available")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT, N_("SIM PIN was incorrect")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_NEW_ACTIVATION,
+                         N_("New connection activation was enqueued")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PARENT_CHANGED, N_("The device's parent changed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED,
+                         N_("The device parent's management changed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_OVSDB_FAILED,
+                         N_("Open vSwitch database connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE,
+                         N_("A duplicate IP address was detected")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED,
+                         N_("The selected IP method is not supported")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED,
+                         N_("Failed to configure SR-IOV parameters")),
+    NM_UTILS_LOOKUP_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND,
+                         N_("The Wi-Fi P2P peer could not be found")), );
+
+NM_UTILS_LOOKUP_STR_DEFINE(
+    nm_active_connection_state_reason_to_string,
+    NMActiveConnectionStateReason,
+    /* TRANSLATORS: Unknown reason for a connection state change (NMActiveConnectionStateReason) */
+    NM_UTILS_LOOKUP_DEFAULT(N_("Unknown")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN, N_("Unknown reason")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_NONE,
+                         N_("The connection was disconnected")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED,
+                         N_("Disconnected by user")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED,
+                         N_("The base network connection was interrupted")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED,
+                         N_("The VPN service stopped unexpectedly")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID,
+                         N_("The VPN service returned invalid configuration")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT,
+                         N_("The connection attempt timed out")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT,
+                         N_("The VPN service did not start in time")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
+                         N_("The VPN service failed to start")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, N_("No valid secrets")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED, N_("Invalid secrets")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED,
+                         N_("The connection was removed")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEPENDENCY_FAILED,
+                         N_("Master connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REALIZE_FAILED,
+                         N_("Could not create a software link")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_REMOVED,
+                         N_("The device disappeared")), );
+
+NMActiveConnectionState
+nmc_activation_get_effective_state(NMActiveConnection *active,
+                                   NMDevice *          device,
+                                   const char **       reason)
+{
+    NMActiveConnectionState       ac_state;
+    NMActiveConnectionStateReason ac_reason;
+    NMDeviceState                 dev_state  = NM_DEVICE_STATE_UNKNOWN;
+    NMDeviceStateReason           dev_reason = NM_DEVICE_STATE_REASON_UNKNOWN;
+
+    g_return_val_if_fail(active, NM_ACTIVE_CONNECTION_STATE_UNKNOWN);
+    g_return_val_if_fail(reason, NM_ACTIVE_CONNECTION_STATE_UNKNOWN);
+
+    *reason   = NULL;
+    ac_reason = nm_active_connection_get_state_reason(active);
+
+    if (device) {
+        dev_state  = nm_device_get_state(device);
+        dev_reason = nm_device_get_state_reason(device);
+    }
+
+    ac_state = nm_active_connection_get_state(active);
+    switch (ac_state) {
+    case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED:
+        if (!device || ac_reason != NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED
+            || nm_device_get_active_connection(device) != active) {
+            /* (1)
+             * - we have no device,
+             * - or, @ac_reason is specific
+             * - or, @device no longer references the current @active
+             * >> we complete with @ac_reason. */
+            *reason = gettext(nm_active_connection_state_reason_to_string(ac_reason));
+        } else if (dev_state <= NM_DEVICE_STATE_DISCONNECTED
+                   || dev_state >= NM_DEVICE_STATE_FAILED) {
+            /* (2)
+             * - not (1)
+             * - and, the device is no longer in an activated state,
+             * >> we complete with @dev_reason. */
+            *reason = gettext(nmc_device_reason_to_string(dev_reason));
+        } else {
+            /* (3)
+             * we wait for the device go disconnect. We will get a better
+             * failure reason from the device (2). */
+            return NM_ACTIVE_CONNECTION_STATE_UNKNOWN;
+        }
+        break;
+    case NM_ACTIVE_CONNECTION_STATE_ACTIVATING:
+        /* activating master connection does not automatically activate any slaves, so their
+         * active connection state will not progress beyond ACTIVATING state.
+         * Monitor the device instead. */
+        if (device
+            && (NM_IS_DEVICE_BOND(device) || NM_IS_DEVICE_TEAM(device)
+                || NM_IS_DEVICE_BRIDGE(device))
+            && dev_state >= NM_DEVICE_STATE_IP_CONFIG && dev_state <= NM_DEVICE_STATE_ACTIVATED) {
+            *reason = "master waiting for slaves";
+            return NM_ACTIVE_CONNECTION_STATE_ACTIVATED;
+        }
+        break;
+    default:
+        break;
+    }
+
+    return ac_state;
+}
+
+static gboolean
+can_show_utf8(void)
+{
+    static gboolean can_show_utf8_set = FALSE;
+    static gboolean can_show_utf8     = TRUE;
+    char *          locale_str;
+
+    if (G_LIKELY(can_show_utf8_set))
+        return can_show_utf8;
+
+    if (!g_get_charset(NULL)) {
+        /* Non-UTF-8 locale */
+        locale_str = g_locale_from_utf8("\342\226\202\342\226\204\342\226\206\342\226\210",
+                                        -1,
+                                        NULL,
+                                        NULL,
+                                        NULL);
+        if (locale_str)
+            g_free(locale_str);
+        else
+            can_show_utf8 = FALSE;
+    }
+
+    return can_show_utf8;
+}
+
+static gboolean
+can_show_graphics(void)
+{
+    static gboolean can_show_graphics_set = FALSE;
+    static gboolean can_show_graphics     = TRUE;
+
+    if (G_LIKELY(can_show_graphics_set))
+        return can_show_graphics;
+
+    can_show_graphics = can_show_utf8();
+
+    /* The linux console font typically doesn't have characters we need */
+    if (g_strcmp0(g_getenv("TERM"), "linux") == 0)
+        can_show_graphics = FALSE;
+
+    return can_show_graphics;
+}
+
+/**
+ * nmc_wifi_strength_bars:
+ * @strength: the access point strength, from 0 to 100
+ *
+ * Converts @strength into a 4-character-wide graphical representation of
+ * strength suitable for printing to stdout. If the current locale and terminal
+ * support it, this will use unicode graphics characters to represent
+ * "bars". Otherwise, it will use 0 to 4 asterisks.
+ *
+ * Returns: the graphical representation of the access point strength
+ */
+const char *
+nmc_wifi_strength_bars(guint8 strength)
+{
+    if (!can_show_graphics())
+        return nm_utils_wifi_strength_bars(strength);
+
+    if (strength > 80)
+        return /* ▂▄▆█ */ "\342\226\202\342\226\204\342\226\206\342\226\210";
+    else if (strength > 55)
+        return /* ▂▄▆_ */ "\342\226\202\342\226\204\342\226\206_";
+    else if (strength > 30)
+        return /* ▂▄__ */ "\342\226\202\342\226\204__";
+    else if (strength > 5)
+        return /* ▂___ */ "\342\226\202___";
+    else
+        return /* ____ */ "____";
+}
+
+/**
+ * nmc_utils_password_subst_char:
+ *
+ * Returns: the string substituted when hiding actual password glyphs
+ */
+const char *
+nmc_password_subst_char(void)
+{
+    if (can_show_graphics())
+        return "\u2022"; /* Bullet */
+    else
+        return "*";
+}
+
+/*
+ * We actually use a small part of qrcodegen.c, but we'd prefer to keep it
+ * intact. Include it instead of linking to it to give the compiler a
+ * chance to optimize bits we don't need away.
+ */
+
+#pragma GCC visibility push(hidden)
+NM_PRAGMA_WARNING_DISABLE("-Wdeclaration-after-statement")
+#undef NDEBUG
+#define NDEBUG
+#include "qrcodegen.c"
+NM_PRAGMA_WARNING_REENABLE
+#pragma GCC visibility pop
+
+void
+nmc_print_qrcode(const char *str)
+{
+    uint8_t  tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
+    uint8_t  qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
+    gboolean term_linux;
+    int      size;
+    int      x;
+    int      y;
+
+    term_linux = g_strcmp0(g_getenv("TERM"), "linux") == 0;
+    if (!term_linux && !can_show_graphics())
+        return;
+
+    if (!qrcodegen_encodeText(str,
+                              tempBuffer,
+                              qrcode,
+                              qrcodegen_Ecc_LOW,
+                              qrcodegen_VERSION_MIN,
+                              qrcodegen_VERSION_MAX,
+                              qrcodegen_Mask_AUTO,
+                              FALSE)) {
+        return;
+    }
+
+    size = qrcodegen_getSize(qrcode);
+
+    g_print("\n");
+
+    if (term_linux) {
+        /* G1 alternate character set on Linux console. */
+        for (y = -1; y < size + 1; y += 1) {
+            g_print("  \033[37;40;1m\016");
+            for (x = -1; x < size + 1; x++) {
+                g_print(qrcodegen_getModule(qrcode, x, y) ? "  " : "\060\060");
+            }
+            g_print("\017\033[0m\n");
+        }
+    } else {
+        /* UTF-8 */
+        for (y = -2; y < size + 2; y += 2) {
+            g_print("  \033[37;40m");
+            for (x = -2; x < size + 2; x++) {
+                bool top    = qrcodegen_getModule(qrcode, x, y);
+                bool bottom = qrcodegen_getModule(qrcode, x, y + 1);
+                if (top) {
+                    g_print(bottom ? " " : "\u2584");
+                } else {
+                    g_print(bottom ? "\u2580" : "\u2588");
+                }
+            }
+            g_print("\033[0m\n");
+        }
+    }
+}
+
+/**
+ * nmc_utils_read_passwd_file:
+ * @passwd_file: file with passwords to parse
+ * @out_error_line: returns in case of a syntax error in the file, the line
+ *   on which it occurred.
+ * @error: location to store error, or %NULL
+ *
+ * Parse passwords given in @passwd_file and insert them into a hash table.
+ * Example of @passwd_file contents:
+ *   wifi.psk:tajne heslo
+ *   802-1x.password:krakonos
+ *   802-11-wireless-security:leap-password:my leap password
+ *
+ * Returns: (transfer full): hash table with parsed passwords, or %NULL on an error
+ */
+GHashTable *
+nmc_utils_read_passwd_file(const char *passwd_file, gssize *out_error_line, GError **error)
+{
+    nm_auto_clear_secret_ptr NMSecretPtr contents = {0};
+
+    NM_SET_OUT(out_error_line, -1);
+
+    if (!passwd_file)
+        return g_hash_table_new_full(nm_str_hash,
+                                     g_str_equal,
+                                     g_free,
+                                     (GDestroyNotify) nm_free_secret);
+
+    if (!nm_utils_file_get_contents(-1,
+                                    passwd_file,
+                                    1024 * 1024,
+                                    NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
+                                    &contents.str,
+                                    &contents.len,
+                                    NULL,
+                                    error))
+        return NULL;
+
+    return nmc_utils_parse_passwd_file(contents.str, out_error_line, error);
+}
+
+GHashTable *
+nmc_utils_parse_passwd_file(char *   contents /* will be modified */,
+                            gssize * out_error_line,
+                            GError **error)
+{
+    gs_unref_hashtable GHashTable *pwds_hash = NULL;
+    const char *                   contents_str;
+    gsize                          contents_line;
+
+    pwds_hash =
+        g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, (GDestroyNotify) nm_free_secret);
+
+    NM_SET_OUT(out_error_line, -1);
+
+    contents_str  = contents;
+    contents_line = 0;
+    while (contents_str[0]) {
+        nm_auto_free_secret char *l_hash_key = NULL;
+        nm_auto_free_secret char *l_hash_val = NULL;
+        const char *              l_content_line;
+        const char *              l_setting;
+        const char *              l_prop;
+        const char *              l_val;
+        const char *              s;
+        gsize                     l_hash_val_len;
+
+        /* consume first line. As line delimiters we accept "\r\n", "\n", and "\r". */
+        l_content_line = contents_str;
+        s              = l_content_line;
+        while (!NM_IN_SET(s[0], '\0', '\r', '\n'))
+            s++;
+        if (s[0] != '\0') {
+            if (s[0] == '\r' && s[1] == '\n') {
+                ((char *) s)[0] = '\0';
+                s += 2;
+            } else {
+                ((char *) s)[0] = '\0';
+                s += 1;
+            }
+        }
+        contents_str = s;
+        contents_line++;
+
+        l_content_line = nm_str_skip_leading_spaces(l_content_line);
+        if (NM_IN_SET(l_content_line[0], '\0', '#')) {
+            /* a comment or empty line. Ignore. */
+            continue;
+        }
+
+        l_setting = l_content_line;
+
+        s = l_setting;
+        while (!NM_IN_SET(s[0], '\0', ':', '='))
+            s++;
+        if (s[0] == '\0') {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("missing colon for \"<setting>.<property>:<secret>\" format"));
+            return NULL;
+        }
+        ((char *) s)[0] = '\0';
+        s++;
+
+        l_val = s;
+
+        g_strchomp((char *) l_setting);
+
+        nm_assert(nm_str_is_stripped(l_setting));
+
+        s = strchr(l_setting, '.');
+        if (!s) {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("missing dot for \"<setting>.<property>:<secret>\" format"));
+            return NULL;
+        } else if (s == l_setting) {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("missing setting for \"<setting>.<property>:<secret>\" format"));
+            return NULL;
+        }
+        ((char *) s)[0] = '\0';
+        s++;
+
+        l_prop = s;
+        if (l_prop[0] == '\0') {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               _("missing property for \"<setting>.<property>:<secret>\" format"));
+            return NULL;
+        }
+
+        /* Accept wifi-sec or wifi instead of cumbersome '802-11-wireless-security' */
+        if (NM_IN_STRSET(l_setting, "wifi-sec", "wifi"))
+            l_setting = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+
+        if (nm_setting_lookup_type(l_setting) == G_TYPE_INVALID) {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("invalid setting name"));
+            return NULL;
+        }
+
+        if (nm_streq(l_setting, "vpn") && NM_STR_HAS_PREFIX(l_prop, "secret.")) {
+            /* in 1.12.0, we wrongly required the VPN secrets to be named
+             * "vpn.secret". It should be "vpn.secrets". Work around it
+             * (rh#1628833). */
+            l_hash_key = g_strdup_printf("vpn.secrets.%s", &l_prop[NM_STRLEN("secret.")]);
+        } else
+            l_hash_key = g_strdup_printf("%s.%s", l_setting, l_prop);
+
+        if (!g_utf8_validate(l_hash_key, -1, NULL)) {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("property name is not UTF-8"));
+            return NULL;
+        }
+
+        /* Support backslash escaping in the secret value. We strip non-escaped leading/trailing whitespaces. */
+        s = nm_utils_buf_utf8safe_unescape(l_val,
+                                           NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES,
+                                           &l_hash_val_len,
+                                           (gpointer *) &l_hash_val);
+        if (!l_hash_val)
+            l_hash_val = g_strdup(s);
+
+        if (!g_utf8_validate(l_hash_val, -1, NULL)) {
+            /* In some cases it might make sense to support binary secrets (like the WPA-PSK which has no
+             * defined encoding. However, all API that follows can only handle UTF-8, and no mechanism
+             * to escape the secrets. Reject non-UTF-8 early. */
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("secret is not UTF-8"));
+            return NULL;
+        }
+
+        if (strlen(l_hash_val) != l_hash_val_len) {
+            NM_SET_OUT(out_error_line, contents_line);
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("secret is not UTF-8"));
+            return NULL;
+        }
+
+        g_hash_table_insert(pwds_hash, g_steal_pointer(&l_hash_key), g_steal_pointer(&l_hash_val));
+    }
+
+    return g_steal_pointer(&pwds_hash);
+}
diff --git a/src/libnmc-base/nm-client-utils.h b/src/libnmc-base/nm-client-utils.h
new file mode 100644
index 00000000..1fc18aca
--- /dev/null
+++ b/src/libnmc-base/nm-client-utils.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2010 - 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_CLIENT_UTILS_H__
+#define __NM_CLIENT_UTILS_H__
+
+#include "nm-active-connection.h"
+#include "nm-device.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+
+const NMObject **nmc_objects_sort_by_path(const NMObject *const *objs, gssize len);
+
+const char *_nmc_string_is_valid(const char *input, const char *const *allowed, GError **error);
+
+#define nmc_string_is_valid(input, allowed, error) \
+    _nmc_string_is_valid((input), NM_CAST_STRV_CC(allowed), (error))
+
+gboolean nmc_string_to_uint(const char *       str,
+                            gboolean           range_check,
+                            unsigned long int  min,
+                            unsigned long int  max,
+                            unsigned long int *value);
+gboolean nmc_string_to_bool(const char *str, gboolean *val_bool, GError **error);
+
+typedef enum {
+    NMC_STRING_TO_TERNARY_FLAGS_NONE               = 0,
+    NMC_STRING_TO_TERNARY_FLAGS_IGNORE_FOR_DEFAULT = (1LL << 0),
+} NMCStringToTernaryFlags;
+
+gboolean nmc_string_to_ternary_full(const char *            str,
+                                    NMCStringToTernaryFlags flags,
+                                    NMTernary *             val,
+                                    GError **               error);
+
+static inline gboolean
+nmc_string_to_ternary(const char *str, NMTernary *val, GError **error)
+{
+    return nmc_string_to_ternary_full(str, NMC_STRING_TO_TERNARY_FLAGS_NONE, val, error);
+}
+
+gboolean matches(const char *cmd, const char *pattern);
+
+/* FIXME: don't expose this function on its own, at least not from this file. */
+const char *nmc_bond_validate_mode(const char *mode, GError **error);
+
+const char *nmc_device_state_to_string_with_external(NMDevice *device);
+
+const char *nm_active_connection_state_reason_to_string(NMActiveConnectionStateReason reason);
+const char *nmc_device_state_to_string(NMDeviceState state);
+const char *nmc_device_reason_to_string(NMDeviceStateReason reason);
+const char *nmc_device_metered_to_string(NMMetered value);
+
+NMActiveConnectionState nmc_activation_get_effective_state(NMActiveConnection *active,
+                                                           NMDevice *          device,
+                                                           const char **       reason);
+
+const char *nmc_wifi_strength_bars(guint8 strength);
+
+const char *nmc_password_subst_char(void);
+
+void nmc_print_qrcode(const char *str);
+
+GHashTable *nmc_utils_parse_passwd_file(char *contents, gssize *out_error_line, GError **error);
+
+GHashTable *
+nmc_utils_read_passwd_file(const char *passwd_file, gssize *out_error_line, GError **error);
+
+#endif /* __NM_CLIENT_UTILS_H__ */
diff --git a/src/libnmc-base/nm-polkit-listener.c b/src/libnmc-base/nm-polkit-listener.c
new file mode 100644
index 00000000..29c25b4e
--- /dev/null
+++ b/src/libnmc-base/nm-polkit-listener.c
@@ -0,0 +1,910 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+/**
+ * SECTION:nm-polkit-listener
+ * @short_description: A polkit agent listener
+ *
+ * #NMPolkitListener is the polkit agent listener used by nmcli and nmtui.
+ * http://www.freedesktop.org/software/polkit/docs/latest/index.html
+ *
+ * For an example polkit agent you can look at polkit source tree:
+ * http://cgit.freedesktop.org/polkit/tree/src/polkitagent/polkitagenttextlistener.c
+ * http://cgit.freedesktop.org/polkit/tree/src/programs/pkttyagent.c
+ * or LXDE polkit agent:
+ * http://git.lxde.org/gitweb/?p=debian/lxpolkit.git;a=blob;f=src/lxpolkit-listener.c
+ * https://github.com/lxde/lxqt-policykit/tree/master/src
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-polkit-listener.h"
+
+#include <gio/gio.h>
+#include <pwd.h>
+#include <fcntl.h>
+
+#include "libnm-glib-aux/nm-dbus-aux.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-glib-aux/nm-secret-utils.h"
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-core-aux-intern/nm-auth-subject.h"
+#include "c-list/src/c-list.h"
+
+#define LOGIND_BUS_NAME "org.freedesktop.login1"
+#define POLKIT_BUS_NAME "org.freedesktop.PolicyKit1"
+
+#define POLKIT_AUTHORITY_OBJ_PATH   "/org/freedesktop/PolicyKit1/Authority"
+#define POLKIT_AUTHORITY_IFACE_NAME "org.freedesktop.PolicyKit1.Authority"
+
+#define POLKIT_AGENT_OBJ_PATH       "/org/freedesktop/PolicyKit1/AuthenticationAgent"
+#define POLKIT_AGENT_DBUS_INTERFACE "org.freedesktop.PolicyKit1.AuthenticationAgent"
+
+#define LOGIND_OBJ_PATH          "/org/freedesktop/login1"
+#define LOGIND_MANAGER_INTERFACE "org.freedesktop.login1.Manager"
+
+#define NM_POLKIT_LISTENER_DBUS_CONNECTION "dbus-connection"
+#define NM_POLKIT_LISTENER_SESSION_AGENT   "session-agent"
+
+#define POLKIT_DBUS_ERROR_FAILED "org.freedesktop.PolicyKit1.Error.Failed"
+
+/*****************************************************************************/
+
+enum { REGISTERED, REQUEST_SYNC, ERROR, LAST_SIGNAL };
+
+static guint signals[LAST_SIGNAL] = {0};
+
+struct _NMPolkitListener {
+    GObject          parent;
+    GDBusConnection *dbus_connection;
+    char *           name_owner;
+    GCancellable *   cancellable;
+    GMainContext *   main_context;
+    CList            request_lst_head;
+    guint            pk_auth_agent_reg_id;
+    guint            name_owner_changed_id;
+    bool             session_agent : 1;
+};
+
+struct _NMPolkitListenerClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMPolkitListener, nm_polkit_listener, G_TYPE_OBJECT);
+
+/*****************************************************************************/
+
+typedef struct {
+    CList request_lst;
+
+    NMPolkitListener *     listener;
+    GSource *              child_stdout_watch_source;
+    GSource *              child_stdin_watch_source;
+    GDBusMethodInvocation *dbus_invocation;
+    char *                 action_id;
+    char *                 message;
+    char *                 username;
+    char *                 cookie;
+    NMStrBuf               in_buffer;
+    NMStrBuf               out_buffer;
+    gsize                  out_buffer_offset;
+
+    int child_stdout;
+    int child_stdin;
+
+    bool request_any_response : 1;
+    bool request_is_completed : 1;
+} AuthRequest;
+
+static const GDBusInterfaceInfo interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+    POLKIT_AGENT_DBUS_INTERFACE,
+    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+        NM_DEFINE_GDBUS_METHOD_INFO("BeginAuthentication",
+                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                        NM_DEFINE_GDBUS_ARG_INFO("action_id", "s"),
+                                        NM_DEFINE_GDBUS_ARG_INFO("message", "s"),
+                                        NM_DEFINE_GDBUS_ARG_INFO("icon_name", "s"),
+                                        NM_DEFINE_GDBUS_ARG_INFO("details", "a{ss}"),
+                                        NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"),
+                                        NM_DEFINE_GDBUS_ARG_INFO("identities", "a(sa{sv})"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO("CancelAuthentication",
+                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                        NM_DEFINE_GDBUS_ARG_INFO("cookie", "s"), ), ), ), );
+
+static void
+auth_request_complete(AuthRequest *request, gboolean success)
+{
+    c_list_unlink(&request->request_lst);
+
+    if (success)
+        g_dbus_method_invocation_return_value(request->dbus_invocation, NULL);
+    else {
+        g_dbus_method_invocation_return_dbus_error(request->dbus_invocation,
+                                                   POLKIT_DBUS_ERROR_FAILED,
+                                                   "");
+    }
+
+    nm_clear_g_free(&request->action_id);
+    nm_clear_g_free(&request->message);
+    nm_clear_g_free(&request->username);
+    nm_clear_g_free(&request->cookie);
+    nm_clear_g_source_inst(&request->child_stdout_watch_source);
+    nm_clear_g_source_inst(&request->child_stdin_watch_source);
+
+    nm_str_buf_destroy(&request->in_buffer);
+    nm_str_buf_destroy(&request->out_buffer);
+
+    if (request->child_stdout != -1) {
+        nm_close(request->child_stdout);
+        request->child_stdout = -1;
+    }
+
+    if (request->child_stdin != -1) {
+        nm_close(request->child_stdin);
+        request->child_stdin = -1;
+    }
+
+    nm_g_slice_free(request);
+}
+
+static gboolean
+uid_to_name(uid_t uid, gboolean *out_cached, char **out_name)
+{
+    if (!*out_cached) {
+        *out_cached = TRUE;
+        *out_name   = nm_utils_uid_to_name(uid);
+    }
+    return !!(*out_name);
+}
+
+static char *
+choose_identity(GVariant *identities)
+{
+    GVariantIter  identity_iter;
+    GVariant *    details_tmp;
+    const char *  kind;
+    gs_free char *username_first = NULL;
+    gs_free char *username_root  = NULL;
+    const char *  user;
+
+    /* Choose identity. First try current user, then root, and else
+     * take the first one we find. */
+
+    user = getenv("USER");
+
+    g_variant_iter_init(&identity_iter, identities);
+    while (g_variant_iter_next(&identity_iter, "(&s@a{sv})", &kind, &details_tmp)) {
+        gs_unref_variant GVariant *details = g_steal_pointer(&details_tmp);
+
+        if (nm_streq(kind, "unix-user")) {
+            gs_unref_variant GVariant *v = NULL;
+
+            v = g_variant_lookup_value(details, "uid", G_VARIANT_TYPE_UINT32);
+            if (v) {
+                guint32       uid    = g_variant_get_uint32(v);
+                gs_free char *u      = NULL;
+                gboolean      cached = FALSE;
+
+                if (user) {
+                    if (!uid_to_name(uid, &cached, &u))
+                        continue;
+                    if (nm_streq(u, user))
+                        return g_steal_pointer(&u);
+                }
+                if (!username_root && uid == 0) {
+                    if (!uid_to_name(uid, &cached, &u))
+                        continue;
+                    username_root = g_strdup(u);
+                    if (!user)
+                        break;
+                }
+                if (!username_root && !username_first) {
+                    if (!uid_to_name(uid, &cached, &u))
+                        continue;
+                    username_first = g_strdup(u);
+                }
+            }
+        }
+    }
+
+    if (username_root)
+        return g_steal_pointer(&username_root);
+
+    if (username_first)
+        return g_steal_pointer(&username_first);
+
+    return NULL;
+}
+
+static void
+agent_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMPolkitListener *listener        = NM_POLKIT_LISTENER(user_data);
+    GDBusConnection * dbus_connection = G_DBUS_CONNECTION(source_object);
+    gs_free_error GError *error       = NULL;
+    gs_unref_variant GVariant *ret    = NULL;
+
+    ret = g_dbus_connection_call_finish(dbus_connection, res, &error);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        return;
+    }
+
+    if (ret) {
+        g_signal_emit(listener, signals[REGISTERED], 0);
+    } else {
+        g_signal_emit(listener, signals[ERROR], 0, error->message);
+    }
+}
+
+static void
+agent_register(NMPolkitListener *self, const char *session_id)
+{
+    const char *    locale                         = NULL;
+    gs_unref_object NMAuthSubject *subject         = NULL;
+    GVariant *                     subject_variant = NULL;
+
+    locale = g_getenv("LANG");
+    if (locale == NULL) {
+        locale = "en_US.UTF-8";
+    }
+
+    if (self->session_agent) {
+        subject = nm_auth_subject_new_unix_session(session_id);
+    } else {
+        subject = nm_auth_subject_new_unix_process_self();
+    }
+    subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject);
+
+    g_dbus_connection_call(
+        self->dbus_connection,
+        self->name_owner,
+        POLKIT_AUTHORITY_OBJ_PATH,
+        POLKIT_AUTHORITY_IFACE_NAME,
+        "RegisterAuthenticationAgent",
+        g_variant_new("(@(sa{sv})ss)", subject_variant, locale, POLKIT_AGENT_OBJ_PATH),
+        NULL,
+        G_DBUS_CALL_FLAGS_NONE,
+        -1,
+        self->cancellable,
+        agent_register_cb,
+        self);
+}
+
+static void
+agent_unregister(NMPolkitListener *self)
+{
+    gs_unref_object NMAuthSubject *subject         = NULL;
+    GVariant *                     subject_variant = NULL;
+
+    subject         = nm_auth_subject_new_unix_process_self();
+    subject_variant = nm_auth_subject_unix_to_polkit_gvariant(subject);
+
+    g_dbus_connection_call(self->dbus_connection,
+                           self->name_owner,
+                           POLKIT_AUTHORITY_OBJ_PATH,
+                           POLKIT_AUTHORITY_IFACE_NAME,
+                           "UnregisterAuthenticationAgent",
+                           g_variant_new("(@(sa{sv})s)", subject_variant, POLKIT_AGENT_OBJ_PATH),
+                           NULL,
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           NULL,
+                           NULL,
+                           self);
+}
+
+static void
+retrieve_session_id_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMPolkitListener *        listener = NM_POLKIT_LISTENER(user_data);
+    char *                    session_id;
+    guint32                   session_uid;
+    nm_auto_free_variant_iter GVariantIter *iter = NULL;
+    gs_unref_variant GVariant *ret               = NULL;
+    gs_free_error GError *error                  = NULL;
+    gs_free char *        err_str                = NULL;
+    uid_t                 uid                    = getuid();
+
+    ret = g_dbus_connection_call_finish(listener->dbus_connection, res, &error);
+
+    if (nm_utils_error_is_cancelled(error)) {
+        return;
+    }
+
+    if (ret) {
+        g_variant_get_child(ret, 0, "a(susso)", &iter);
+
+        while (
+            g_variant_iter_next(iter, "(&su@s@s@o)", &session_id, &session_uid, NULL, NULL, NULL)) {
+            if (session_uid == uid) {
+                agent_register(listener, session_id);
+                return;
+            }
+        }
+        err_str = g_strdup_printf(_("Could not find any session id for uid %d"), uid);
+    } else {
+        err_str = g_strdup_printf(_("Could not retrieve session id: %s"), error->message);
+    }
+
+    g_signal_emit(listener, signals[ERROR], 0, err_str);
+}
+
+static void
+retrieve_session_id(NMPolkitListener *self)
+{
+    g_dbus_connection_call(self->dbus_connection,
+                           LOGIND_BUS_NAME,
+                           LOGIND_OBJ_PATH,
+                           LOGIND_MANAGER_INTERFACE,
+                           "ListSessions",
+                           NULL,
+                           G_VARIANT_TYPE("(a(susso))"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           self->cancellable,
+                           retrieve_session_id_cb,
+                           self);
+}
+
+static gboolean
+io_watch_can_write(int fd, GIOCondition condition, gpointer user_data)
+{
+    AuthRequest *request = user_data;
+    gssize       n_written;
+
+    if (NM_FLAGS_ANY(condition, (G_IO_HUP | G_IO_ERR)))
+        goto done;
+
+    n_written =
+        write(request->child_stdin,
+              &((nm_str_buf_get_str_unsafe(&request->out_buffer))[request->out_buffer_offset]),
+              request->out_buffer.len - request->out_buffer_offset);
+
+    if (n_written < 0 && errno != EAGAIN)
+        goto done;
+
+    if (n_written > 0) {
+        if ((gsize) n_written >= (request->out_buffer.len - request->out_buffer_offset)) {
+            nm_assert((gsize) n_written == (request->out_buffer.len - request->out_buffer_offset));
+            goto done;
+        }
+        request->out_buffer_offset += (gsize) n_written;
+    }
+
+    return G_SOURCE_CONTINUE;
+
+done:
+    nm_str_buf_set_size(&request->out_buffer, 0, TRUE, FALSE);
+    request->out_buffer_offset = 0;
+    nm_clear_g_source_inst(&request->child_stdin_watch_source);
+
+    if (request->request_is_completed)
+        auth_request_complete(request, TRUE);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+queue_string_to_helper(AuthRequest *request, const char *response)
+{
+    g_return_if_fail(response);
+
+    if (!nm_str_buf_is_initalized(&request->out_buffer))
+        nm_str_buf_init(&request->out_buffer, strlen(response) + 2u, TRUE);
+
+    nm_str_buf_append(&request->out_buffer, response);
+    nm_str_buf_ensure_trailing_c(&request->out_buffer, '\n');
+
+    if (!request->child_stdin_watch_source) {
+        request->child_stdin_watch_source = nm_g_unix_fd_source_new(request->child_stdin,
+                                                                    G_IO_OUT | G_IO_ERR | G_IO_HUP,
+                                                                    G_PRIORITY_DEFAULT,
+                                                                    io_watch_can_write,
+                                                                    request,
+                                                                    NULL);
+        g_source_attach(request->child_stdin_watch_source, request->listener->main_context);
+    }
+}
+
+static gboolean
+io_watch_have_data(int fd, GIOCondition condition, gpointer user_data)
+{
+    AuthRequest *request = user_data;
+    gboolean     auth_result;
+    gssize       n_read;
+
+    if (NM_FLAGS_ANY(condition, G_IO_HUP | G_IO_ERR))
+        n_read = -EIO;
+    else
+        n_read = nm_utils_fd_read(fd, &request->in_buffer);
+
+    if (n_read <= 0) {
+        if (n_read == -EAGAIN) {
+            /* wait longer. */
+            return G_SOURCE_CONTINUE;
+        }
+
+        /* Either an error or EOF happened. The data we parsed so far was not relevant.
+         * Regardless of what we still have unprocessed in the receive buffers, we are done.
+         *
+         * We would expect that the other side completed with SUCCESS or FAILURE. Apparently
+         * it didn't. If we had any good request, we assume success. */
+        auth_result = request->request_any_response;
+        goto out;
+    }
+
+    while (TRUE) {
+        char *      line_terminator;
+        const char *line;
+
+        line            = nm_str_buf_get_str(&request->in_buffer);
+        line_terminator = (char *) strchr(line, '\n');
+        if (!line_terminator) {
+            /* We don't have a complete line. Wait longer. */
+            return G_SOURCE_CONTINUE;
+        }
+        line_terminator[0] = '\0';
+
+        if (NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_OFF ")
+            || NM_STR_HAS_PREFIX(line, "PAM_PROMPT_ECHO_ON ")) {
+            nm_auto_free_secret char *response = NULL;
+
+            /* FIXME(cli-async): emit signal and wait for response (blocking) */
+            g_signal_emit(request->listener,
+                          signals[REQUEST_SYNC],
+                          0,
+                          request->action_id,
+                          request->message,
+                          request->username,
+                          &response);
+
+            if (response) {
+                queue_string_to_helper(request, response);
+                request->request_any_response = TRUE;
+                goto erase_line;
+            }
+            auth_result = FALSE;
+        } else if (nm_streq(line, "SUCCESS"))
+            auth_result = TRUE;
+        else if (nm_streq(line, "FAILURE"))
+            auth_result = FALSE;
+        else if (NM_STR_HAS_PREFIX(line, "PAM_ERROR_MSG ")
+                 || NM_STR_HAS_PREFIX(line, "PAM_TEXT_INFO ")) {
+            /* ignore. */
+            goto erase_line;
+        } else {
+            /* unknown command. Fail. */
+            auth_result = FALSE;
+        }
+
+        break;
+erase_line:
+        nm_str_buf_erase(&request->in_buffer, 0, line_terminator - line + 1u, TRUE);
+    }
+
+out:
+    request->request_is_completed = TRUE;
+    nm_clear_g_source_inst(&request->child_stdout_watch_source);
+    if (auth_result && request->child_stdin_watch_source) {
+        /* we need to wait for the buffer to send the response. */
+    } else
+        auth_request_complete(request, auth_result);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+begin_authentication(AuthRequest *request)
+{
+    int         fd_flags;
+    const char *helper_argv[] = {
+        POLKIT_AGENT_HELPER_1_PATH,
+        request->username,
+        NULL,
+    };
+
+    if (!request->username) {
+        auth_request_complete(request, FALSE);
+        return;
+    }
+
+    if (!g_spawn_async_with_pipes(NULL,
+                                  (char **) helper_argv,
+                                  NULL,
+                                  G_SPAWN_STDERR_TO_DEV_NULL,
+                                  NULL,
+                                  NULL,
+                                  NULL,
+                                  &request->child_stdin,
+                                  &request->child_stdout,
+                                  NULL,
+                                  NULL)) {
+        /* not findind the PolicyKit setuid helper is a critical error */
+        request->child_stdin  = -1;
+        request->child_stdout = -1;
+        g_signal_emit(request->listener,
+                      signals[ERROR],
+                      0,
+                      "The PolicyKit setuid helper 'polkit-agent-helper-1' has not been found");
+
+        auth_request_complete(request, FALSE);
+        return;
+    }
+
+    fd_flags = fcntl(request->child_stdin, F_GETFD, 0);
+    fcntl(request->child_stdin, F_SETFL, fd_flags | O_NONBLOCK);
+
+    fd_flags = fcntl(request->child_stdout, F_GETFD, 0);
+    fcntl(request->child_stdout, F_SETFL, fd_flags | O_NONBLOCK);
+
+    request->child_stdout_watch_source = nm_g_unix_fd_source_new(request->child_stdout,
+                                                                 G_IO_IN | G_IO_ERR | G_IO_HUP,
+                                                                 G_PRIORITY_DEFAULT,
+                                                                 io_watch_have_data,
+                                                                 request,
+                                                                 NULL);
+    g_source_attach(request->child_stdout_watch_source, request->listener->main_context);
+
+    /* Write the cookie on stdin so it can't be seen by other processes */
+    queue_string_to_helper(request, request->cookie);
+
+    return;
+}
+
+static AuthRequest *
+get_request(NMPolkitListener *listener, const char *cookie)
+{
+    AuthRequest *request;
+
+    c_list_for_each_entry (request, &listener->request_lst_head, request_lst) {
+        if (nm_streq0(cookie, request->cookie)) {
+            return request;
+        }
+    }
+    return NULL;
+}
+
+static AuthRequest *
+create_request(NMPolkitListener *     listener,
+               GDBusMethodInvocation *invocation,
+               const char *           action_id,
+               const char *           message,
+               char *                 username_take,
+               const char *           cookie)
+{
+    AuthRequest *request;
+
+    request  = g_slice_new(AuthRequest);
+    *request = (AuthRequest){
+        .listener             = listener,
+        .dbus_invocation      = invocation,
+        .action_id            = g_strdup(action_id),
+        .message              = g_strdup(message),
+        .username             = g_steal_pointer(&username_take),
+        .cookie               = g_strdup(cookie),
+        .request_any_response = FALSE,
+        .request_is_completed = FALSE,
+    };
+
+    nm_str_buf_init(&request->in_buffer, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
+
+    c_list_link_tail(&listener->request_lst_head, &request->request_lst);
+    return request;
+}
+
+static void
+dbus_method_call_cb(GDBusConnection *      connection,
+                    const char *           sender,
+                    const char *           object_path,
+                    const char *           interface_name,
+                    const char *           method_name,
+                    GVariant *             parameters,
+                    GDBusMethodInvocation *invocation,
+                    gpointer               user_data)
+{
+    NMPolkitListener *listener = NM_POLKIT_LISTENER(user_data);
+    const char *      action_id;
+    const char *      message;
+    const char *      cookie;
+    AuthRequest *     request;
+    gs_unref_variant GVariant *identities_gvariant = NULL;
+
+    if (nm_streq(method_name, "BeginAuthentication")) {
+        g_variant_get(parameters,
+                      "(&s&s&s@a{ss}&s@a(sa{sv}))",
+                      &action_id,
+                      &message,
+                      NULL,
+                      NULL,
+                      &cookie,
+                      &identities_gvariant);
+
+        request = create_request(listener,
+                                 invocation,
+                                 action_id,
+                                 message,
+                                 choose_identity(identities_gvariant),
+                                 cookie);
+        begin_authentication(request);
+        return;
+    }
+
+    if (nm_streq(method_name, "CancelAuthentication")) {
+        g_variant_get(parameters, "&s", &cookie);
+        request = get_request(listener, cookie);
+
+        if (!request) {
+            gs_free char *msg = NULL;
+
+            msg = g_strdup_printf("No pending authentication request for cookie '%s'", cookie);
+            g_dbus_method_invocation_return_dbus_error(invocation, POLKIT_DBUS_ERROR_FAILED, msg);
+            return;
+        }
+
+        /* Complete a cancelled request with success. */
+        auth_request_complete(request, TRUE);
+        return;
+    }
+
+    g_dbus_method_invocation_return_error(invocation,
+                                          G_DBUS_ERROR,
+                                          G_DBUS_ERROR_UNKNOWN_METHOD,
+                                          "Unknown method %s",
+                                          method_name);
+}
+
+static gboolean
+export_dbus_iface(NMPolkitListener *self, GError **error)
+{
+    GDBusInterfaceVTable interface_vtable = {
+        .method_call  = dbus_method_call_cb,
+        .set_property = NULL,
+        .get_property = NULL,
+    };
+
+    g_return_val_if_fail(NM_IS_POLKIT_LISTENER(self), FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+
+    /* Agent listener iface has been exported already */
+    if (self->pk_auth_agent_reg_id) {
+        return TRUE;
+    }
+
+    self->pk_auth_agent_reg_id =
+        g_dbus_connection_register_object(self->dbus_connection,
+                                          POLKIT_AGENT_OBJ_PATH,
+                                          (GDBusInterfaceInfo *) &interface_info,
+                                          &interface_vtable,
+                                          self,
+                                          NULL,
+                                          error);
+    if (!self->pk_auth_agent_reg_id) {
+        g_signal_emit(self,
+                      signals[ERROR],
+                      0,
+                      "Could not register as a PolicyKit Authentication Agent");
+    }
+    return self->pk_auth_agent_reg_id;
+}
+
+static void
+name_owner_changed(NMPolkitListener *self, const char *name_owner)
+{
+    gs_free_error GError *error = NULL;
+
+    name_owner = nm_str_not_empty(name_owner);
+
+    if (nm_streq0(self->name_owner, name_owner)) {
+        return;
+    }
+
+    g_free(self->name_owner);
+    self->name_owner = g_strdup(name_owner);
+
+    if (!self->name_owner) {
+        return;
+    }
+
+    if (export_dbus_iface(self, &error)) {
+        if (self->session_agent) {
+            retrieve_session_id(self);
+        } else {
+            agent_register(self, NULL);
+        }
+    } else {
+        g_signal_emit(self,
+                      signals[ERROR],
+                      0,
+                      "Could not export the PolicyKit Authentication Agent DBus interface");
+    }
+}
+
+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)
+{
+    NMPolkitListener *self = user_data;
+    const char *      new_owner;
+
+    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);
+}
+
+static void
+get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+    if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        return;
+    }
+    name_owner_changed(user_data, name_owner);
+}
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMPolkitListener, PROP_DBUS_CONNECTION, PROP_SESSION_AGENT, );
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMPolkitListener *self = NM_POLKIT_LISTENER(object);
+
+    switch (prop_id) {
+    case PROP_DBUS_CONNECTION:
+        self->dbus_connection = g_value_dup_object(value);
+        break;
+    case PROP_SESSION_AGENT:
+        self->session_agent = g_value_get_boolean(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+nm_polkit_listener_init(NMPolkitListener *self)
+{
+    c_list_init(&self->request_lst_head);
+    self->main_context = g_main_context_ref_thread_default();
+}
+
+static void
+constructed(GObject *object)
+{
+    NMPolkitListener *self = NM_POLKIT_LISTENER(object);
+
+    self->cancellable = g_cancellable_new();
+
+    self->name_owner_changed_id =
+        nm_dbus_connection_signal_subscribe_name_owner_changed(self->dbus_connection,
+                                                               POLKIT_BUS_NAME,
+                                                               name_owner_changed_cb,
+                                                               self,
+                                                               NULL);
+
+    nm_dbus_connection_call_get_name_owner(self->dbus_connection,
+                                           POLKIT_BUS_NAME,
+                                           -1,
+                                           self->cancellable,
+                                           get_name_owner_cb,
+                                           self);
+
+    G_OBJECT_CLASS(nm_polkit_listener_parent_class)->constructed(object);
+}
+
+/**
+ * nm_polkit_listener_new:
+ * @dbus_connection: a open DBus connection
+ * @session_agent: TRUE if a session agent is wanted, FALSE for a process agent
+ *
+ * Creates a new #NMPolkitListener and registers it as a polkit agent.
+ *
+ * Returns: a new #NMPolkitListener
+ */
+NMPolkitListener *
+nm_polkit_listener_new(GDBusConnection *dbus_connection, gboolean session_agent)
+{
+    return g_object_new(NM_TYPE_POLKIT_LISTENER,
+                        NM_POLKIT_LISTENER_DBUS_CONNECTION,
+                        dbus_connection,
+                        NM_POLKIT_LISTENER_SESSION_AGENT,
+                        session_agent,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMPolkitListener *self = NM_POLKIT_LISTENER(object);
+    AuthRequest *     request;
+
+    nm_clear_g_cancellable(&self->cancellable);
+
+    while ((request = c_list_first_entry(&self->request_lst_head, AuthRequest, request_lst)))
+        auth_request_complete(request, FALSE);
+
+    if (self->dbus_connection) {
+        nm_clear_g_dbus_connection_signal(self->dbus_connection, &self->name_owner_changed_id);
+        g_dbus_connection_unregister_object(self->dbus_connection, self->pk_auth_agent_reg_id);
+        agent_unregister(self);
+        nm_clear_g_free(&self->name_owner);
+        g_clear_object(&self->dbus_connection);
+    }
+
+    nm_clear_pointer(&self->main_context, g_main_context_unref);
+
+    G_OBJECT_CLASS(nm_polkit_listener_parent_class)->dispose(object);
+}
+
+static void
+nm_polkit_listener_class_init(NMPolkitListenerClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->set_property = set_property;
+    object_class->constructed  = constructed;
+    object_class->dispose      = dispose;
+
+    obj_properties[PROP_DBUS_CONNECTION] =
+        g_param_spec_object(NM_POLKIT_LISTENER_DBUS_CONNECTION,
+                            "",
+                            "",
+                            G_TYPE_DBUS_CONNECTION,
+                            G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SESSION_AGENT] =
+        g_param_spec_boolean(NM_POLKIT_LISTENER_SESSION_AGENT,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[REQUEST_SYNC] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REQUEST_SYNC,
+                                         NM_TYPE_POLKIT_LISTENER,
+                                         G_SIGNAL_RUN_LAST,
+                                         0,
+                                         NULL,
+                                         NULL,
+                                         NULL,
+                                         G_TYPE_STRING,
+                                         3,
+                                         G_TYPE_STRING,
+                                         G_TYPE_STRING,
+                                         G_TYPE_STRING);
+
+    signals[REGISTERED] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_REGISTERED,
+                                       NM_TYPE_POLKIT_LISTENER,
+                                       G_SIGNAL_RUN_FIRST,
+                                       0,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       G_TYPE_NONE,
+                                       0);
+
+    signals[ERROR] = g_signal_new(NM_POLKIT_LISTENER_SIGNAL_ERROR,
+                                  NM_TYPE_POLKIT_LISTENER,
+                                  G_SIGNAL_RUN_FIRST,
+                                  0,
+                                  NULL,
+                                  NULL,
+                                  NULL,
+                                  G_TYPE_NONE,
+                                  1,
+                                  G_TYPE_STRING);
+}
diff --git a/src/libnmc-base/nm-polkit-listener.h b/src/libnmc-base/nm-polkit-listener.h
new file mode 100644
index 00000000..8a4c6c38
--- /dev/null
+++ b/src/libnmc-base/nm-polkit-listener.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_POLKIT_LISTENER_H__
+#define __NM_POLKIT_LISTENER_H__
+
+#define NM_POLKIT_LISTENER_SIGNAL_REGISTERED   "registered"
+#define NM_POLKIT_LISTENER_SIGNAL_REQUEST_SYNC "request-sync"
+#define NM_POLKIT_LISTENER_SIGNAL_AUTH_SUCCESS "auth-success"
+#define NM_POLKIT_LISTENER_SIGNAL_AUTH_FAILURE "auth-failure"
+#define NM_POLKIT_LISTENER_SIGNAL_ERROR        "error"
+
+typedef struct _NMPolkitListener      NMPolkitListener;
+typedef struct _NMPolkitListenerClass NMPolkitListenerClass;
+
+#define NM_TYPE_POLKIT_LISTENER (nm_polkit_listener_get_type())
+#define NM_POLKIT_LISTENER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_POLKIT_LISTENER, NMPolkitListener))
+#define NM_POLKIT_LISTENER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_POLKIT_LISTENER, NMPolkitListenerClass))
+#define NM_IS_POLKIT_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_POLKIT_LISTENER))
+#define NM_IS_POLKIT_LISTENER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_POLKIT_LISTENER))
+#define NM_POLKIT_LISTENER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_POLKIT_LISTENER, NMPolkitListenerClass))
+
+GType nm_polkit_listener_get_type(void);
+
+NMPolkitListener *nm_polkit_listener_new(GDBusConnection *dbus_connection, gboolean session_agent);
+
+#endif /* __NM_POLKIT_LISTENER_H__ */
diff --git a/src/libnmc-base/nm-secret-agent-simple.c b/src/libnmc-base/nm-secret-agent-simple.c
new file mode 100644
index 00000000..a3d5062d
--- /dev/null
+++ b/src/libnmc-base/nm-secret-agent-simple.c
@@ -0,0 +1,1405 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2015 Red Hat, Inc.
+ * Copyright (C) 2011 Giovanni Campagna <scampa.giovanni@gmail.com>
+ */
+
+/**
+ * SECTION:nm-secret-agent-simple
+ * @short_description: A simple secret agent for NetworkManager
+ *
+ * #NMSecretAgentSimple is the secret agent used by nmtui-connect and nmcli.
+ *
+ * This is a stripped-down version of gnome-shell's ShellNetworkAgent,
+ * with bits of the corresponding JavaScript code squished down into
+ * it. It is intended to eventually be generic enough that it could
+ * replace ShellNetworkAgent.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-secret-agent-simple.h"
+
+#include <gio/gunixoutputstream.h>
+#include <gio/gunixinputstream.h>
+
+#include "nm-vpn-service-plugin.h"
+#include "nm-vpn-helpers.h"
+#include "libnm-glib-aux/nm-secret-utils.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    char *request_id;
+
+    NMSecretAgentSimple *self;
+
+    NMConnection *                 connection;
+    const char *                   setting_name;
+    char **                        hints;
+    NMSecretAgentOldGetSecretsFunc callback;
+    gpointer                       callback_data;
+    GCancellable *                 cancellable;
+    NMSecretAgentGetSecretsFlags   flags;
+} RequestData;
+
+enum {
+    REQUEST_SECRETS,
+
+    LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    GHashTable *requests;
+
+    char *   path;
+    gboolean enabled;
+} NMSecretAgentSimplePrivate;
+
+struct _NMSecretAgentSimple {
+    NMSecretAgentOld           parent;
+    NMSecretAgentSimplePrivate _priv;
+};
+
+struct _NMSecretAgentSimpleClass {
+    NMSecretAgentOldClass parent;
+};
+
+G_DEFINE_TYPE(NMSecretAgentSimple, nm_secret_agent_simple, NM_TYPE_SECRET_AGENT_OLD)
+
+#define NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMSecretAgentSimple, NM_IS_SECRET_AGENT_SIMPLE, NMSecretAgentOld)
+
+/*****************************************************************************/
+
+static void
+_request_data_free(gpointer data)
+{
+    RequestData *request = data;
+
+    g_free(request->request_id);
+    nm_clear_g_cancellable(&request->cancellable);
+    g_object_unref(request->connection);
+    g_strfreev(request->hints);
+
+    g_slice_free(RequestData, request);
+}
+
+static void
+_request_data_complete(RequestData *   request,
+                       GVariant *      secrets,
+                       GError *        error,
+                       GHashTableIter *iter_to_remove)
+{
+    NMSecretAgentSimple *       self = request->self;
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self);
+
+    nm_assert((secrets != NULL) != (error != NULL));
+
+    request->callback(NM_SECRET_AGENT_OLD(request->self),
+                      request->connection,
+                      secrets,
+                      error,
+                      request->callback_data);
+
+    if (iter_to_remove)
+        g_hash_table_iter_remove(iter_to_remove);
+    else
+        g_hash_table_remove(priv->requests, request);
+}
+
+/*****************************************************************************/
+
+/**
+ * NMSecretAgentSimpleSecret:
+ * @name: the user-visible name of the secret. Eg, "WEP Passphrase".
+ * @value: the value of the secret
+ * @password: %TRUE if this secret represents a password, %FALSE
+ *   if it represents non-secret data.
+ *
+ * A single "secret" being requested.
+ */
+
+typedef struct {
+    NMSecretAgentSimpleSecret base;
+    NMSetting *               setting;
+    char *                    property;
+} SecretReal;
+
+static void
+_secret_real_free(NMSecretAgentSimpleSecret *secret)
+{
+    SecretReal *real = (SecretReal *) secret;
+
+    g_free((char *) secret->pretty_name);
+    g_free((char *) secret->entry_id);
+    nm_free_secret(secret->value);
+    g_free((char *) secret->vpn_type);
+    g_free(real->property);
+    g_clear_object(&real->setting);
+
+    g_slice_free(SecretReal, real);
+}
+
+static NMSecretAgentSimpleSecret *
+_secret_real_new_plain(NMSecretAgentSecretType secret_type,
+                       const char *            pretty_name,
+                       NMSetting *             setting,
+                       const char *            property)
+{
+    SecretReal *  real;
+    gs_free char *value = NULL;
+
+    nm_assert(property);
+    nm_assert(NM_IS_SETTING(setting));
+    nm_assert(NM_IN_SET(secret_type,
+                        NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+                        NM_SECRET_AGENT_SECRET_TYPE_SECRET));
+    nm_assert(g_object_class_find_property(G_OBJECT_GET_CLASS(setting), property));
+    nm_assert((secret_type == NM_SECRET_AGENT_SECRET_TYPE_SECRET)
+              == nm_setting_get_secret_flags(setting, property, NULL, NULL));
+
+    g_object_get(setting, property, &value, NULL);
+
+    real  = g_slice_new(SecretReal);
+    *real = (SecretReal){
+        .base.secret_type = secret_type,
+        .base.pretty_name = g_strdup(pretty_name),
+        .base.entry_id    = g_strdup_printf("%s.%s", nm_setting_get_name(setting), property),
+        .base.value       = g_steal_pointer(&value),
+        .base.is_secret   = (secret_type != NM_SECRET_AGENT_SECRET_TYPE_PROPERTY),
+        .setting          = g_object_ref(setting),
+        .property         = g_strdup(property),
+    };
+    return &real->base;
+}
+
+static NMSecretAgentSimpleSecret *
+_secret_real_new_vpn_secret(const char *pretty_name,
+                            NMSetting * setting,
+                            const char *property,
+                            const char *vpn_type)
+{
+    SecretReal *real;
+    const char *value;
+
+    nm_assert(property);
+    nm_assert(NM_IS_SETTING_VPN(setting));
+    nm_assert(vpn_type);
+
+    value = nm_setting_vpn_get_secret(NM_SETTING_VPN(setting), property);
+
+    real  = g_slice_new(SecretReal);
+    *real = (SecretReal){
+        .base.secret_type = NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET,
+        .base.pretty_name = g_strdup(pretty_name),
+        .base.entry_id =
+            g_strdup_printf("%s%s", NM_SECRET_AGENT_ENTRY_ID_PREFX_VPN_SECRETS, property),
+        .base.value     = g_strdup(value),
+        .base.is_secret = TRUE,
+        .base.vpn_type  = g_strdup(vpn_type),
+        .setting        = g_object_ref(setting),
+        .property       = g_strdup(property),
+    };
+    return &real->base;
+}
+
+static NMSecretAgentSimpleSecret *
+_secret_real_new_wireguard_peer_psk(NMSettingWireGuard *s_wg,
+                                    const char *        public_key,
+                                    const char *        preshared_key)
+{
+    SecretReal *real;
+
+    nm_assert(NM_IS_SETTING_WIREGUARD(s_wg));
+    nm_assert(public_key);
+
+    real  = g_slice_new(SecretReal);
+    *real = (SecretReal){
+        .base.secret_type        = NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK,
+        .base.pretty_name        = g_strdup_printf(_("Preshared-key for %s"), public_key),
+        .base.entry_id           = g_strdup_printf(NM_SETTING_WIREGUARD_SETTING_NAME
+                                         "." NM_SETTING_WIREGUARD_PEERS
+                                         ".%s." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY,
+                                         public_key),
+        .base.value              = g_strdup(preshared_key),
+        .base.is_secret          = TRUE,
+        .base.no_prompt_entry_id = TRUE,
+        .setting                 = NM_SETTING(g_object_ref(s_wg)),
+        .property                = g_strdup(public_key),
+    };
+    return &real->base;
+}
+
+/*****************************************************************************/
+
+static gboolean
+add_8021x_secrets(RequestData *request, GPtrArray *secrets)
+{
+    NMSetting8021x *           s_8021x = nm_connection_get_setting_802_1x(request->connection);
+    const char *               eap_method;
+    NMSecretAgentSimpleSecret *secret;
+
+    /* If hints are given, then always ask for what the hints require */
+    if (request->hints && request->hints[0]) {
+        char **iter;
+
+        for (iter = request->hints; *iter; iter++) {
+            secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                            _(*iter),
+                                            NM_SETTING(s_8021x),
+                                            *iter);
+            g_ptr_array_add(secrets, secret);
+        }
+
+        return TRUE;
+    }
+
+    eap_method = nm_setting_802_1x_get_eap_method(s_8021x, 0);
+    if (!eap_method)
+        return FALSE;
+
+    if (NM_IN_STRSET(eap_method, "md5", "leap", "ttls", "peap")) {
+        /* TTLS and PEAP are actually much more complicated, but this complication
+         * is not visible here since we only care about phase2 authentication
+         * (and don't even care of which one)
+         */
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+                                        _("Username"),
+                                        NM_SETTING(s_8021x),
+                                        NM_SETTING_802_1X_IDENTITY);
+        g_ptr_array_add(secrets, secret);
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Password"),
+                                        NM_SETTING(s_8021x),
+                                        NM_SETTING_802_1X_PASSWORD);
+        g_ptr_array_add(secrets, secret);
+        return TRUE;
+    }
+
+    if (nm_streq(eap_method, "tls")) {
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+                                        _("Identity"),
+                                        NM_SETTING(s_8021x),
+                                        NM_SETTING_802_1X_IDENTITY);
+        g_ptr_array_add(secrets, secret);
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Private key password"),
+                                        NM_SETTING(s_8021x),
+                                        NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD);
+        g_ptr_array_add(secrets, secret);
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static gboolean
+add_wireless_secrets(RequestData *request, GPtrArray *secrets)
+{
+    NMSettingWirelessSecurity *s_wsec =
+        nm_connection_get_setting_wireless_security(request->connection);
+    const char *               key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    NMSecretAgentSimpleSecret *secret;
+
+    if (!key_mgmt || nm_streq(key_mgmt, "owe"))
+        return FALSE;
+
+    if (NM_IN_STRSET(key_mgmt, "wpa-psk", "sae")) {
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Password"),
+                                        NM_SETTING(s_wsec),
+                                        NM_SETTING_WIRELESS_SECURITY_PSK);
+        g_ptr_array_add(secrets, secret);
+        return TRUE;
+    }
+
+    if (nm_streq(key_mgmt, "none")) {
+        guint32 index;
+        char    key[100];
+
+        index  = nm_setting_wireless_security_get_wep_tx_keyidx(s_wsec);
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Key"),
+                                        NM_SETTING(s_wsec),
+                                        nm_sprintf_buf(key, "wep-key%u", (guint) index));
+        g_ptr_array_add(secrets, secret);
+        return TRUE;
+    }
+
+    if (nm_streq(key_mgmt, "iee8021x")) {
+        if (nm_streq0(nm_setting_wireless_security_get_auth_alg(s_wsec), "leap")) {
+            secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                            _("Password"),
+                                            NM_SETTING(s_wsec),
+                                            NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD);
+            g_ptr_array_add(secrets, secret);
+            return TRUE;
+        } else
+            return add_8021x_secrets(request, secrets);
+    }
+
+    if (nm_streq(key_mgmt, "wpa-eap") || nm_streq(key_mgmt, "wpa-eap-suite-b-192"))
+        return add_8021x_secrets(request, secrets);
+
+    return FALSE;
+}
+
+static gboolean
+add_pppoe_secrets(RequestData *request, GPtrArray *secrets)
+{
+    NMSettingPppoe *           s_pppoe = nm_connection_get_setting_pppoe(request->connection);
+    NMSecretAgentSimpleSecret *secret;
+
+    secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+                                    _("Username"),
+                                    NM_SETTING(s_pppoe),
+                                    NM_SETTING_PPPOE_USERNAME);
+    g_ptr_array_add(secrets, secret);
+    secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+                                    _("Service"),
+                                    NM_SETTING(s_pppoe),
+                                    NM_SETTING_PPPOE_SERVICE);
+    g_ptr_array_add(secrets, secret);
+    secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                    _("Password"),
+                                    NM_SETTING(s_pppoe),
+                                    NM_SETTING_PPPOE_PASSWORD);
+    g_ptr_array_add(secrets, secret);
+    return TRUE;
+}
+
+static NMSettingSecretFlags
+get_vpn_secret_flags(NMSettingVpn *s_vpn, const char *secret_name)
+{
+    NMSettingSecretFlags flags = NM_SETTING_SECRET_FLAG_NONE;
+    GHashTable *         vpn_data;
+
+    g_object_get(s_vpn, NM_SETTING_VPN_DATA, &vpn_data, NULL);
+    nm_vpn_service_plugin_get_secret_flags(vpn_data, secret_name, &flags);
+    g_hash_table_unref(vpn_data);
+
+    return flags;
+}
+
+static void
+add_vpn_secret_helper(GPtrArray *   secrets,
+                      NMSettingVpn *s_vpn,
+                      const char *  name,
+                      const char *  ui_name)
+{
+    NMSecretAgentSimpleSecret *secret;
+    NMSettingSecretFlags       flags;
+    int                        i;
+
+    flags = get_vpn_secret_flags(s_vpn, name);
+    if (flags & NM_SETTING_SECRET_FLAG_AGENT_OWNED || flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) {
+        secret = _secret_real_new_vpn_secret(ui_name,
+                                             NM_SETTING(s_vpn),
+                                             name,
+                                             nm_setting_vpn_get_service_type(s_vpn));
+
+        /* Check for duplicates */
+        for (i = 0; i < secrets->len; i++) {
+            NMSecretAgentSimpleSecret *s = secrets->pdata[i];
+
+            if (s->secret_type == secret->secret_type && nm_streq0(s->vpn_type, secret->vpn_type)
+                && nm_streq0(s->entry_id, secret->entry_id)) {
+                _secret_real_free(secret);
+                return;
+            }
+        }
+
+        g_ptr_array_add(secrets, secret);
+    }
+}
+
+#define VPN_MSG_TAG "x-vpn-message:"
+
+static gboolean
+add_vpn_secrets(RequestData *request, GPtrArray *secrets, char **msg)
+{
+    NMSettingVpn *            s_vpn = nm_connection_get_setting_vpn(request->connection);
+    const NmcVpnPasswordName *p;
+    const char *              vpn_msg = NULL;
+    char **                   iter;
+
+    /* If hints are given, then always ask for what the hints require */
+    if (request->hints) {
+        for (iter = request->hints; *iter; iter++) {
+            if (!vpn_msg && g_str_has_prefix(*iter, VPN_MSG_TAG))
+                vpn_msg = &(*iter)[NM_STRLEN(VPN_MSG_TAG)];
+            else
+                add_vpn_secret_helper(secrets, s_vpn, *iter, *iter);
+        }
+    }
+
+    NM_SET_OUT(msg, g_strdup(vpn_msg));
+
+    /* Now add what client thinks might be required, because hints may be empty or incomplete */
+    p = nm_vpn_get_secret_names(nm_setting_vpn_get_service_type(s_vpn));
+    while (p && p->name) {
+        add_vpn_secret_helper(secrets, s_vpn, p->name, _(p->ui_name));
+        p++;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+add_wireguard_secrets(RequestData *request, GPtrArray *secrets, char **msg, GError **error)
+{
+    NMSettingWireGuard *       s_wg;
+    NMSecretAgentSimpleSecret *secret;
+    guint                      i;
+
+    s_wg = NM_SETTING_WIREGUARD(
+        nm_connection_get_setting(request->connection, NM_TYPE_SETTING_WIREGUARD));
+    if (!s_wg) {
+        g_set_error(error,
+                    NM_SECRET_AGENT_ERROR,
+                    NM_SECRET_AGENT_ERROR_FAILED,
+                    "Cannot service a WireGuard secrets request %s for a connection without "
+                    "WireGuard settings",
+                    request->request_id);
+        return FALSE;
+    }
+
+    if (!request->hints || !request->hints[0]
+        || g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_WIREGUARD_PRIVATE_KEY)) {
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("WireGuard private-key"),
+                                        NM_SETTING(s_wg),
+                                        NM_SETTING_WIREGUARD_PRIVATE_KEY);
+        g_ptr_array_add(secrets, secret);
+    }
+
+    if (request->hints) {
+        for (i = 0; request->hints[i]; i++) {
+            NMWireGuardPeer *peer;
+            const char *     name       = request->hints[i];
+            gs_free char *   public_key = NULL;
+
+            if (nm_streq(name, NM_SETTING_WIREGUARD_PRIVATE_KEY))
+                continue;
+
+            if (NM_STR_HAS_PREFIX(name, NM_SETTING_WIREGUARD_PEERS ".")) {
+                const char *tmp;
+
+                tmp = &name[NM_STRLEN(NM_SETTING_WIREGUARD_PEERS ".")];
+                if (NM_STR_HAS_SUFFIX(tmp, "." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)) {
+                    public_key = g_strndup(
+                        tmp,
+                        strlen(tmp) - NM_STRLEN("." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY));
+                }
+            }
+
+            if (!public_key)
+                continue;
+
+            peer = nm_setting_wireguard_get_peer_by_public_key(s_wg, public_key, NULL);
+
+            g_ptr_array_add(secrets,
+                            _secret_real_new_wireguard_peer_psk(
+                                s_wg,
+                                (peer ? nm_wireguard_peer_get_public_key(peer) : public_key),
+                                (peer ? nm_wireguard_peer_get_preshared_key(peer) : NULL)));
+        }
+    }
+
+    *msg = g_strdup_printf(_("Secrets are required to connect WireGuard VPN '%s'"),
+                           nm_connection_get_id(request->connection));
+    return TRUE;
+}
+
+typedef struct {
+    GPid           auth_dialog_pid;
+    GString *      auth_dialog_response;
+    RequestData *  request;
+    GPtrArray *    secrets;
+    GCancellable * cancellable;
+    gulong         cancellable_id;
+    guint          child_watch_id;
+    GInputStream * input_stream;
+    GOutputStream *output_stream;
+    char           read_buf[5];
+} AuthDialogData;
+
+static void
+_auth_dialog_data_free(AuthDialogData *data)
+{
+    nm_clear_g_signal_handler(data->cancellable, &data->cancellable_id);
+    g_clear_object(&data->cancellable);
+    nm_clear_g_source(&data->child_watch_id);
+    g_ptr_array_unref(data->secrets);
+    g_spawn_close_pid(data->auth_dialog_pid);
+    g_string_free(data->auth_dialog_response, TRUE);
+    g_object_unref(data->input_stream);
+    g_object_unref(data->output_stream);
+    g_slice_free(AuthDialogData, data);
+}
+
+static void
+_auth_dialog_exited(GPid pid, int status, gpointer user_data)
+{
+    AuthDialogData *      data              = user_data;
+    RequestData *         request           = data->request;
+    GPtrArray *           secrets           = data->secrets;
+    NMSettingVpn *        s_vpn             = nm_connection_get_setting_vpn(request->connection);
+    nm_auto_unref_keyfile GKeyFile *keyfile = NULL;
+    gs_strfreev char **             groups  = NULL;
+    gs_free char *                  title   = NULL;
+    gs_free char *                  message = NULL;
+    int                             i;
+    gs_free_error GError *error = NULL;
+
+    data->child_watch_id = 0;
+
+    nm_clear_g_cancellable_disconnect(data->cancellable, &data->cancellable_id);
+
+    if (status != 0) {
+        g_set_error(&error,
+                    NM_SECRET_AGENT_ERROR,
+                    NM_SECRET_AGENT_ERROR_FAILED,
+                    "Auth dialog failed with error code %d\n",
+                    status);
+        goto out;
+    }
+
+    keyfile = g_key_file_new();
+    if (!g_key_file_load_from_data(keyfile,
+                                   data->auth_dialog_response->str,
+                                   data->auth_dialog_response->len,
+                                   G_KEY_FILE_NONE,
+                                   &error)) {
+        goto out;
+    }
+
+    groups = g_key_file_get_groups(keyfile, NULL);
+    if (!nm_streq0(groups[0], "VPN Plugin UI")) {
+        g_set_error(&error,
+                    NM_SECRET_AGENT_ERROR,
+                    NM_SECRET_AGENT_ERROR_FAILED,
+                    "Expected [VPN Plugin UI] in auth dialog response");
+        goto out;
+    }
+
+    title = g_key_file_get_string(keyfile, "VPN Plugin UI", "Title", &error);
+    if (!title)
+        goto out;
+
+    message = g_key_file_get_string(keyfile, "VPN Plugin UI", "Description", &error);
+    if (!message)
+        goto out;
+
+    for (i = 1; groups[i]; i++) {
+        gs_free char *pretty_name = NULL;
+
+        if (!g_key_file_get_boolean(keyfile, groups[i], "IsSecret", NULL))
+            continue;
+        if (!g_key_file_get_boolean(keyfile, groups[i], "ShouldAsk", NULL))
+            continue;
+
+        pretty_name = g_key_file_get_string(keyfile, groups[i], "Label", NULL);
+        g_ptr_array_add(secrets,
+                        _secret_real_new_vpn_secret(pretty_name,
+                                                    NM_SETTING(s_vpn),
+                                                    groups[i],
+                                                    nm_setting_vpn_get_service_type(s_vpn)));
+    }
+
+out:
+    /* Try to fall back to the hardwired VPN support if the auth dialog fails.
+     * We may eventually get rid of the whole hardwired secrets handling at some point,
+     * when the auth helpers are goode enough.. */
+    if (error && add_vpn_secrets(request, secrets, &message)) {
+        g_clear_error(&error);
+        if (!message) {
+            message = g_strdup_printf(_("A password is required to connect to '%s'."),
+                                      nm_connection_get_id(request->connection));
+        }
+    }
+
+    if (error)
+        _request_data_complete(request, NULL, error, NULL);
+    else {
+        g_signal_emit(request->self,
+                      signals[REQUEST_SECRETS],
+                      0,
+                      request->request_id,
+                      title,
+                      message,
+                      secrets);
+    }
+
+    _auth_dialog_data_free(data);
+}
+
+static void
+_request_cancelled(GObject *object, gpointer user_data)
+{
+    _auth_dialog_data_free(user_data);
+}
+
+static void
+_auth_dialog_read_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GInputStream *  auth_dialog_out = G_INPUT_STREAM(source_object);
+    AuthDialogData *data            = user_data;
+    gssize          read_size;
+    gs_free_error GError *error = NULL;
+
+    read_size = g_input_stream_read_finish(auth_dialog_out, res, &error);
+    switch (read_size) {
+    case -1:
+        if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+            _request_data_complete(data->request, NULL, error, NULL);
+        _auth_dialog_data_free(data);
+        break;
+    case 0:
+        /* Done reading. Let's wait for the auth dialog to exit so that we're able to collect the status.
+         * Remember we can be cancelled in between. */
+        data->child_watch_id = g_child_watch_add(data->auth_dialog_pid, _auth_dialog_exited, data);
+        data->cancellable    = g_object_ref(data->request->cancellable);
+        data->cancellable_id =
+            g_cancellable_connect(data->cancellable, G_CALLBACK(_request_cancelled), data, NULL);
+        break;
+    default:
+        g_string_append_len(data->auth_dialog_response, data->read_buf, read_size);
+        g_input_stream_read_async(auth_dialog_out,
+                                  data->read_buf,
+                                  sizeof(data->read_buf),
+                                  G_PRIORITY_DEFAULT,
+                                  NULL,
+                                  _auth_dialog_read_done,
+                                  data);
+        return;
+    }
+
+    g_input_stream_close(auth_dialog_out, NULL, NULL);
+}
+
+static void
+_auth_dialog_write_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GOutputStream *auth_dialog_out                    = G_OUTPUT_STREAM(source_object);
+    _nm_unused gs_free char *auth_dialog_request_free = user_data;
+
+    /* We don't care about write errors. If there are any problems, the
+     * reader shall notice. */
+    g_output_stream_write_finish(auth_dialog_out, res, NULL);
+    g_output_stream_close(auth_dialog_out, NULL, NULL);
+}
+
+static void
+_add_to_string(GString *string, const char *key, const char *value)
+{
+    gs_strfreev char **lines = NULL;
+    int                i;
+
+    lines = g_strsplit(value, "\n", -1);
+
+    g_string_append(string, key);
+    for (i = 0; lines[i]; i++) {
+        g_string_append_c(string, '=');
+        g_string_append(string, lines[i]);
+        g_string_append_c(string, '\n');
+    }
+}
+
+static void
+_add_data_item_to_string(const char *key, const char *value, gpointer user_data)
+{
+    GString *string = user_data;
+
+    _add_to_string(string, "DATA_KEY", key);
+    _add_to_string(string, "DATA_VAL", value);
+    g_string_append_c(string, '\n');
+}
+
+static void
+_add_secret_to_string(const char *key, const char *value, gpointer user_data)
+{
+    GString *string = user_data;
+
+    _add_to_string(string, "SECRET_KEY", key);
+    _add_to_string(string, "SECRET_VAL", value);
+    g_string_append_c(string, '\n');
+}
+
+static gboolean
+try_spawn_vpn_auth_helper(RequestData *request, GPtrArray *secrets)
+{
+    NMSettingVpn *    s_vpn = nm_connection_get_setting_vpn(request->connection);
+    gs_unref_ptrarray GPtrArray *auth_dialog_argv = NULL;
+    NMVpnPluginInfo *            plugin_info;
+    const char *                 s;
+    GPid                         auth_dialog_pid;
+    int                          auth_dialog_in_fd;
+    int                          auth_dialog_out_fd;
+    GOutputStream *              auth_dialog_in;
+    GInputStream *               auth_dialog_out;
+    GError *                     error = NULL;
+    GString *                    auth_dialog_request;
+    gs_free char *               auth_dialog_request_str = NULL;
+    gsize                        auth_dialog_request_len;
+    AuthDialogData *             data;
+    int                          i;
+
+    plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(),
+                                                          nm_setting_vpn_get_service_type(s_vpn));
+    if (!plugin_info)
+        return FALSE;
+
+    s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-external-ui-mode");
+    if (!_nm_utils_ascii_str_to_bool(s, FALSE))
+        return FALSE;
+
+    auth_dialog_argv = g_ptr_array_new();
+
+    s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "auth-dialog");
+    g_return_val_if_fail(s, FALSE);
+    g_ptr_array_add(auth_dialog_argv, (gpointer) s);
+
+    g_ptr_array_add(auth_dialog_argv, "-u");
+    g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_uuid(request->connection));
+    g_ptr_array_add(auth_dialog_argv, "-n");
+    g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_id(request->connection));
+    g_ptr_array_add(auth_dialog_argv, "-s");
+    g_ptr_array_add(auth_dialog_argv, (gpointer) nm_setting_vpn_get_service_type(s_vpn));
+    g_ptr_array_add(auth_dialog_argv, "--external-ui-mode");
+    g_ptr_array_add(auth_dialog_argv, "-i");
+
+    if (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)
+        g_ptr_array_add(auth_dialog_argv, "-r");
+
+    s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-hints");
+    if (_nm_utils_ascii_str_to_bool(s, FALSE)) {
+        for (i = 0; request->hints[i]; i++) {
+            g_ptr_array_add(auth_dialog_argv, "-t");
+            g_ptr_array_add(auth_dialog_argv, request->hints[i]);
+        }
+    }
+
+    g_ptr_array_add(auth_dialog_argv, NULL);
+    if (!g_spawn_async_with_pipes(NULL,
+                                  (char **) auth_dialog_argv->pdata,
+                                  NULL,
+                                  G_SPAWN_DO_NOT_REAP_CHILD,
+                                  NULL,
+                                  NULL,
+                                  &auth_dialog_pid,
+                                  &auth_dialog_in_fd,
+                                  &auth_dialog_out_fd,
+                                  NULL,
+                                  &error)) {
+        g_warning("Failed to spawn the auth dialog%s\n", error->message);
+        return FALSE;
+    }
+
+    auth_dialog_in  = g_unix_output_stream_new(auth_dialog_in_fd, TRUE);
+    auth_dialog_out = g_unix_input_stream_new(auth_dialog_out_fd, TRUE);
+
+    auth_dialog_request = g_string_new_len(NULL, 1024);
+    nm_setting_vpn_foreach_data_item(s_vpn, _add_data_item_to_string, auth_dialog_request);
+    nm_setting_vpn_foreach_secret(s_vpn, _add_secret_to_string, auth_dialog_request);
+    g_string_append(auth_dialog_request, "DONE\nQUIT\n");
+    auth_dialog_request_len = auth_dialog_request->len;
+    auth_dialog_request_str = g_string_free(auth_dialog_request, FALSE);
+
+    data  = g_slice_new(AuthDialogData);
+    *data = (AuthDialogData){
+        .auth_dialog_response = g_string_new_len(NULL, sizeof(data->read_buf)),
+        .auth_dialog_pid      = auth_dialog_pid,
+        .request              = request,
+        .secrets              = g_ptr_array_ref(secrets),
+        .input_stream         = auth_dialog_out,
+        .output_stream        = auth_dialog_in,
+    };
+
+    g_output_stream_write_async(auth_dialog_in,
+                                auth_dialog_request_str,
+                                auth_dialog_request_len,
+                                G_PRIORITY_DEFAULT,
+                                request->cancellable,
+                                _auth_dialog_write_done,
+                                auth_dialog_request_str);
+
+    /* Ownership of the pointer was passed on to g_output_stream_write_async(). */
+    g_steal_pointer(&auth_dialog_request_str);
+
+    g_input_stream_read_async(auth_dialog_out,
+                              data->read_buf,
+                              sizeof(data->read_buf),
+                              G_PRIORITY_DEFAULT,
+                              request->cancellable,
+                              _auth_dialog_read_done,
+                              data);
+
+    return TRUE;
+}
+
+static void
+request_secrets_from_ui(RequestData *request)
+{
+    gs_unref_ptrarray GPtrArray *secrets = NULL;
+    gs_free_error GError *      error    = NULL;
+    NMSecretAgentSimplePrivate *priv;
+    NMSecretAgentSimpleSecret * secret;
+    const char *                title;
+    gs_free char *              msg = NULL;
+
+    priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(request->self);
+    g_return_if_fail(priv->enabled);
+
+    /* We only handle requests for connection with @path if set. */
+    if (priv->path && !g_str_has_prefix(request->request_id, priv->path)) {
+        g_set_error(&error,
+                    NM_SECRET_AGENT_ERROR,
+                    NM_SECRET_AGENT_ERROR_FAILED,
+                    "Request for %s secrets doesn't match path %s",
+                    request->request_id,
+                    priv->path);
+        goto out_fail_error;
+    }
+
+    secrets = g_ptr_array_new_with_free_func((GDestroyNotify) _secret_real_free);
+
+    if (nm_connection_is_type(request->connection, NM_SETTING_WIRELESS_SETTING_NAME)) {
+        NMSettingWireless *s_wireless;
+        GBytes *           ssid;
+        gs_free char *     ssid_utf8 = NULL;
+
+        s_wireless = nm_connection_get_setting_wireless(request->connection);
+        ssid       = nm_setting_wireless_get_ssid(s_wireless);
+        ssid_utf8  = nm_utils_ssid_to_utf8(g_bytes_get_data(ssid, NULL), g_bytes_get_size(ssid));
+
+        title = _("Authentication required by wireless network");
+        msg   = g_strdup_printf(
+            _("Passwords or encryption keys are required to access the wireless network '%s'."),
+            ssid_utf8);
+
+        if (!add_wireless_secrets(request, secrets))
+            goto out_fail;
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_WIRED_SETTING_NAME)) {
+        title = _("Wired 802.1X authentication");
+        msg   = g_strdup_printf(_("Secrets are required to access the wired network '%s'"),
+                              nm_connection_get_id(request->connection));
+
+        if (!add_8021x_secrets(request, secrets))
+            goto out_fail;
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_PPPOE_SETTING_NAME)) {
+        title = _("DSL authentication");
+        msg   = g_strdup_printf(_("Secrets are required for the DSL connection '%s'"),
+                              nm_connection_get_id(request->connection));
+
+        if (!add_pppoe_secrets(request, secrets))
+            goto out_fail;
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_GSM_SETTING_NAME)) {
+        NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(request->connection);
+
+        if (g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_GSM_PIN)) {
+            title = _("PIN code required");
+            msg   = g_strdup(_("PIN code is needed for the mobile broadband device"));
+
+            secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                            _("PIN"),
+                                            NM_SETTING(s_gsm),
+                                            NM_SETTING_GSM_PIN);
+            g_ptr_array_add(secrets, secret);
+        } else {
+            title = _("Mobile broadband network password");
+            msg   = g_strdup_printf(_("A password is required to connect to '%s'."),
+                                  nm_connection_get_id(request->connection));
+
+            secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                            _("Password"),
+                                            NM_SETTING(s_gsm),
+                                            NM_SETTING_GSM_PASSWORD);
+            g_ptr_array_add(secrets, secret);
+        }
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_MACSEC_SETTING_NAME)) {
+        NMSettingMacsec *s_macsec = nm_connection_get_setting_macsec(request->connection);
+
+        msg = g_strdup_printf(_("Secrets are required to access the MACsec network '%s'"),
+                              nm_connection_get_id(request->connection));
+
+        if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_PSK) {
+            title  = _("MACsec PSK authentication");
+            secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                            _("MKA CAK"),
+                                            NM_SETTING(s_macsec),
+                                            NM_SETTING_MACSEC_MKA_CAK);
+            g_ptr_array_add(secrets, secret);
+        } else {
+            title = _("MACsec EAP authentication");
+            if (!add_8021x_secrets(request, secrets))
+                goto out_fail;
+        }
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_WIREGUARD_SETTING_NAME)) {
+        title = _("WireGuard VPN secret");
+        if (!add_wireguard_secrets(request, secrets, &msg, &error))
+            goto out_fail_error;
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_CDMA_SETTING_NAME)) {
+        NMSettingCdma *s_cdma = nm_connection_get_setting_cdma(request->connection);
+
+        title = _("Mobile broadband network password");
+        msg   = g_strdup_printf(_("A password is required to connect to '%s'."),
+                              nm_connection_get_id(request->connection));
+
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Password"),
+                                        NM_SETTING(s_cdma),
+                                        NM_SETTING_CDMA_PASSWORD);
+        g_ptr_array_add(secrets, secret);
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) {
+        NMSetting *setting = NULL;
+
+        setting = nm_connection_get_setting_by_name(request->connection,
+                                                    NM_SETTING_BLUETOOTH_SETTING_NAME);
+        if (setting
+            && !nm_streq0(nm_setting_bluetooth_get_connection_type(NM_SETTING_BLUETOOTH(setting)),
+                          NM_SETTING_BLUETOOTH_TYPE_NAP)) {
+            setting =
+                nm_connection_get_setting_by_name(request->connection, NM_SETTING_GSM_SETTING_NAME);
+            if (!setting)
+                setting = nm_connection_get_setting_by_name(request->connection,
+                                                            NM_SETTING_CDMA_SETTING_NAME);
+        }
+
+        if (!setting)
+            goto out_fail;
+
+        title = _("Mobile broadband network password");
+        msg   = g_strdup_printf(_("A password is required to connect to '%s'."),
+                              nm_connection_get_id(request->connection));
+
+        secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+                                        _("Password"),
+                                        setting,
+                                        "password");
+        g_ptr_array_add(secrets, secret);
+    } else if (nm_connection_is_type(request->connection, NM_SETTING_VPN_SETTING_NAME)) {
+        title = _("VPN password required");
+
+        if (try_spawn_vpn_auth_helper(request, secrets)) {
+            /* This will emit REQUEST_SECRETS when ready */
+            return;
+        }
+
+        if (!add_vpn_secrets(request, secrets, &msg))
+            goto out_fail;
+        if (!msg) {
+            msg = g_strdup_printf(_("A password is required to connect to '%s'."),
+                                  nm_connection_get_id(request->connection));
+        }
+    } else
+        goto out_fail;
+
+    if (secrets->len == 0)
+        goto out_fail;
+
+    g_signal_emit(request->self,
+                  signals[REQUEST_SECRETS],
+                  0,
+                  request->request_id,
+                  title,
+                  msg,
+                  secrets);
+    return;
+
+out_fail:
+    g_set_error(&error,
+                NM_SECRET_AGENT_ERROR,
+                NM_SECRET_AGENT_ERROR_FAILED,
+                "Cannot service a secrets request %s for a %s connection",
+                request->request_id,
+                nm_connection_get_connection_type(request->connection));
+out_fail_error:
+    _request_data_complete(request, NULL, error, NULL);
+}
+
+static void
+get_secrets(NMSecretAgentOld *             agent,
+            NMConnection *                 connection,
+            const char *                   connection_path,
+            const char *                   setting_name,
+            const char **                  hints,
+            NMSecretAgentGetSecretsFlags   flags,
+            NMSecretAgentOldGetSecretsFunc callback,
+            gpointer                       callback_data)
+{
+    NMSecretAgentSimple *       self = NM_SECRET_AGENT_SIMPLE(agent);
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self);
+    RequestData *               request;
+    gs_free_error GError *error      = NULL;
+    gs_free char *        request_id = NULL;
+    const char *          request_id_setting_name;
+
+    request_id = g_strdup_printf("%s/%s", connection_path, setting_name);
+
+    if (g_hash_table_contains(priv->requests, &request_id)) {
+        /* We already have a request pending for this (connection, setting) */
+        error = g_error_new(NM_SECRET_AGENT_ERROR,
+                            NM_SECRET_AGENT_ERROR_FAILED,
+                            "Request for %s secrets already pending",
+                            request_id);
+        callback(agent, connection, NULL, error, callback_data);
+        return;
+    }
+
+    if (!(flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION)) {
+        /* We don't do stored passwords */
+        error = g_error_new(NM_SECRET_AGENT_ERROR,
+                            NM_SECRET_AGENT_ERROR_NO_SECRETS,
+                            "Stored passwords not supported");
+        callback(agent, connection, NULL, error, callback_data);
+        return;
+    }
+
+    nm_assert(g_str_has_suffix(request_id, setting_name));
+    request_id_setting_name = &request_id[strlen(request_id) - strlen(setting_name)];
+    nm_assert(nm_streq(request_id_setting_name, setting_name));
+
+    request  = g_slice_new(RequestData);
+    *request = (RequestData){
+        .self          = self,
+        .connection    = g_object_ref(connection),
+        .setting_name  = request_id_setting_name,
+        .hints         = g_strdupv((char **) hints),
+        .callback      = callback,
+        .callback_data = callback_data,
+        .request_id    = g_steal_pointer(&request_id),
+        .flags         = flags,
+        .cancellable   = g_cancellable_new(),
+    };
+    g_hash_table_add(priv->requests, request);
+
+    if (priv->enabled)
+        request_secrets_from_ui(request);
+}
+
+/**
+ * nm_secret_agent_simple_response:
+ * @self: the #NMSecretAgentSimple
+ * @request_id: the request ID being responded to
+ * @secrets: (allow-none): the array of secrets, or %NULL
+ *
+ * Response to a #NMSecretAgentSimple::get-secrets signal.
+ *
+ * If the user provided secrets, the caller should set the
+ * corresponding <literal>value</literal> fields in the
+ * #NMSecretAgentSimpleSecrets (freeing any initial values they had), and
+ * pass the array to nm_secret_agent_simple_response(). If the user
+ * cancelled the request, @secrets should be NULL.
+ */
+void
+nm_secret_agent_simple_response(NMSecretAgentSimple *self,
+                                const char *         request_id,
+                                GPtrArray *          secrets)
+{
+    NMSecretAgentSimplePrivate *priv;
+    RequestData *               request;
+    gs_unref_variant GVariant *secrets_dict = NULL;
+    gs_free_error GError *error             = NULL;
+    int                   i;
+
+    g_return_if_fail(NM_IS_SECRET_AGENT_SIMPLE(self));
+
+    priv    = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self);
+    request = g_hash_table_lookup(priv->requests, &request_id);
+    g_return_if_fail(request != NULL);
+
+    if (secrets) {
+        GVariantBuilder conn_builder, *setting_builder;
+        GVariantBuilder vpn_secrets_builder;
+        GVariantBuilder wg_secrets_builder;
+        GVariantBuilder wg_peer_builder;
+        GHashTable *    settings;
+        GHashTableIter  iter;
+        const char *    name;
+        gboolean        has_vpn = FALSE;
+        gboolean        has_wg  = FALSE;
+
+        settings = g_hash_table_new_full(nm_str_hash,
+                                         g_str_equal,
+                                         NULL,
+                                         (GDestroyNotify) g_variant_builder_unref);
+        for (i = 0; i < secrets->len; i++) {
+            SecretReal *secret = secrets->pdata[i];
+
+            setting_builder = g_hash_table_lookup(settings, nm_setting_get_name(secret->setting));
+            if (!setting_builder) {
+                setting_builder = g_variant_builder_new(NM_VARIANT_TYPE_SETTING);
+                g_hash_table_insert(settings,
+                                    (char *) nm_setting_get_name(secret->setting),
+                                    setting_builder);
+            }
+
+            switch (secret->base.secret_type) {
+            case NM_SECRET_AGENT_SECRET_TYPE_PROPERTY:
+            case NM_SECRET_AGENT_SECRET_TYPE_SECRET:
+                g_variant_builder_add(setting_builder,
+                                      "{sv}",
+                                      secret->property,
+                                      g_variant_new_string(secret->base.value));
+                break;
+            case NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET:
+                if (!has_vpn) {
+                    g_variant_builder_init(&vpn_secrets_builder, G_VARIANT_TYPE("a{ss}"));
+                    has_vpn = TRUE;
+                }
+                g_variant_builder_add(&vpn_secrets_builder,
+                                      "{ss}",
+                                      secret->property,
+                                      secret->base.value);
+                break;
+            case NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK:
+                if (!has_wg) {
+                    g_variant_builder_init(&wg_secrets_builder, G_VARIANT_TYPE("aa{sv}"));
+                    has_wg = TRUE;
+                }
+                g_variant_builder_init(&wg_peer_builder, G_VARIANT_TYPE("a{sv}"));
+                g_variant_builder_add(&wg_peer_builder,
+                                      "{sv}",
+                                      NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY,
+                                      g_variant_new_string(secret->property));
+                g_variant_builder_add(&wg_peer_builder,
+                                      "{sv}",
+                                      NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY,
+                                      g_variant_new_string(secret->base.value));
+                g_variant_builder_add(&wg_secrets_builder, "a{sv}", &wg_peer_builder);
+                break;
+            }
+        }
+
+        if (has_vpn) {
+            g_variant_builder_add(setting_builder,
+                                  "{sv}",
+                                  "secrets",
+                                  g_variant_builder_end(&vpn_secrets_builder));
+        }
+
+        if (has_wg) {
+            g_variant_builder_add(setting_builder,
+                                  "{sv}",
+                                  NM_SETTING_WIREGUARD_PEERS,
+                                  g_variant_builder_end(&wg_secrets_builder));
+        }
+
+        g_variant_builder_init(&conn_builder, NM_VARIANT_TYPE_CONNECTION);
+        g_hash_table_iter_init(&iter, settings);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &setting_builder))
+            g_variant_builder_add(&conn_builder, "{sa{sv}}", name, setting_builder);
+        secrets_dict = g_variant_ref_sink(g_variant_builder_end(&conn_builder));
+        g_hash_table_destroy(settings);
+    } else {
+        error = g_error_new(NM_SECRET_AGENT_ERROR,
+                            NM_SECRET_AGENT_ERROR_USER_CANCELED,
+                            "User cancelled");
+    }
+
+    _request_data_complete(request, secrets_dict, error, NULL);
+}
+
+static void
+cancel_get_secrets(NMSecretAgentOld *agent, const char *connection_path, const char *setting_name)
+{
+    NMSecretAgentSimple *       self = NM_SECRET_AGENT_SIMPLE(agent);
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self);
+    gs_free_error GError *error      = NULL;
+    gs_free char *        request_id = NULL;
+    RequestData *         request;
+
+    request_id = g_strdup_printf("%s/%s", connection_path, setting_name);
+    request    = g_hash_table_lookup(priv->requests, &request_id);
+    if (!request) {
+        /* this is really a bug of the caller (or us?). We cannot invoke a callback,
+         * hence the caller cannot cleanup the request. */
+        g_return_if_reached();
+    }
+
+    g_set_error(&error,
+                NM_SECRET_AGENT_ERROR,
+                NM_SECRET_AGENT_ERROR_AGENT_CANCELED,
+                "The secret agent is going away");
+    _request_data_complete(request, NULL, error, NULL);
+}
+
+static void
+save_secrets(NMSecretAgentOld *              agent,
+             NMConnection *                  connection,
+             const char *                    connection_path,
+             NMSecretAgentOldSaveSecretsFunc callback,
+             gpointer                        callback_data)
+{
+    /* We don't support secret storage */
+    callback(agent, connection, NULL, callback_data);
+}
+
+static void
+delete_secrets(NMSecretAgentOld *                agent,
+               NMConnection *                    connection,
+               const char *                      connection_path,
+               NMSecretAgentOldDeleteSecretsFunc callback,
+               gpointer                          callback_data)
+{
+    /* We don't support secret storage, so there's nothing to delete. */
+    callback(agent, connection, NULL, callback_data);
+}
+
+/**
+ * nm_secret_agent_simple_enable:
+ * @self: the #NMSecretAgentSimple
+ * @path: (allow-none): the path of the connection (if any) to handle secrets
+ *        for.  If %NULL, secrets for any connection will be handled.
+ *
+ * Enables servicing the requests including the already queued ones.  If @path
+ * is given, the agent will only handle requests for connections that match
+ * @path.
+ */
+void
+nm_secret_agent_simple_enable(NMSecretAgentSimple *self, const char *path)
+{
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self);
+    gs_free RequestData **requests   = NULL;
+    gsize                 i;
+    gs_free char *        path_full = NULL;
+
+    /* The path is only used to match a request_id with the current
+     * connection. Since the request_id is "${CONNECTION_PATH}/${SETTING}",
+     * add a trailing '/' to the path to match the full connection path.
+     */
+    path_full = path ? g_strdup_printf("%s/", path) : NULL;
+
+    if (!nm_streq0(path_full, priv->path)) {
+        g_free(priv->path);
+        priv->path = g_steal_pointer(&path_full);
+    }
+
+    if (priv->enabled)
+        return;
+    priv->enabled = TRUE;
+
+    /* Service pending secret requests. */
+    requests = (RequestData **) g_hash_table_get_keys_as_array(priv->requests, NULL);
+    for (i = 0; requests[i]; i++)
+        request_secrets_from_ui(requests[i]);
+}
+
+/*****************************************************************************/
+
+static void
+nm_secret_agent_simple_init(NMSecretAgentSimple *agent)
+{
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(agent);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(RequestData, request_id) == 0);
+    priv->requests = g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, NULL, _request_data_free);
+}
+
+/**
+ * nm_secret_agent_simple_new:
+ * @name: the identifier of secret agent
+ *
+ * Creates a new #NMSecretAgentSimple. It does not serve any requests until
+ * nm_secret_agent_simple_enable() is called.
+ *
+ * Returns: a new #NMSecretAgentSimple if the agent creation is successful
+ * or %NULL in case of a failure.
+ */
+NMSecretAgentSimple *
+nm_secret_agent_simple_new(const char *name)
+{
+    return g_initable_new(NM_TYPE_SECRET_AGENT_SIMPLE,
+                          NULL,
+                          NULL,
+                          NM_SECRET_AGENT_OLD_IDENTIFIER,
+                          name,
+                          NM_SECRET_AGENT_OLD_CAPABILITIES,
+                          NM_SECRET_AGENT_CAPABILITY_VPN_HINTS,
+                          NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object);
+    gs_free_error GError *error      = NULL;
+    GHashTableIter        iter;
+    RequestData *         request;
+
+    g_hash_table_iter_init(&iter, priv->requests);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &request)) {
+        if (!error)
+            nm_utils_error_set_cancelled(&error, TRUE, "NMSecretAgentSimple");
+        _request_data_complete(request, NULL, error, &iter);
+    }
+
+    G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object);
+
+    g_hash_table_destroy(priv->requests);
+
+    g_free(priv->path);
+
+    G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->finalize(object);
+}
+
+void
+nm_secret_agent_simple_class_init(NMSecretAgentSimpleClass *klass)
+{
+    GObjectClass *         object_class = G_OBJECT_CLASS(klass);
+    NMSecretAgentOldClass *agent_class  = NM_SECRET_AGENT_OLD_CLASS(klass);
+
+    object_class->dispose  = dispose;
+    object_class->finalize = finalize;
+
+    agent_class->get_secrets        = get_secrets;
+    agent_class->cancel_get_secrets = cancel_get_secrets;
+    agent_class->save_secrets       = save_secrets;
+    agent_class->delete_secrets     = delete_secrets;
+
+    /**
+     * NMSecretAgentSimple::request-secrets:
+     * @agent: the #NMSecretAgentSimple
+     * @request_id: request ID, to eventually pass to
+     *   nm_secret_agent_simple_response().
+     * @title: a title for the password dialog
+     * @prompt: a prompt message for the password dialog
+     * @secrets: (element-type #NMSecretAgentSimpleSecret): array of secrets
+     *   being requested.
+     *
+     * Emitted when the agent requires secrets from the user.
+     *
+     * The application should ask user for the secrets. For example,
+     * nmtui should create a password dialog (#NmtPasswordDialog)
+     * with the given title and prompt, and an entry for each
+     * element of @secrets. If any of the secrets already have a
+     * <literal>value</literal> filled in, the corresponding entry
+     * should be initialized to that value.
+     *
+     * When the dialog is complete, the app must call
+     * nm_secret_agent_simple_response() with the results.
+     */
+    signals[REQUEST_SECRETS] = g_signal_new(NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS,
+                                            G_TYPE_FROM_CLASS(klass),
+                                            0,
+                                            0,
+                                            NULL,
+                                            NULL,
+                                            NULL,
+                                            G_TYPE_NONE,
+                                            4,
+                                            G_TYPE_STRING, /* request_id */
+                                            G_TYPE_STRING, /* title */
+                                            G_TYPE_STRING, /* prompt */
+                                            G_TYPE_PTR_ARRAY);
+}
diff --git a/src/libnmc-base/nm-secret-agent-simple.h b/src/libnmc-base/nm-secret-agent-simple.h
new file mode 100644
index 00000000..878f9c75
--- /dev/null
+++ b/src/libnmc-base/nm-secret-agent-simple.h
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_SECRET_AGENT_SIMPLE_H__
+#define __NM_SECRET_AGENT_SIMPLE_H__
+
+#include "nm-secret-agent-old.h"
+
+typedef enum {
+    NM_SECRET_AGENT_SECRET_TYPE_PROPERTY,
+    NM_SECRET_AGENT_SECRET_TYPE_SECRET,
+    NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET,
+    NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK,
+} NMSecretAgentSecretType;
+
+typedef struct {
+    NMSecretAgentSecretType secret_type;
+    const char *            pretty_name;
+    const char *            entry_id;
+    char *                  value;
+    const char *            vpn_type;
+    bool                    is_secret : 1;
+    bool                    no_prompt_entry_id : 1;
+} NMSecretAgentSimpleSecret;
+
+#define NM_SECRET_AGENT_ENTRY_ID_PREFX_VPN_SECRETS "vpn.secrets."
+
+#define NM_SECRET_AGENT_VPN_TYPE_OPENCONNECT NM_DBUS_INTERFACE ".openconnect"
+
+/*****************************************************************************/
+
+#define NM_TYPE_SECRET_AGENT_SIMPLE (nm_secret_agent_simple_get_type())
+#define NM_SECRET_AGENT_SIMPLE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimple))
+#define NM_SECRET_AGENT_SIMPLE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimpleClass))
+#define NM_IS_SECRET_AGENT_SIMPLE(obj) \
+    (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SECRET_AGENT_SIMPLE))
+#define NM_IS_SECRET_AGENT_SIMPLE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_SECRET_AGENT_SIMPLE))
+#define NM_SECRET_AGENT_SIMPLE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_SECRET_AGENT_SIMPLE, NMSecretAgentSimpleClass))
+
+#define NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS "request-secrets"
+
+typedef struct _NMSecretAgentSimple      NMSecretAgentSimple;
+typedef struct _NMSecretAgentSimpleClass NMSecretAgentSimpleClass;
+
+GType nm_secret_agent_simple_get_type(void);
+
+NMSecretAgentSimple *nm_secret_agent_simple_new(const char *name);
+
+void nm_secret_agent_simple_response(NMSecretAgentSimple *self,
+                                     const char *         request_id,
+                                     GPtrArray *          secrets);
+
+void nm_secret_agent_simple_enable(NMSecretAgentSimple *self, const char *path);
+
+#endif /* __NM_SECRET_AGENT_SIMPLE_H__ */
diff --git a/src/libnmc-base/nm-vpn-helpers.c b/src/libnmc-base/nm-vpn-helpers.c
new file mode 100644
index 00000000..72691e34
--- /dev/null
+++ b/src/libnmc-base/nm-vpn-helpers.c
@@ -0,0 +1,821 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+/**
+ * SECTION:nm-vpn-helpers
+ * @short_description: VPN-related utilities
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nm-vpn-helpers.h"
+
+#include <arpa/inet.h>
+#include <net/if.h>
+
+#include "nm-client-utils.h"
+#include "nm-utils.h"
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-glib-aux/nm-secret-utils.h"
+
+/*****************************************************************************/
+
+NMVpnEditorPlugin *
+nm_vpn_get_editor_plugin(const char *service_type, GError **error)
+{
+    NMVpnEditorPlugin *plugin = NULL;
+    NMVpnPluginInfo *  plugin_info;
+    gs_free_error GError *local = NULL;
+
+    g_return_val_if_fail(service_type, NULL);
+    g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+
+    plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(), service_type);
+
+    if (!plugin_info) {
+        g_set_error(error,
+                    NM_VPN_PLUGIN_ERROR,
+                    NM_VPN_PLUGIN_ERROR_FAILED,
+                    _("unknown VPN plugin \"%s\""),
+                    service_type);
+        return NULL;
+    }
+    plugin = nm_vpn_plugin_info_get_editor_plugin(plugin_info);
+    if (!plugin)
+        plugin = nm_vpn_plugin_info_load_editor_plugin(plugin_info, &local);
+
+    if (!plugin) {
+        if (!nm_vpn_plugin_info_get_plugin(plugin_info)
+            && nm_vpn_plugin_info_lookup_property(plugin_info,
+                                                  NM_VPN_PLUGIN_INFO_KF_GROUP_GNOME,
+                                                  "properties")) {
+            g_set_error(error,
+                        NM_VPN_PLUGIN_ERROR,
+                        NM_VPN_PLUGIN_ERROR_FAILED,
+                        _("cannot load legacy-only VPN plugin \"%s\" for \"%s\""),
+                        nm_vpn_plugin_info_get_name(plugin_info),
+                        nm_vpn_plugin_info_get_filename(plugin_info));
+        } else if (g_error_matches(local, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+            g_set_error(
+                error,
+                NM_VPN_PLUGIN_ERROR,
+                NM_VPN_PLUGIN_ERROR_FAILED,
+                _("cannot load VPN plugin \"%s\" due to missing \"%s\". Missing client plugin?"),
+                nm_vpn_plugin_info_get_name(plugin_info),
+                nm_vpn_plugin_info_get_plugin(plugin_info));
+        } else {
+            g_set_error(error,
+                        NM_VPN_PLUGIN_ERROR,
+                        NM_VPN_PLUGIN_ERROR_FAILED,
+                        _("failed to load VPN plugin \"%s\": %s"),
+                        nm_vpn_plugin_info_get_name(plugin_info),
+                        local->message);
+        }
+        return NULL;
+    }
+
+    return plugin;
+}
+
+GSList *
+nm_vpn_get_plugin_infos(void)
+{
+    static bool    plugins_loaded;
+    static GSList *plugins = NULL;
+
+    if (G_LIKELY(plugins_loaded))
+        return plugins;
+    plugins_loaded = TRUE;
+    plugins        = nm_vpn_plugin_info_list_load();
+    return plugins;
+}
+
+gboolean
+nm_vpn_supports_ipv6(NMConnection *connection)
+{
+    NMSettingVpn *     s_vpn;
+    const char *       service_type;
+    NMVpnEditorPlugin *plugin;
+    guint32            capabilities;
+
+    s_vpn = nm_connection_get_setting_vpn(connection);
+    g_return_val_if_fail(s_vpn != NULL, FALSE);
+
+    service_type = nm_setting_vpn_get_service_type(s_vpn);
+    if (!service_type)
+        return FALSE;
+
+    plugin = nm_vpn_get_editor_plugin(service_type, NULL);
+    if (!plugin)
+        return FALSE;
+
+    capabilities = nm_vpn_editor_plugin_get_capabilities(plugin);
+    return NM_FLAGS_HAS(capabilities, NM_VPN_EDITOR_PLUGIN_CAPABILITY_IPV6);
+}
+
+const NmcVpnPasswordName *
+nm_vpn_get_secret_names(const char *service_type)
+{
+    const char *type;
+
+    if (!service_type)
+        return NULL;
+
+    if (!NM_STR_HAS_PREFIX(service_type, NM_DBUS_INTERFACE)
+        || service_type[NM_STRLEN(NM_DBUS_INTERFACE)] != '.') {
+        /* all our well-known, hard-coded vpn-types start with NM_DBUS_INTERFACE. */
+        return NULL;
+    }
+
+    type = service_type + (NM_STRLEN(NM_DBUS_INTERFACE) + 1);
+
+#define _VPN_PASSWORD_LIST(...)                    \
+    ({                                             \
+        static const NmcVpnPasswordName _arr[] = { \
+            __VA_ARGS__{0},                        \
+        };                                         \
+        _arr;                                      \
+    })
+
+    if (NM_IN_STRSET(type, "pptp", "iodine", "ssh", "l2tp", "fortisslvpn")) {
+        return _VPN_PASSWORD_LIST({"password", N_("Password")}, );
+    }
+
+    if (NM_IN_STRSET(type, "openvpn")) {
+        return _VPN_PASSWORD_LIST({"password", N_("Password")},
+                                  {"cert-pass", N_("Certificate password")},
+                                  {"http-proxy-password", N_("HTTP proxy password")}, );
+    }
+
+    if (NM_IN_STRSET(type, "vpnc")) {
+        return _VPN_PASSWORD_LIST({"Xauth password", N_("Password")},
+                                  {"IPSec secret", N_("Group password")}, );
+    };
+
+    if (NM_IN_STRSET(type, "openswan", "libreswan", "strongswan")) {
+        return _VPN_PASSWORD_LIST({"xauthpassword", N_("Password")},
+                                  {"pskvalue", N_("Group password")}, );
+    };
+
+    if (NM_IN_STRSET(type, "openconnect")) {
+        return _VPN_PASSWORD_LIST({"gateway", N_("Gateway")},
+                                  {"cookie", N_("Cookie")},
+                                  {"gwcert", N_("Gateway certificate hash")}, );
+    };
+
+    return NULL;
+}
+
+static gboolean
+_extract_variable_value(char *line, const char *tag, char **value)
+{
+    char *p1, *p2;
+
+    if (!g_str_has_prefix(line, tag))
+        return FALSE;
+
+    p1 = line + strlen(tag);
+    p2 = line + strlen(line) - 1;
+    if ((*p1 == '\'' || *p1 == '"') && (*p1 == *p2)) {
+        p1++;
+        *p2 = '\0';
+    }
+    NM_SET_OUT(value, g_strdup(p1));
+    return TRUE;
+}
+
+gboolean
+nm_vpn_openconnect_authenticate_helper(const char *host,
+                                       char **     cookie,
+                                       char **     gateway,
+                                       char **     gwcert,
+                                       int *       status,
+                                       GError **   error)
+{
+    gs_free char *       output   = NULL;
+    gs_free const char **output_v = NULL;
+    const char *const *  iter;
+    const char *         path;
+    const char *const    DEFAULT_PATHS[] = {
+        "/sbin/",
+        "/usr/sbin/",
+        "/usr/local/sbin/",
+        "/bin/",
+        "/usr/bin/",
+        "/usr/local/bin/",
+        NULL,
+    };
+
+    path = nm_utils_file_search_in_paths("openconnect",
+                                         "/usr/sbin/openconnect",
+                                         DEFAULT_PATHS,
+                                         G_FILE_TEST_IS_EXECUTABLE,
+                                         NULL,
+                                         NULL,
+                                         error);
+    if (!path)
+        return FALSE;
+
+    if (!g_spawn_sync(NULL,
+                      (char **) NM_MAKE_STRV(path, "--authenticate", host),
+                      NULL,
+                      G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN,
+                      NULL,
+                      NULL,
+                      &output,
+                      NULL,
+                      status,
+                      error))
+        return FALSE;
+
+    /* Parse output and set cookie, gateway and gwcert
+     * output example:
+     * COOKIE='loremipsum'
+     * HOST='1.2.3.4'
+     * FINGERPRINT='sha1:32bac90cf09a722e10ecc1942c67fe2ac8c21e2e'
+     */
+    output_v = nm_utils_strsplit_set_with_empty(output, "\r\n");
+    for (iter = output_v; iter && *iter; iter++) {
+        char *s_mutable = (char *) *iter;
+
+        _extract_variable_value(s_mutable, "COOKIE=", cookie);
+        _extract_variable_value(s_mutable, "HOST=", gateway);
+        _extract_variable_value(s_mutable, "FINGERPRINT=", gwcert);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_wg_complete_peer(GPtrArray **     p_peers,
+                  NMWireGuardPeer *peer_take,
+                  gsize            peer_start_line_nr,
+                  const char *     filename,
+                  GError **        error)
+{
+    nm_auto_unref_wgpeer NMWireGuardPeer *peer = peer_take;
+    gs_free_error GError *local                = NULL;
+
+    if (!peer)
+        return TRUE;
+
+    if (!nm_wireguard_peer_is_valid(peer, TRUE, TRUE, &local)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           _("Invalid peer starting at %s:%zu: %s"),
+                           filename,
+                           peer_start_line_nr,
+                           local->message);
+        return FALSE;
+    }
+
+    if (!*p_peers)
+        *p_peers = g_ptr_array_new_with_free_func((GDestroyNotify) nm_wireguard_peer_unref);
+    g_ptr_array_add(*p_peers, g_steal_pointer(&peer));
+    return TRUE;
+}
+
+static gboolean
+_line_match(char *line, const char *key, gsize key_len, const char **out_key, char **out_value)
+{
+    nm_assert(line);
+    nm_assert(key);
+    nm_assert(strlen(key) == key_len);
+    nm_assert(!strchr(key, '='));
+    nm_assert(out_key && !*out_key);
+    nm_assert(out_value && !*out_value);
+
+    /* Note that `wg-quick` (linux.bash) does case-insensitive comparison (shopt -s nocasematch).
+     * `wg setconf` does case-insensitive comparison too (with strncasecmp, which is locale dependent).
+     *
+     * We do a case-insensitive comparison of the key, however in a locale-independent manner. */
+
+    if (g_ascii_strncasecmp(line, key, key_len) != 0)
+        return FALSE;
+
+    if (line[key_len] != '=')
+        return FALSE;
+
+    *out_key   = key;
+    *out_value = &line[key_len + 1];
+    return TRUE;
+}
+
+#define line_match(line, key, out_key, out_value) \
+    _line_match((line), "" key "", NM_STRLEN(key), (out_key), (out_value))
+
+static gboolean
+value_split_word(char **line_remainder, char **out_word)
+{
+    char *str;
+
+    if ((*line_remainder)[0] == '\0')
+        return FALSE;
+
+    *out_word = *line_remainder;
+
+    str = strchrnul(*line_remainder, ',');
+    if (str[0] == ',') {
+        str[0]          = '\0';
+        *line_remainder = &str[1];
+    } else
+        *line_remainder = str;
+    return TRUE;
+}
+
+NMConnection *
+nm_vpn_wireguard_import(const char *filename, GError **error)
+{
+    nm_auto_clear_secret_ptr NMSecretPtr file_content = NM_SECRET_PTR_INIT();
+    char                                 ifname[IFNAMSIZ];
+    gs_free char *                       uuid         = NULL;
+    gboolean                             ifname_valid = FALSE;
+    const char *                         cstr;
+    char *                               line_remainder;
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingConnection *         s_con;
+    NMSettingIPConfig *           s_ip4;
+    NMSettingIPConfig *           s_ip6;
+    NMSettingWireGuard *          s_wg;
+    gs_free_error GError *local = NULL;
+    enum {
+        LINE_CONTEXT_INIT,
+        LINE_CONTEXT_INTERFACE,
+        LINE_CONTEXT_PEER,
+    } line_context;
+    gsize                line_nr;
+    gsize                current_peer_start_line_nr    = 0;
+    nm_auto_unref_wgpeer NMWireGuardPeer *current_peer = NULL;
+    gs_unref_ptrarray GPtrArray *data_dns_search       = NULL;
+    gs_unref_ptrarray GPtrArray *data_dns_v4           = NULL;
+    gs_unref_ptrarray GPtrArray *data_dns_v6           = NULL;
+    gs_unref_ptrarray GPtrArray *data_addr_v4          = NULL;
+    gs_unref_ptrarray GPtrArray *data_addr_v6          = NULL;
+    gs_unref_ptrarray GPtrArray *data_peers            = NULL;
+    const char *                 data_private_key      = NULL;
+    gint64                       data_table;
+    guint                        data_listen_port = 0;
+    guint                        data_fwmark      = 0;
+    guint                        data_mtu         = 0;
+    int                          is_v4;
+    guint                        i;
+
+    g_return_val_if_fail(filename, NULL);
+    g_return_val_if_fail(!error || !*error, NULL);
+
+    /* contrary to "wg-quick", we never interpret the filename as "/etc/wireguard/$INTERFACE.conf".
+     * If the filename has no '/', it is interpreted as relative to the current working directory.
+     * However, we do require a suitable filename suffix and that the name corresponds to the interface
+     * name. */
+    cstr = strrchr(filename, '/');
+    cstr = cstr ? &cstr[1] : filename;
+    if (NM_STR_HAS_SUFFIX(cstr, ".conf")) {
+        gsize len = strlen(cstr) - NM_STRLEN(".conf");
+
+        if (len > 0 && len < sizeof(ifname)) {
+            memcpy(ifname, cstr, len);
+            ifname[len] = '\0';
+
+            if (nm_utils_ifname_valid(ifname, NMU_IFACE_KERNEL, NULL))
+                ifname_valid = TRUE;
+        }
+    }
+    if (!ifname_valid) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_UNKNOWN,
+                                   _("The name of the WireGuard config must be a valid interface "
+                                     "name followed by \".conf\""));
+        return FALSE;
+    }
+
+    if (!nm_utils_file_get_contents(-1,
+                                    filename,
+                                    10 * 1024 * 1024,
+                                    NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
+                                    &file_content.str,
+                                    &file_content.len,
+                                    NULL,
+                                    error))
+        return NULL;
+
+        /* We interpret the file like `wg-quick up` and `wg setconf` do.
+     *
+     * Of course the WireGuard scripts do something fundamentlly different. They
+     * perform actions to configure the WireGuard link in kernel, add routes and
+     * addresses, and call resolvconf. It all happens at the time when the script
+     * run.
+     *
+     * This code here instead generates a NetworkManager connection profile so that
+     * NetworkManager will apply a similar configuration when later activating the profile. */
+
+#define _TABLE_AUTO ((gint64) -1)
+#define _TABLE_OFF  ((gint64) -2)
+
+    data_table = _TABLE_AUTO;
+
+    line_remainder = file_content.str;
+    line_context   = LINE_CONTEXT_INIT;
+    line_nr        = 0;
+    while (line_remainder[0] != '\0') {
+        const char *matched_key = NULL;
+        char *      value       = NULL;
+        char *      line;
+        char        ch;
+        gint64      i64;
+
+        line_nr++;
+
+        line           = line_remainder;
+        line_remainder = strchrnul(line, '\n');
+        if (line_remainder[0] != '\0')
+            (line_remainder++)[0] = '\0';
+
+        /* Drop all spaces and truncate at first '#'.
+         * See wg's config_read_line().
+         *
+         * Note that wg-quick doesn't do that.
+         *
+         * Neither `wg setconf` nor `wg-quick` does a strict parsing.
+         * We don't either. Just try to interpret the file (mostly) the same as
+         * they would.
+         */
+        {
+            gsize l, n;
+
+            n = 0;
+            for (l = 0; (ch = line[l]); l++) {
+                if (g_ascii_isspace(ch)) {
+                    /* wg-setconf strips all whitespace before parsing the content. That means,
+                     * *[I nterface]" will be accepted. We do that too. */
+                    continue;
+                }
+                if (ch == '#')
+                    break;
+                line[n++] = line[l];
+            }
+            if (n == 0)
+                continue;
+            line[n] = '\0';
+        }
+
+        if (g_ascii_strcasecmp(line, "[Interface]") == 0) {
+            if (!_wg_complete_peer(&data_peers,
+                                   g_steal_pointer(&current_peer),
+                                   current_peer_start_line_nr,
+                                   filename,
+                                   error))
+                return FALSE;
+            line_context = LINE_CONTEXT_INTERFACE;
+            continue;
+        }
+
+        if (g_ascii_strcasecmp(line, "[Peer]") == 0) {
+            if (!_wg_complete_peer(&data_peers,
+                                   g_steal_pointer(&current_peer),
+                                   current_peer_start_line_nr,
+                                   filename,
+                                   error))
+                return FALSE;
+            current_peer_start_line_nr = line_nr;
+            current_peer               = nm_wireguard_peer_new();
+            line_context               = LINE_CONTEXT_PEER;
+            continue;
+        }
+
+        if (line_context == LINE_CONTEXT_INTERFACE) {
+            if (line_match(line, "Address", &matched_key, &value)) {
+                char *value_word;
+
+                while (value_split_word(&value, &value_word)) {
+                    GPtrArray **p_data_addr;
+                    NMIPAddr    addr_bin;
+                    int         addr_family;
+                    int         prefix_len;
+
+                    if (!nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC,
+                                                          value_word,
+                                                          &addr_family,
+                                                          &addr_bin,
+                                                          &prefix_len))
+                        goto fail_invalid_value;
+
+                    p_data_addr = (addr_family == AF_INET) ? &data_addr_v4 : &data_addr_v6;
+
+                    if (!*p_data_addr)
+                        *p_data_addr =
+                            g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+
+                    g_ptr_array_add(
+                        *p_data_addr,
+                        nm_ip_address_new_binary(
+                            addr_family,
+                            &addr_bin,
+                            prefix_len == -1 ? ((addr_family == AF_INET) ? 32 : 128) : prefix_len,
+                            NULL));
+                }
+                continue;
+            }
+
+            if (line_match(line, "MTU", &matched_key, &value)) {
+                i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXUINT32, -1);
+                if (i64 == -1)
+                    goto fail_invalid_value;
+
+                /* wg-quick accepts the "MTU" value, but it also fetches routes to
+                 * autodetect it. NetworkManager won't do that, we can only configure
+                 * an explicit MTU or no autodetection will be performed. */
+                data_mtu = i64;
+                continue;
+            }
+
+            if (line_match(line, "DNS", &matched_key, &value)) {
+                char *value_word;
+
+                while (value_split_word(&value, &value_word)) {
+                    GPtrArray **p_data_dns;
+                    NMIPAddr    addr_bin;
+                    int         addr_family;
+
+                    if (nm_utils_parse_inaddr_bin(AF_UNSPEC, value_word, &addr_family, &addr_bin)) {
+                        p_data_dns = (addr_family == AF_INET) ? &data_dns_v4 : &data_dns_v6;
+                        if (!*p_data_dns)
+                            *p_data_dns = g_ptr_array_new_with_free_func(g_free);
+
+                        g_ptr_array_add(*p_data_dns,
+                                        nm_utils_inet_ntop_dup(addr_family, &addr_bin));
+                        continue;
+                    }
+
+                    if (!data_dns_search)
+                        data_dns_search = g_ptr_array_new_with_free_func(g_free);
+                    g_ptr_array_add(data_dns_search, g_strdup(value_word));
+                }
+                continue;
+            }
+
+            if (line_match(line, "Table", &matched_key, &value)) {
+                if (nm_streq(value, "auto"))
+                    data_table = _TABLE_AUTO;
+                else if (nm_streq(value, "off"))
+                    data_table = _TABLE_OFF;
+                else {
+                    /* we don't support table names from /etc/iproute2/rt_tables
+                     * But we accept hex like `ip route add` would. */
+                    i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXINT32, -1);
+                    if (i64 == -1)
+                        goto fail_invalid_value;
+                    data_table = i64;
+                }
+                continue;
+            }
+
+            if (line_match(line, "PreUp", &matched_key, &value)
+                || line_match(line, "PreDown", &matched_key, &value)
+                || line_match(line, "PostUp", &matched_key, &value)
+                || line_match(line, "PostDown", &matched_key, &value)) {
+                /* we don't run any scripts. Silently ignore these parameters. */
+                continue;
+            }
+
+            if (line_match(line, "SaveConfig", &matched_key, &value)) {
+                /* we ignore the setting, but enforce that it's either true or false (like
+                 * wg-quick. */
+                if (!NM_IN_STRSET(value, "true", "false"))
+                    goto fail_invalid_value;
+                continue;
+            }
+
+            if (line_match(line, "ListenPort", &matched_key, &value)) {
+                /* we don't use getaddrinfo(), unlike `wg setconf`. Just interpret
+                 * the port as plain decimal number. */
+                i64 = _nm_utils_ascii_str_to_int64(value, 10, 0, 0xFFFF, -1);
+                if (i64 == -1)
+                    goto fail_invalid_value;
+                data_listen_port = i64;
+                continue;
+            }
+
+            if (line_match(line, "FwMark", &matched_key, &value)) {
+                if (nm_streq(value, "off"))
+                    data_fwmark = 0;
+                else {
+                    i64 = _nm_utils_ascii_str_to_int64(value, 0, 0, G_MAXINT32, -1);
+                    if (i64 == -1)
+                        goto fail_invalid_value;
+                    data_fwmark = i64;
+                }
+                continue;
+            }
+
+            if (line_match(line, "PrivateKey", &matched_key, &value)) {
+                if (!nm_utils_base64secret_decode(value, NM_WIREGUARD_PUBLIC_KEY_LEN, NULL))
+                    goto fail_invalid_secret;
+                data_private_key = value;
+                continue;
+            }
+
+            goto fail_invalid_line;
+        }
+
+        if (line_context == LINE_CONTEXT_PEER) {
+            if (line_match(line, "Endpoint", &matched_key, &value)) {
+                if (!nm_wireguard_peer_set_endpoint(current_peer, value, FALSE))
+                    goto fail_invalid_value;
+                continue;
+            }
+
+            if (line_match(line, "PublicKey", &matched_key, &value)) {
+                if (!nm_wireguard_peer_set_public_key(current_peer, value, FALSE))
+                    goto fail_invalid_value;
+                continue;
+            }
+
+            if (line_match(line, "AllowedIPs", &matched_key, &value)) {
+                char *value_word;
+
+                while (value_split_word(&value, &value_word)) {
+                    if (!nm_wireguard_peer_append_allowed_ip(current_peer, value_word, FALSE))
+                        goto fail_invalid_value;
+                }
+                continue;
+            }
+
+            if (line_match(line, "PersistentKeepalive", &matched_key, &value)) {
+                if (nm_streq(value, "off"))
+                    i64 = 0;
+                else {
+                    i64 = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXUINT16, -1);
+                    if (i64 == -1)
+                        goto fail_invalid_value;
+                }
+                nm_wireguard_peer_set_persistent_keepalive(current_peer, i64);
+                continue;
+            }
+
+            if (line_match(line, "PresharedKey", &matched_key, &value)) {
+                if (!nm_wireguard_peer_set_preshared_key(current_peer, value, FALSE))
+                    goto fail_invalid_secret;
+                nm_wireguard_peer_set_preshared_key_flags(current_peer,
+                                                          NM_SETTING_SECRET_FLAG_NONE);
+                continue;
+            }
+
+            goto fail_invalid_line;
+        }
+
+fail_invalid_line:
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_INVALID_ARGUMENT,
+                           _("unrecognized line at %s:%zu"),
+                           filename,
+                           line_nr);
+        return FALSE;
+fail_invalid_value:
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_INVALID_ARGUMENT,
+                           _("invalid value for '%s' at %s:%zu"),
+                           matched_key,
+                           filename,
+                           line_nr);
+        return FALSE;
+fail_invalid_secret:
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_INVALID_ARGUMENT,
+                           _("invalid secret '%s' at %s:%zu"),
+                           matched_key,
+                           filename,
+                           line_nr);
+        return FALSE;
+    }
+
+    if (!_wg_complete_peer(&data_peers,
+                           g_steal_pointer(&current_peer),
+                           current_peer_start_line_nr,
+                           filename,
+                           error))
+        return FALSE;
+
+    connection = nm_simple_connection_new();
+    s_con      = NM_SETTING_CONNECTION(nm_setting_connection_new());
+    nm_connection_add_setting(connection, NM_SETTING(s_con));
+    s_ip4 = NM_SETTING_IP_CONFIG(nm_setting_ip4_config_new());
+    nm_connection_add_setting(connection, NM_SETTING(s_ip4));
+    s_ip6 = NM_SETTING_IP_CONFIG(nm_setting_ip6_config_new());
+    nm_connection_add_setting(connection, NM_SETTING(s_ip6));
+    s_wg = NM_SETTING_WIREGUARD(nm_setting_wireguard_new());
+    nm_connection_add_setting(connection, NM_SETTING(s_wg));
+
+    uuid = nm_utils_uuid_generate();
+
+    g_object_set(s_con,
+                 NM_SETTING_CONNECTION_ID,
+                 ifname,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_WIREGUARD_SETTING_NAME,
+                 NM_SETTING_CONNECTION_INTERFACE_NAME,
+                 ifname,
+                 NULL);
+
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
+                 data_private_key,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 data_listen_port,
+                 NM_SETTING_WIREGUARD_FWMARK,
+                 data_fwmark,
+                 NM_SETTING_WIREGUARD_MTU,
+                 data_mtu,
+                 NULL);
+
+    if (data_peers) {
+        for (i = 0; i < data_peers->len; i++)
+            nm_setting_wireguard_append_peer(s_wg, data_peers->pdata[i]);
+    }
+
+    for (is_v4 = 0; is_v4 < 2; is_v4++) {
+        const char *method_disabled =
+            is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED : NM_SETTING_IP6_CONFIG_METHOD_DISABLED;
+        const char *method_manual =
+            is_v4 ? NM_SETTING_IP4_CONFIG_METHOD_MANUAL : NM_SETTING_IP6_CONFIG_METHOD_MANUAL;
+        NMSettingIPConfig *s_ip             = is_v4 ? s_ip4 : s_ip6;
+        GPtrArray *        data_dns         = is_v4 ? data_dns_v4 : data_dns_v6;
+        GPtrArray *        data_addr        = is_v4 ? data_addr_v4 : data_addr_v6;
+        GPtrArray *        data_dns_search2 = data_dns_search;
+
+        if (data_dns && !data_addr) {
+            /* When specifying "DNS", we also require an "Address" for the same address
+             * family. That is because a NMSettingIPConfig cannot have @method_disabled
+             * and DNS settings at the same time.
+             *
+             * We don't have addresses. Silently ignore the DNS setting. */
+            data_dns         = NULL;
+            data_dns_search2 = NULL;
+        }
+
+        g_object_set(s_ip,
+                     NM_SETTING_IP_CONFIG_METHOD,
+                     data_addr ? method_manual : method_disabled,
+                     NULL);
+
+        /* For WireGuard profiles, always set dns-priority to a negative value,
+         * so that DNS servers on other profiles get ignored. This is also what
+         * wg-quick does, by calling `resolvconf -x`. */
+        g_object_set(s_ip, NM_SETTING_IP_CONFIG_DNS_PRIORITY, (int) -50, NULL);
+
+        if (data_addr) {
+            for (i = 0; i < data_addr->len; i++)
+                nm_setting_ip_config_add_address(s_ip, data_addr->pdata[i]);
+        }
+        if (data_dns) {
+            for (i = 0; i < data_dns->len; i++)
+                nm_setting_ip_config_add_dns(s_ip, data_dns->pdata[i]);
+
+            /* Of the wg-quick doesn't specify a search domain, assume the user
+             * wants to use the domain server for all searches. */
+            if (!data_dns_search2)
+                nm_setting_ip_config_add_dns_search(s_ip, "~");
+        }
+        if (data_dns_search2) {
+            for (i = 0; i < data_dns_search2->len; i++)
+                nm_setting_ip_config_add_dns_search(s_ip, data_dns_search2->pdata[i]);
+        }
+
+        if (data_table == _TABLE_AUTO) {
+            /* in the "auto" setting, wg-quick adds peer-routes automatically to the main
+             * table. NetworkManager will do that too, but there are differences:
+             *
+             * - NetworkManager (contrary to wg-quick) does not check whether the peer-route is necessary.
+             *   It will always add a route for each allowed-ips range, even if there is already another
+             *   route that would ensure packets to the endpoint are routed via the WireGuard interface.
+             *   If you don't want that, disable "wireguard.peer-routes", and add the necessary routes
+             *   yourself to "ipv4.routes" and "ipv6.routes".
+             *
+             * - With "auto", wg-quick also configures policy routing to handle default-routes (/0) to
+             *   avoid routing loops.
+             *   The imported connection profile will have wireguard.ip4-auto-default-route and
+             *   wireguard.ip6-auto-default-route set to "default". It will thus configure wg-quick's
+             *   policy routing if the profile has any AllowedIPs ranges with /0.
+             */
+        } else if (data_table == _TABLE_OFF) {
+            if (is_v4) {
+                g_object_set(s_wg, NM_SETTING_WIREGUARD_PEER_ROUTES, FALSE, NULL);
+            }
+        } else {
+            g_object_set(s_ip, NM_SETTING_IP_CONFIG_ROUTE_TABLE, (guint) data_table, NULL);
+        }
+    }
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &local)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_INVALID_ARGUMENT,
+                           _("Failed to create WireGuard connection: %s"),
+                           local->message);
+        return FALSE;
+    }
+
+    return g_steal_pointer(&connection);
+}
diff --git a/src/libnmc-base/nm-vpn-helpers.h b/src/libnmc-base/nm-vpn-helpers.h
new file mode 100644
index 00000000..1cf06743
--- /dev/null
+++ b/src/libnmc-base/nm-vpn-helpers.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_VPN_HELPERS_H__
+#define __NM_VPN_HELPERS_H__
+
+typedef struct {
+    const char *name;
+    const char *ui_name;
+} NmcVpnPasswordName;
+
+GSList *nm_vpn_get_plugin_infos(void);
+
+NMVpnEditorPlugin *nm_vpn_get_editor_plugin(const char *service_type, GError **error);
+
+gboolean nm_vpn_supports_ipv6(NMConnection *connection);
+
+const NmcVpnPasswordName *nm_vpn_get_secret_names(const char *service_type);
+
+gboolean nm_vpn_openconnect_authenticate_helper(const char *host,
+                                                char **     cookie,
+                                                char **     gateway,
+                                                char **     gwcert,
+                                                int *       status,
+                                                GError **   error);
+
+NMConnection *nm_vpn_wireguard_import(const char *filename, GError **error);
+
+#endif /* __NM_VPN_HELPERS_H__ */
diff --git a/src/libnmc-base/qrcodegen.c b/src/libnmc-base/qrcodegen.c
new file mode 100644
index 00000000..2c40fcb9
--- /dev/null
+++ b/src/libnmc-base/qrcodegen.c
@@ -0,0 +1,1141 @@
+/*
+ * QR Code generator library (C)
+ *
+ * Copyright (c) Project Nayuki. (MIT License)
+ * https://www.nayuki.io/page/qr-code-generator-library
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * - The above copyright notice and this permission notice shall be included in
+ *   all copies or substantial portions of the Software.
+ * - The Software is provided "as is", without warranty of any kind, express or
+ *   implied, including but not limited to the warranties of merchantability,
+ *   fitness for a particular purpose and noninfringement. In no event shall the
+ *   authors or copyright holders be liable for any claim, damages or other
+ *   liability, whether in an action of contract, tort or otherwise, arising from,
+ *   out of or in connection with the Software or the use or other dealings in the
+ *   Software.
+ */
+
+#include <assert.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include "qrcodegen.h"
+
+#ifndef QRCODEGEN_TEST
+    #define testable static  // Keep functions private
+#else
+    #define testable  // Expose private functions
+#endif
+
+/*---- Forward declarations for private functions ----*/
+
+// Regarding all public and private functions defined in this source file:
+// - They require all pointer/array arguments to be not null unless the array length is zero.
+// - They only read input scalar/array arguments, write to output pointer/array
+//   arguments, and return scalar values; they are "pure" functions.
+// - They don't read mutable global variables or write to any global variables.
+// - They don't perform I/O, read the clock, print to console, etc.
+// - They allocate a small and constant amount of stack memory.
+// - They don't allocate or free any memory on the heap.
+// - They don't recurse or mutually recurse. All the code
+//   could be inlined into the top-level public functions.
+// - They run in at most quadratic time with respect to input arguments.
+//   Most functions run in linear time, and some in constant time.
+//   There are no unbounded loops or non-obvious termination conditions.
+// - They are completely thread-safe if the caller does not give the
+//   same writable buffer to concurrent calls to these functions.
+
+testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen);
+
+testable void
+addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]);
+testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl);
+testable int getNumRawDataModules(int ver);
+
+testable void    calcReedSolomonGenerator(int degree, uint8_t result[]);
+testable void    calcReedSolomonRemainder(const uint8_t data[],
+                                          int           dataLen,
+                                          const uint8_t generator[],
+                                          int           degree,
+                                          uint8_t       result[]);
+testable uint8_t finiteFieldMultiply(uint8_t x, uint8_t y);
+
+testable void initializeFunctionModules(int version, uint8_t qrcode[]);
+static void   drawWhiteFunctionModules(uint8_t qrcode[], int version);
+static void   drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]);
+testable int  getAlignmentPatternPositions(int version, uint8_t result[7]);
+static void   fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]);
+
+static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]);
+static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask);
+static long getPenaltyScore(const uint8_t qrcode[]);
+static void addRunToHistory(unsigned char run, unsigned char history[7]);
+static bool hasFinderLikePattern(unsigned char runHistory[7]);
+
+testable bool getModule(const uint8_t qrcode[], int x, int y);
+testable void setModule(uint8_t qrcode[], int x, int y, bool isBlack);
+testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isBlack);
+static bool   getBit(int x, int i);
+
+testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars);
+testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version);
+static int   numCharCountBits(enum qrcodegen_Mode mode, int version);
+
+/*---- Private tables of constants ----*/
+
+// The set of all legal characters in alphanumeric mode, where each character
+// value maps to the index in the string. For checking text and encoding segments.
+static const char *ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
+
+// For generating error correction codes.
+testable const int8_t ECC_CODEWORDS_PER_BLOCK[4][41] = {
+    // Version: (note that index 0 is for padding, and is set to an illegal value)
+    //0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40    Error correction level
+    {-1, 7,  10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28,
+     28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30},  // Low
+    {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26,
+     26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28},  // Medium
+    {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30,
+     28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30},  // Quartile
+    {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28,
+     30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30},  // High
+};
+
+#define qrcodegen_REED_SOLOMON_DEGREE_MAX 30  // Based on the table above
+
+// For generating error correction codes.
+testable const int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41] = {
+    // Version: (note that index 0 is for padding, and is set to an illegal value)
+    //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40    Error correction level
+    {-1, 1, 1, 1,  1,  1,  2,  2,  2,  2,  4,  4,  4,  4,  4,  6,  6,  6,  6,  7, 8,
+     8,  9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25},  // Low
+    {-1, 1,  1,  1,  2,  2,  4,  4,  4,  5,  5,  5,  8,  9,  9,  10, 10, 11, 13, 14, 16,
+     17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49},  // Medium
+    {-1, 1,  1,  2,  2,  4,  4,  6,  6,  8,  8,  8,  10, 12, 16, 12, 17, 16, 18, 21, 20,
+     23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68},  // Quartile
+    {-1, 1,  1,  2,  4,  4,  4,  5,  6,  8,  8,  11, 11, 16, 16, 18, 16, 19, 21, 25, 25,
+     25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81},  // High
+};
+
+// For automatic mask pattern selection.
+static const int PENALTY_N1 = 3;
+static const int PENALTY_N2 = 3;
+static const int PENALTY_N3 = 40;
+static const int PENALTY_N4 = 10;
+
+/*---- High-level QR Code encoding functions ----*/
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_encodeText(const char *        text,
+                     uint8_t             tempBuffer[],
+                     uint8_t             qrcode[],
+                     enum qrcodegen_Ecc  ecl,
+                     int                 minVersion,
+                     int                 maxVersion,
+                     enum qrcodegen_Mask mask,
+                     bool                boostEcl)
+{
+    size_t textLen = strlen(text);
+    if (textLen == 0)
+        return qrcodegen_encodeSegmentsAdvanced(NULL,
+                                                0,
+                                                ecl,
+                                                minVersion,
+                                                maxVersion,
+                                                mask,
+                                                boostEcl,
+                                                tempBuffer,
+                                                qrcode);
+    size_t bufLen = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion);
+
+    struct qrcodegen_Segment seg;
+    if (qrcodegen_isNumeric(text)) {
+        if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen)
+            goto fail;
+        seg = qrcodegen_makeNumeric(text, tempBuffer);
+    } else if (qrcodegen_isAlphanumeric(text)) {
+        if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_ALPHANUMERIC, textLen) > bufLen)
+            goto fail;
+        seg = qrcodegen_makeAlphanumeric(text, tempBuffer);
+    } else {
+        if (textLen > bufLen)
+            goto fail;
+        for (size_t i = 0; i < textLen; i++)
+            tempBuffer[i] = (uint8_t) text[i];
+        seg.mode      = qrcodegen_Mode_BYTE;
+        seg.bitLength = calcSegmentBitLength(seg.mode, textLen);
+        if (seg.bitLength == -1)
+            goto fail;
+        seg.numChars = (int) textLen;
+        seg.data     = tempBuffer;
+    }
+    return qrcodegen_encodeSegmentsAdvanced(&seg,
+                                            1,
+                                            ecl,
+                                            minVersion,
+                                            maxVersion,
+                                            mask,
+                                            boostEcl,
+                                            tempBuffer,
+                                            qrcode);
+
+fail:
+    qrcode[0] = 0;  // Set size to invalid value for safety
+    return false;
+}
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_encodeBinary(uint8_t             dataAndTemp[],
+                       size_t              dataLen,
+                       uint8_t             qrcode[],
+                       enum qrcodegen_Ecc  ecl,
+                       int                 minVersion,
+                       int                 maxVersion,
+                       enum qrcodegen_Mask mask,
+                       bool                boostEcl)
+{
+    struct qrcodegen_Segment seg;
+    seg.mode      = qrcodegen_Mode_BYTE;
+    seg.bitLength = calcSegmentBitLength(seg.mode, dataLen);
+    if (seg.bitLength == -1) {
+        qrcode[0] = 0;  // Set size to invalid value for safety
+        return false;
+    }
+    seg.numChars = (int) dataLen;
+    seg.data     = dataAndTemp;
+    return qrcodegen_encodeSegmentsAdvanced(&seg,
+                                            1,
+                                            ecl,
+                                            minVersion,
+                                            maxVersion,
+                                            mask,
+                                            boostEcl,
+                                            dataAndTemp,
+                                            qrcode);
+}
+
+// Appends the given number of low-order bits of the given value to the given byte-based
+// bit buffer, increasing the bit length. Requires 0 <= numBits <= 16 and val < 2^numBits.
+testable void
+appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen)
+{
+    assert(0 <= numBits && numBits <= 16 && (unsigned long) val >> numBits == 0);
+    for (int i = numBits - 1; i >= 0; i--, (*bitLen)++)
+        buffer[*bitLen >> 3] |= ((val >> i) & 1) << (7 - (*bitLen & 7));
+}
+
+/*---- Low-level QR Code encoding functions ----*/
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[],
+                         size_t                         len,
+                         enum qrcodegen_Ecc             ecl,
+                         uint8_t                        tempBuffer[],
+                         uint8_t                        qrcode[])
+{
+    return qrcodegen_encodeSegmentsAdvanced(segs,
+                                            len,
+                                            ecl,
+                                            qrcodegen_VERSION_MIN,
+                                            qrcodegen_VERSION_MAX,
+                                            -1,
+                                            true,
+                                            tempBuffer,
+                                            qrcode);
+}
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[],
+                                 size_t                         len,
+                                 enum qrcodegen_Ecc             ecl,
+                                 int                            minVersion,
+                                 int                            maxVersion,
+                                 int                            mask,
+                                 bool                           boostEcl,
+                                 uint8_t                        tempBuffer[],
+                                 uint8_t                        qrcode[])
+{
+    assert(segs != NULL || len == 0);
+    assert(qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion
+           && maxVersion <= qrcodegen_VERSION_MAX);
+    assert(0 <= (int) ecl && (int) ecl <= 3 && -1 <= (int) mask && (int) mask <= 7);
+
+    // Find the minimal version number to use
+    int version, dataUsedBits;
+    for (version = minVersion;; version++) {
+        int dataCapacityBits =
+            getNumDataCodewords(version, ecl) * 8;  // Number of data bits available
+        dataUsedBits = getTotalBits(segs, len, version);
+        if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits)
+            break;                    // This version number is found to be suitable
+        if (version >= maxVersion) {  // All versions in the range could not fit the given data
+            qrcode[0] = 0;            // Set size to invalid value for safety
+            return false;
+        }
+    }
+    assert(dataUsedBits != -1);
+
+    // Increase the error correction level while the data still fits in the current version number
+    for (int i = (int) qrcodegen_Ecc_MEDIUM; i <= (int) qrcodegen_Ecc_HIGH;
+         i++) {  // From low to high
+        if (boostEcl && dataUsedBits <= getNumDataCodewords(version, (enum qrcodegen_Ecc) i) * 8)
+            ecl = (enum qrcodegen_Ecc) i;
+    }
+
+    // Concatenate all segments to create the data bit string
+    memset(qrcode, 0, qrcodegen_BUFFER_LEN_FOR_VERSION(version) * sizeof(qrcode[0]));
+    int bitLen = 0;
+    for (size_t i = 0; i < len; i++) {
+        const struct qrcodegen_Segment *seg = &segs[i];
+        appendBitsToBuffer((int) seg->mode, 4, qrcode, &bitLen);
+        appendBitsToBuffer(seg->numChars, numCharCountBits(seg->mode, version), qrcode, &bitLen);
+        for (int j = 0; j < seg->bitLength; j++)
+            appendBitsToBuffer((seg->data[j >> 3] >> (7 - (j & 7))) & 1, 1, qrcode, &bitLen);
+    }
+    assert(bitLen == dataUsedBits);
+
+    // Add terminator and pad up to a byte if applicable
+    int dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
+    assert(bitLen <= dataCapacityBits);
+    int terminatorBits = dataCapacityBits - bitLen;
+    if (terminatorBits > 4)
+        terminatorBits = 4;
+    appendBitsToBuffer(0, terminatorBits, qrcode, &bitLen);
+    appendBitsToBuffer(0, (8 - bitLen % 8) % 8, qrcode, &bitLen);
+    assert(bitLen % 8 == 0);
+
+    // Pad with alternating bytes until data capacity is reached
+    for (uint8_t padByte = 0xEC; bitLen < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
+        appendBitsToBuffer(padByte, 8, qrcode, &bitLen);
+
+    // Draw function and data codeword modules
+    addEccAndInterleave(qrcode, version, ecl, tempBuffer);
+    initializeFunctionModules(version, qrcode);
+    drawCodewords(tempBuffer, getNumRawDataModules(version) / 8, qrcode);
+    drawWhiteFunctionModules(qrcode, version);
+    initializeFunctionModules(version, tempBuffer);
+
+    // Handle masking
+    if (mask == qrcodegen_Mask_AUTO) {  // Automatically choose best mask
+        long minPenalty = LONG_MAX;
+        for (int i = 0; i < 8; i++) {
+            enum qrcodegen_Mask msk = (enum qrcodegen_Mask) i;
+            drawFormatBits(ecl, msk, qrcode);
+            applyMask(tempBuffer, qrcode, msk);
+            long penalty = getPenaltyScore(qrcode);
+            if (penalty < minPenalty) {
+                mask       = msk;
+                minPenalty = penalty;
+            }
+            applyMask(tempBuffer, qrcode, msk);  // Undoes the mask due to XOR
+        }
+    }
+    assert(0 <= (int) mask && (int) mask <= 7);
+    drawFormatBits(ecl, mask, qrcode);
+    applyMask(tempBuffer, qrcode, mask);
+    return true;
+}
+
+/*---- Error correction code generation functions ----*/
+
+// Appends error correction bytes to each block of the given data array, then interleaves
+// bytes from the blocks and stores them in the result array. data[0 : dataLen] contains
+// the input data. data[dataLen : rawCodewords] is used as a temporary work area and will
+// be clobbered by this function. The final answer is stored in result[0 : rawCodewords].
+testable void
+addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[])
+{
+    // Calculate parameter numbers
+    assert(0 <= (int) ecl && (int) ecl < 4 && qrcodegen_VERSION_MIN <= version
+           && version <= qrcodegen_VERSION_MAX);
+    int numBlocks         = NUM_ERROR_CORRECTION_BLOCKS[(int) ecl][version];
+    int blockEccLen       = ECC_CODEWORDS_PER_BLOCK[(int) ecl][version];
+    int rawCodewords      = getNumRawDataModules(version) / 8;
+    int dataLen           = getNumDataCodewords(version, ecl);
+    int numShortBlocks    = numBlocks - rawCodewords % numBlocks;
+    int shortBlockDataLen = rawCodewords / numBlocks - blockEccLen;
+
+    // Split data into blocks, calculate ECC, and interleave
+    // (not concatenate) the bytes into a single sequence
+    uint8_t generator[qrcodegen_REED_SOLOMON_DEGREE_MAX];
+    calcReedSolomonGenerator(blockEccLen, generator);
+    const uint8_t *dat = data;
+    for (int i = 0; i < numBlocks; i++) {
+        int      datLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1);
+        uint8_t *ecc    = &data[dataLen];  // Temporary storage
+        calcReedSolomonRemainder(dat, datLen, generator, blockEccLen, ecc);
+        for (int j = 0, k = i; j < datLen; j++, k += numBlocks) {  // Copy data
+            if (j == shortBlockDataLen)
+                k -= numShortBlocks;
+            result[k] = dat[j];
+        }
+        for (int j = 0, k = dataLen + i; j < blockEccLen; j++, k += numBlocks)  // Copy ECC
+            result[k] = ecc[j];
+        dat += datLen;
+    }
+}
+
+// Returns the number of 8-bit codewords that can be used for storing data (not ECC),
+// for the given version number and error correction level. The result is in the range [9, 2956].
+testable int
+getNumDataCodewords(int version, enum qrcodegen_Ecc ecl)
+{
+    int v = version, e = (int) ecl;
+    assert(0 <= e && e < 4);
+    return getNumRawDataModules(v) / 8
+           - ECC_CODEWORDS_PER_BLOCK[e][v] * NUM_ERROR_CORRECTION_BLOCKS[e][v];
+}
+
+// Returns the number of data bits that can be stored in a QR Code of the given version number, after
+// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
+// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
+testable int
+getNumRawDataModules(int ver)
+{
+    assert(qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX);
+    int result = (16 * ver + 128) * ver + 64;
+    if (ver >= 2) {
+        int numAlign = ver / 7 + 2;
+        result -= (25 * numAlign - 10) * numAlign - 55;
+        if (ver >= 7)
+            result -= 36;
+    }
+    return result;
+}
+
+/*---- Reed-Solomon ECC generator functions ----*/
+
+// Calculates the Reed-Solomon generator polynomial of the given degree, storing in result[0 : degree].
+testable void
+calcReedSolomonGenerator(int degree, uint8_t result[])
+{
+    // Start with the monomial x^0
+    assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX);
+    memset(result, 0, degree * sizeof(result[0]));
+    result[degree - 1] = 1;
+
+    // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
+    // drop the highest term, and store the rest of the coefficients in order of descending powers.
+    // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
+    uint8_t root = 1;
+    for (int i = 0; i < degree; i++) {
+        // Multiply the current product by (x - r^i)
+        for (int j = 0; j < degree; j++) {
+            result[j] = finiteFieldMultiply(result[j], root);
+            if (j + 1 < degree)
+                result[j] ^= result[j + 1];
+        }
+        root = finiteFieldMultiply(root, 0x02);
+    }
+}
+
+// Calculates the remainder of the polynomial data[0 : dataLen] when divided by the generator[0 : degree], where all
+// polynomials are in big endian and the generator has an implicit leading 1 term, storing the result in result[0 : degree].
+testable void
+calcReedSolomonRemainder(const uint8_t data[],
+                         int           dataLen,
+                         const uint8_t generator[],
+                         int           degree,
+                         uint8_t       result[])
+{
+    // Perform polynomial division
+    assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX);
+    memset(result, 0, degree * sizeof(result[0]));
+    for (int i = 0; i < dataLen; i++) {
+        uint8_t factor = data[i] ^ result[0];
+        memmove(&result[0], &result[1], (degree - 1) * sizeof(result[0]));
+        result[degree - 1] = 0;
+        for (int j = 0; j < degree; j++)
+            result[j] ^= finiteFieldMultiply(generator[j], factor);
+    }
+}
+
+#undef qrcodegen_REED_SOLOMON_DEGREE_MAX
+
+// Returns the product of the two given field elements modulo GF(2^8/0x11D).
+// All inputs are valid. This could be implemented as a 256*256 lookup table.
+testable uint8_t
+finiteFieldMultiply(uint8_t x, uint8_t y)
+{
+    // Russian peasant multiplication
+    uint8_t z = 0;
+    for (int i = 7; i >= 0; i--) {
+        z = (z << 1) ^ ((z >> 7) * 0x11D);
+        z ^= ((y >> i) & 1) * x;
+    }
+    return z;
+}
+
+/*---- Drawing function modules ----*/
+
+// Clears the given QR Code grid with white modules for the given
+// version's size, then marks every function module as black.
+testable void
+initializeFunctionModules(int version, uint8_t qrcode[])
+{
+    // Initialize QR Code
+    int qrsize = version * 4 + 17;
+    memset(qrcode, 0, ((qrsize * qrsize + 7) / 8 + 1) * sizeof(qrcode[0]));
+    qrcode[0] = (uint8_t) qrsize;
+
+    // Fill horizontal and vertical timing patterns
+    fillRectangle(6, 0, 1, qrsize, qrcode);
+    fillRectangle(0, 6, qrsize, 1, qrcode);
+
+    // Fill 3 finder patterns (all corners except bottom right) and format bits
+    fillRectangle(0, 0, 9, 9, qrcode);
+    fillRectangle(qrsize - 8, 0, 8, 9, qrcode);
+    fillRectangle(0, qrsize - 8, 9, 8, qrcode);
+
+    // Fill numerous alignment patterns
+    uint8_t alignPatPos[7];
+    int     numAlign = getAlignmentPatternPositions(version, alignPatPos);
+    for (int i = 0; i < numAlign; i++) {
+        for (int j = 0; j < numAlign; j++) {
+            // Don't draw on the three finder corners
+            if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1)
+                  || (i == numAlign - 1 && j == 0)))
+                fillRectangle(alignPatPos[i] - 2, alignPatPos[j] - 2, 5, 5, qrcode);
+        }
+    }
+
+    // Fill version blocks
+    if (version >= 7) {
+        fillRectangle(qrsize - 11, 0, 3, 6, qrcode);
+        fillRectangle(0, qrsize - 11, 6, 3, qrcode);
+    }
+}
+
+// Draws white function modules and possibly some black modules onto the given QR Code, without changing
+// non-function modules. This does not draw the format bits. This requires all function modules to be previously
+// marked black (namely by initializeFunctionModules()), because this may skip redrawing black function modules.
+static void
+drawWhiteFunctionModules(uint8_t qrcode[], int version)
+{
+    // Draw horizontal and vertical timing patterns
+    int qrsize = qrcodegen_getSize(qrcode);
+    for (int i = 7; i < qrsize - 7; i += 2) {
+        setModule(qrcode, 6, i, false);
+        setModule(qrcode, i, 6, false);
+    }
+
+    // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
+    for (int dy = -4; dy <= 4; dy++) {
+        for (int dx = -4; dx <= 4; dx++) {
+            int dist = abs(dx);
+            if (abs(dy) > dist)
+                dist = abs(dy);
+            if (dist == 2 || dist == 4) {
+                setModuleBounded(qrcode, 3 + dx, 3 + dy, false);
+                setModuleBounded(qrcode, qrsize - 4 + dx, 3 + dy, false);
+                setModuleBounded(qrcode, 3 + dx, qrsize - 4 + dy, false);
+            }
+        }
+    }
+
+    // Draw numerous alignment patterns
+    uint8_t alignPatPos[7];
+    int     numAlign = getAlignmentPatternPositions(version, alignPatPos);
+    for (int i = 0; i < numAlign; i++) {
+        for (int j = 0; j < numAlign; j++) {
+            if ((i == 0 && j == 0) || (i == 0 && j == numAlign - 1)
+                || (i == numAlign - 1 && j == 0))
+                continue;  // Don't draw on the three finder corners
+            for (int dy = -1; dy <= 1; dy++) {
+                for (int dx = -1; dx <= 1; dx++)
+                    setModule(qrcode, alignPatPos[i] + dx, alignPatPos[j] + dy, dx == 0 && dy == 0);
+            }
+        }
+    }
+
+    // Draw version blocks
+    if (version >= 7) {
+        // Calculate error correction code and pack bits
+        int rem = version;  // version is uint6, in the range [7, 40]
+        for (int i = 0; i < 12; i++)
+            rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
+        long bits = (long) version << 12 | rem;  // uint18
+        assert(bits >> 18 == 0);
+
+        // Draw two copies
+        for (int i = 0; i < 6; i++) {
+            for (int j = 0; j < 3; j++) {
+                int k = qrsize - 11 + j;
+                setModule(qrcode, k, i, (bits & 1) != 0);
+                setModule(qrcode, i, k, (bits & 1) != 0);
+                bits >>= 1;
+            }
+        }
+    }
+}
+
+// Draws two copies of the format bits (with its own error correction code) based
+// on the given mask and error correction level. This always draws all modules of
+// the format bits, unlike drawWhiteFunctionModules() which might skip black modules.
+static void
+drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[])
+{
+    // Calculate error correction code and pack bits
+    assert(0 <= (int) mask && (int) mask <= 7);
+    static const int table[] = {1, 0, 3, 2};
+    int data = table[(int) ecl] << 3 | (int) mask;  // errCorrLvl is uint2, mask is uint3
+    int rem  = data;
+    for (int i = 0; i < 10; i++)
+        rem = (rem << 1) ^ ((rem >> 9) * 0x537);
+    int bits = (data << 10 | rem) ^ 0x5412;  // uint15
+    assert(bits >> 15 == 0);
+
+    // Draw first copy
+    for (int i = 0; i <= 5; i++)
+        setModule(qrcode, 8, i, getBit(bits, i));
+    setModule(qrcode, 8, 7, getBit(bits, 6));
+    setModule(qrcode, 8, 8, getBit(bits, 7));
+    setModule(qrcode, 7, 8, getBit(bits, 8));
+    for (int i = 9; i < 15; i++)
+        setModule(qrcode, 14 - i, 8, getBit(bits, i));
+
+    // Draw second copy
+    int qrsize = qrcodegen_getSize(qrcode);
+    for (int i = 0; i < 8; i++)
+        setModule(qrcode, qrsize - 1 - i, 8, getBit(bits, i));
+    for (int i = 8; i < 15; i++)
+        setModule(qrcode, 8, qrsize - 15 + i, getBit(bits, i));
+    setModule(qrcode, 8, qrsize - 8, true);  // Always black
+}
+
+// Calculates and stores an ascending list of positions of alignment patterns
+// for this version number, returning the length of the list (in the range [0,7]).
+// Each position is in the range [0,177), and are used on both the x and y axes.
+// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes.
+testable int
+getAlignmentPatternPositions(int version, uint8_t result[7])
+{
+    if (version == 1)
+        return 0;
+    int numAlign = version / 7 + 2;
+    int step     = (version == 32) ? 26 : (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2;
+    for (int i = numAlign - 1, pos = version * 4 + 10; i >= 1; i--, pos -= step)
+        result[i] = pos;
+    result[0] = 6;
+    return numAlign;
+}
+
+// Sets every pixel in the range [left : left + width] * [top : top + height] to black.
+static void
+fillRectangle(int left, int top, int width, int height, uint8_t qrcode[])
+{
+    for (int dy = 0; dy < height; dy++) {
+        for (int dx = 0; dx < width; dx++)
+            setModule(qrcode, left + dx, top + dy, true);
+    }
+}
+
+/*---- Drawing data modules and masking ----*/
+
+// Draws the raw codewords (including data and ECC) onto the given QR Code. This requires the initial state of
+// the QR Code to be black at function modules and white at codeword modules (including unused remainder bits).
+static void
+drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[])
+{
+    int qrsize = qrcodegen_getSize(qrcode);
+    int i      = 0;  // Bit index into the data
+    // Do the funny zigzag scan
+    for (int right = qrsize - 1; right >= 1;
+         right -= 2) {  // Index of right column in each column pair
+        if (right == 6)
+            right = 5;
+        for (int vert = 0; vert < qrsize; vert++) {  // Vertical counter
+            for (int j = 0; j < 2; j++) {
+                int  x      = right - j;  // Actual x coordinate
+                bool upward = ((right + 1) & 2) == 0;
+                int  y      = upward ? qrsize - 1 - vert : vert;  // Actual y coordinate
+                if (!getModule(qrcode, x, y) && i < dataLen * 8) {
+                    bool black = getBit(data[i >> 3], 7 - (i & 7));
+                    setModule(qrcode, x, y, black);
+                    i++;
+                }
+                // If this QR Code has any remainder bits (0 to 7), they were assigned as
+                // 0/false/white by the constructor and are left unchanged by this method
+            }
+        }
+    }
+    assert(i == dataLen * 8);
+}
+
+// XORs the codeword modules in this QR Code with the given mask pattern.
+// The function modules must be marked and the codeword bits must be drawn
+// before masking. Due to the arithmetic of XOR, calling applyMask() with
+// the same mask value a second time will undo the mask. A final well-formed
+// QR Code needs exactly one (not zero, two, etc.) mask applied.
+static void
+applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask)
+{
+    assert(0 <= (int) mask && (int) mask <= 7);  // Disallows qrcodegen_Mask_AUTO
+    int qrsize = qrcodegen_getSize(qrcode);
+    for (int y = 0; y < qrsize; y++) {
+        for (int x = 0; x < qrsize; x++) {
+            if (getModule(functionModules, x, y))
+                continue;
+            bool invert;
+            switch ((int) mask) {
+            case 0:
+                invert = (x + y) % 2 == 0;
+                break;
+            case 1:
+                invert = y % 2 == 0;
+                break;
+            case 2:
+                invert = x % 3 == 0;
+                break;
+            case 3:
+                invert = (x + y) % 3 == 0;
+                break;
+            case 4:
+                invert = (x / 3 + y / 2) % 2 == 0;
+                break;
+            case 5:
+                invert = x * y % 2 + x * y % 3 == 0;
+                break;
+            case 6:
+                invert = (x * y % 2 + x * y % 3) % 2 == 0;
+                break;
+            case 7:
+                invert = ((x + y) % 2 + x * y % 3) % 2 == 0;
+                break;
+            default:
+                assert(false);
+                return;
+            }
+            bool val = getModule(qrcode, x, y);
+            setModule(qrcode, x, y, val ^ invert);
+        }
+    }
+}
+
+// Calculates and returns the penalty score based on state of the given QR Code's current modules.
+// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
+static long
+getPenaltyScore(const uint8_t qrcode[])
+{
+    int  qrsize = qrcodegen_getSize(qrcode);
+    long result = 0;
+
+    // Adjacent modules in row having same color, and finder-like patterns
+    for (int y = 0; y < qrsize; y++) {
+        unsigned char runHistory[7] = {0};
+        bool          color         = false;
+        unsigned char runX          = 0;
+        for (int x = 0; x < qrsize; x++) {
+            if (getModule(qrcode, x, y) == color) {
+                runX++;
+                if (runX == 5)
+                    result += PENALTY_N1;
+                else if (runX > 5)
+                    result++;
+            } else {
+                addRunToHistory(runX, runHistory);
+                if (!color && hasFinderLikePattern(runHistory))
+                    result += PENALTY_N3;
+                color = getModule(qrcode, x, y);
+                runX  = 1;
+            }
+        }
+        addRunToHistory(runX, runHistory);
+        if (color)
+            addRunToHistory(0, runHistory);  // Dummy run of white
+        if (hasFinderLikePattern(runHistory))
+            result += PENALTY_N3;
+    }
+    // Adjacent modules in column having same color, and finder-like patterns
+    for (int x = 0; x < qrsize; x++) {
+        unsigned char runHistory[7] = {0};
+        bool          color         = false;
+        unsigned char runY          = 0;
+        for (int y = 0; y < qrsize; y++) {
+            if (getModule(qrcode, x, y) == color) {
+                runY++;
+                if (runY == 5)
+                    result += PENALTY_N1;
+                else if (runY > 5)
+                    result++;
+            } else {
+                addRunToHistory(runY, runHistory);
+                if (!color && hasFinderLikePattern(runHistory))
+                    result += PENALTY_N3;
+                color = getModule(qrcode, x, y);
+                runY  = 1;
+            }
+        }
+        addRunToHistory(runY, runHistory);
+        if (color)
+            addRunToHistory(0, runHistory);  // Dummy run of white
+        if (hasFinderLikePattern(runHistory))
+            result += PENALTY_N3;
+    }
+
+    // 2*2 blocks of modules having same color
+    for (int y = 0; y < qrsize - 1; y++) {
+        for (int x = 0; x < qrsize - 1; x++) {
+            bool color = getModule(qrcode, x, y);
+            if (color == getModule(qrcode, x + 1, y) && color == getModule(qrcode, x, y + 1)
+                && color == getModule(qrcode, x + 1, y + 1))
+                result += PENALTY_N2;
+        }
+    }
+
+    // Balance of black and white modules
+    int black = 0;
+    for (int y = 0; y < qrsize; y++) {
+        for (int x = 0; x < qrsize; x++) {
+            if (getModule(qrcode, x, y))
+                black++;
+        }
+    }
+    int total = qrsize * qrsize;  // Note that size is odd, so black/total != 1/2
+    // Compute the smallest integer k >= 0 such that (45-5k)% <= black/total <= (55+5k)%
+    int k = (int) ((labs(black * 20L - total * 10L) + total - 1) / total) - 1;
+    result += k * PENALTY_N4;
+    return result;
+}
+
+// Inserts the given value to the front of the given array, which shifts over the
+// existing values and deletes the last value. A helper function for getPenaltyScore().
+static void
+addRunToHistory(unsigned char run, unsigned char history[7])
+{
+    memmove(&history[1], &history[0], 6 * sizeof(history[0]));
+    history[0] = run;
+}
+
+// Tests whether the given run history has the pattern of ratio 1:1:3:1:1 in the middle, and
+// surrounded by at least 4 on either or both ends. A helper function for getPenaltyScore().
+// Must only be called immediately after a run of white modules has ended.
+static bool
+hasFinderLikePattern(unsigned char runHistory[7])
+{
+    unsigned char n = runHistory[1];
+    // The maximum QR Code size is 177, hence the run length n <= 177.
+    // Arithmetic is promoted to int, so n*4 will not overflow.
+    return n > 0 && runHistory[2] == n && runHistory[4] == n && runHistory[5] == n
+           && runHistory[3] == n * 3 && (runHistory[0] >= n * 4 || runHistory[6] >= n * 4);
+}
+
+/*---- Basic QR Code information ----*/
+
+// Public function - see documentation comment in header file.
+int
+qrcodegen_getSize(const uint8_t qrcode[])
+{
+    assert(qrcode != NULL);
+    int result = qrcode[0];
+    assert((qrcodegen_VERSION_MIN * 4 + 17) <= result
+           && result <= (qrcodegen_VERSION_MAX * 4 + 17));
+    return result;
+}
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_getModule(const uint8_t qrcode[], int x, int y)
+{
+    assert(qrcode != NULL);
+    int qrsize = qrcode[0];
+    return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && getModule(qrcode, x, y);
+}
+
+// Gets the module at the given coordinates, which must be in bounds.
+testable bool
+getModule(const uint8_t qrcode[], int x, int y)
+{
+    int qrsize = qrcode[0];
+    assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize);
+    int index = y * qrsize + x;
+    return getBit(qrcode[(index >> 3) + 1], index & 7);
+}
+
+// Sets the module at the given coordinates, which must be in bounds.
+testable void
+setModule(uint8_t qrcode[], int x, int y, bool isBlack)
+{
+    int qrsize = qrcode[0];
+    assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize);
+    int index     = y * qrsize + x;
+    int bitIndex  = index & 7;
+    int byteIndex = (index >> 3) + 1;
+    if (isBlack)
+        qrcode[byteIndex] |= 1 << bitIndex;
+    else
+        qrcode[byteIndex] &= (1 << bitIndex) ^ 0xFF;
+}
+
+// Sets the module at the given coordinates, doing nothing if out of bounds.
+testable void
+setModuleBounded(uint8_t qrcode[], int x, int y, bool isBlack)
+{
+    int qrsize = qrcode[0];
+    if (0 <= x && x < qrsize && 0 <= y && y < qrsize)
+        setModule(qrcode, x, y, isBlack);
+}
+
+// Returns true iff the i'th bit of x is set to 1. Requires x >= 0 and 0 <= i <= 14.
+static bool
+getBit(int x, int i)
+{
+    return ((x >> i) & 1) != 0;
+}
+
+/*---- Segment handling ----*/
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_isAlphanumeric(const char *text)
+{
+    assert(text != NULL);
+    for (; *text != '\0'; text++) {
+        if (strchr(ALPHANUMERIC_CHARSET, *text) == NULL)
+            return false;
+    }
+    return true;
+}
+
+// Public function - see documentation comment in header file.
+bool
+qrcodegen_isNumeric(const char *text)
+{
+    assert(text != NULL);
+    for (; *text != '\0'; text++) {
+        if (*text < '0' || *text > '9')
+            return false;
+    }
+    return true;
+}
+
+// Public function - see documentation comment in header file.
+size_t
+qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars)
+{
+    int temp = calcSegmentBitLength(mode, numChars);
+    if (temp == -1)
+        return SIZE_MAX;
+    assert(0 <= temp && temp <= INT16_MAX);
+    return ((size_t) temp + 7) / 8;
+}
+
+// Returns the number of data bits needed to represent a segment
+// containing the given number of characters using the given mode. Notes:
+// - Returns -1 on failure, i.e. numChars > INT16_MAX or
+//   the number of needed bits exceeds INT16_MAX (i.e. 32767).
+// - Otherwise, all valid results are in the range [0, INT16_MAX].
+// - For byte mode, numChars measures the number of bytes, not Unicode code points.
+// - For ECI mode, numChars must be 0, and the worst-case number of bits is returned.
+//   An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
+testable int
+calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars)
+{
+    // All calculations are designed to avoid overflow on all platforms
+    if (numChars > (unsigned int) INT16_MAX)
+        return -1;
+    long result = (long) numChars;
+    if (mode == qrcodegen_Mode_NUMERIC)
+        result = (result * 10 + 2) / 3;  // ceil(10/3 * n)
+    else if (mode == qrcodegen_Mode_ALPHANUMERIC)
+        result = (result * 11 + 1) / 2;  // ceil(11/2 * n)
+    else if (mode == qrcodegen_Mode_BYTE)
+        result *= 8;
+    else if (mode == qrcodegen_Mode_KANJI)
+        result *= 13;
+    else if (mode == qrcodegen_Mode_ECI && numChars == 0)
+        result = 3 * 8;
+    else {  // Invalid argument
+        assert(false);
+        return -1;
+    }
+    assert(result >= 0);
+    if (result > (unsigned int) INT16_MAX)
+        return -1;
+    return (int) result;
+}
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment
+qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[])
+{
+    assert(data != NULL || len == 0);
+    struct qrcodegen_Segment result;
+    result.mode      = qrcodegen_Mode_BYTE;
+    result.bitLength = calcSegmentBitLength(result.mode, len);
+    assert(result.bitLength != -1);
+    result.numChars = (int) len;
+    if (len > 0)
+        memcpy(buf, data, len * sizeof(buf[0]));
+    result.data = buf;
+    return result;
+}
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment
+qrcodegen_makeNumeric(const char *digits, uint8_t buf[])
+{
+    assert(digits != NULL);
+    struct qrcodegen_Segment result;
+    size_t                   len = strlen(digits);
+    result.mode                  = qrcodegen_Mode_NUMERIC;
+    int bitLen                   = calcSegmentBitLength(result.mode, len);
+    assert(bitLen != -1);
+    result.numChars = (int) len;
+    if (bitLen > 0)
+        memset(buf, 0, ((size_t) bitLen + 7) / 8 * sizeof(buf[0]));
+    result.bitLength = 0;
+
+    unsigned int accumData  = 0;
+    int          accumCount = 0;
+    for (; *digits != '\0'; digits++) {
+        char c = *digits;
+        assert('0' <= c && c <= '9');
+        accumData = accumData * 10 + (unsigned int) (c - '0');
+        accumCount++;
+        if (accumCount == 3) {
+            appendBitsToBuffer(accumData, 10, buf, &result.bitLength);
+            accumData  = 0;
+            accumCount = 0;
+        }
+    }
+    if (accumCount > 0)  // 1 or 2 digits remaining
+        appendBitsToBuffer(accumData, accumCount * 3 + 1, buf, &result.bitLength);
+    assert(result.bitLength == bitLen);
+    result.data = buf;
+    return result;
+}
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment
+qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[])
+{
+    assert(text != NULL);
+    struct qrcodegen_Segment result;
+    size_t                   len = strlen(text);
+    result.mode                  = qrcodegen_Mode_ALPHANUMERIC;
+    int bitLen                   = calcSegmentBitLength(result.mode, len);
+    assert(bitLen != -1);
+    result.numChars = (int) len;
+    if (bitLen > 0)
+        memset(buf, 0, ((size_t) bitLen + 7) / 8 * sizeof(buf[0]));
+    result.bitLength = 0;
+
+    unsigned int accumData  = 0;
+    int          accumCount = 0;
+    for (; *text != '\0'; text++) {
+        const char *temp = strchr(ALPHANUMERIC_CHARSET, *text);
+        assert(temp != NULL);
+        accumData = accumData * 45 + (unsigned int) (temp - ALPHANUMERIC_CHARSET);
+        accumCount++;
+        if (accumCount == 2) {
+            appendBitsToBuffer(accumData, 11, buf, &result.bitLength);
+            accumData  = 0;
+            accumCount = 0;
+        }
+    }
+    if (accumCount > 0)  // 1 character remaining
+        appendBitsToBuffer(accumData, 6, buf, &result.bitLength);
+    assert(result.bitLength == bitLen);
+    result.data = buf;
+    return result;
+}
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment
+qrcodegen_makeEci(long assignVal, uint8_t buf[])
+{
+    struct qrcodegen_Segment result;
+    result.mode      = qrcodegen_Mode_ECI;
+    result.numChars  = 0;
+    result.bitLength = 0;
+    if (assignVal < 0)
+        assert(false);
+    else if (assignVal < (1 << 7)) {
+        memset(buf, 0, 1 * sizeof(buf[0]));
+        appendBitsToBuffer(assignVal, 8, buf, &result.bitLength);
+    } else if (assignVal < (1 << 14)) {
+        memset(buf, 0, 2 * sizeof(buf[0]));
+        appendBitsToBuffer(2, 2, buf, &result.bitLength);
+        appendBitsToBuffer(assignVal, 14, buf, &result.bitLength);
+    } else if (assignVal < 1000000L) {
+        memset(buf, 0, 3 * sizeof(buf[0]));
+        appendBitsToBuffer(6, 3, buf, &result.bitLength);
+        appendBitsToBuffer(assignVal >> 10, 11, buf, &result.bitLength);
+        appendBitsToBuffer(assignVal & 0x3FF, 10, buf, &result.bitLength);
+    } else
+        assert(false);
+    result.data = buf;
+    return result;
+}
+
+// Calculates the number of bits needed to encode the given segments at the given version.
+// Returns a non-negative number if successful. Otherwise, returns -1 if a segment has too
+// many characters to fit its length field, or the total bits exceeds INT16_MAX.
+testable int
+getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version)
+{
+    assert(segs != NULL || len == 0);
+    long result = 0;
+    for (size_t i = 0; i < len; i++) {
+        int numChars  = segs[i].numChars;
+        int bitLength = segs[i].bitLength;
+        assert(0 <= numChars && numChars <= INT16_MAX);
+        assert(0 <= bitLength && bitLength <= INT16_MAX);
+        int ccbits = numCharCountBits(segs[i].mode, version);
+        assert(0 <= ccbits && ccbits <= 16);
+        if (numChars >= (1L << ccbits))
+            return -1;  // The segment's length doesn't fit the field's bit width
+        result += 4L + ccbits + bitLength;
+        if (result > INT16_MAX)
+            return -1;  // The sum might overflow an int type
+    }
+    assert(0 <= result && result <= INT16_MAX);
+    return (int) result;
+}
+
+// Returns the bit width of the character count field for a segment in the given mode
+// in a QR Code at the given version number. The result is in the range [0, 16].
+static int
+numCharCountBits(enum qrcodegen_Mode mode, int version)
+{
+    assert(qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX);
+    int i = (version + 7) / 17;
+    switch (mode) {
+    case qrcodegen_Mode_NUMERIC:
+    {
+        static const int temp[] = {10, 12, 14};
+        return temp[i];
+    }
+    case qrcodegen_Mode_ALPHANUMERIC:
+    {
+        static const int temp[] = {9, 11, 13};
+        return temp[i];
+    }
+    case qrcodegen_Mode_BYTE:
+    {
+        static const int temp[] = {8, 16, 16};
+        return temp[i];
+    }
+    case qrcodegen_Mode_KANJI:
+    {
+        static const int temp[] = {8, 10, 12};
+        return temp[i];
+    }
+    case qrcodegen_Mode_ECI:
+        return 0;
+    default:
+        assert(false);
+        return -1;  // Dummy value
+    }
+}
diff --git a/src/libnmc-base/qrcodegen.h b/src/libnmc-base/qrcodegen.h
new file mode 100644
index 00000000..b91ae49a
--- /dev/null
+++ b/src/libnmc-base/qrcodegen.h
@@ -0,0 +1,312 @@
+/*
+ * QR Code generator library (C)
+ *
+ * Copyright (c) Project Nayuki. (MIT License)
+ * https://www.nayuki.io/page/qr-code-generator-library
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * - The above copyright notice and this permission notice shall be included in
+ *   all copies or substantial portions of the Software.
+ * - The Software is provided "as is", without warranty of any kind, express or
+ *   implied, including but not limited to the warranties of merchantability,
+ *   fitness for a particular purpose and noninfringement. In no event shall the
+ *   authors or copyright holders be liable for any claim, damages or other
+ *   liability, whether in an action of contract, tort or otherwise, arising from,
+ *   out of or in connection with the Software or the use or other dealings in the
+ *   Software.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * This library creates QR Code symbols, which is a type of two-dimension barcode.
+ * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
+ * A QR Code structure is an immutable square grid of black and white cells.
+ * The library provides functions to create a QR Code from text or binary data.
+ * The library covers the QR Code Model 2 specification, supporting all versions (sizes)
+ * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
+ *
+ * Ways to create a QR Code object:
+ * - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary().
+ * - Low level: Custom-make the list of segments and call
+ *   qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced().
+ * (Note that all ways require supplying the desired error correction level and various byte buffers.)
+ */
+
+/*---- Enum and struct types----*/
+
+/*
+ * The error correction level in a QR Code symbol.
+ */
+enum qrcodegen_Ecc {
+    // Must be declared in ascending order of error protection
+    // so that an internal qrcodegen function works properly
+    qrcodegen_Ecc_LOW = 0,   // The QR Code can tolerate about  7% erroneous codewords
+    qrcodegen_Ecc_MEDIUM,    // The QR Code can tolerate about 15% erroneous codewords
+    qrcodegen_Ecc_QUARTILE,  // The QR Code can tolerate about 25% erroneous codewords
+    qrcodegen_Ecc_HIGH,      // The QR Code can tolerate about 30% erroneous codewords
+};
+
+/*
+ * The mask pattern used in a QR Code symbol.
+ */
+enum qrcodegen_Mask {
+    // A special value to tell the QR Code encoder to
+    // automatically select an appropriate mask pattern
+    qrcodegen_Mask_AUTO = -1,
+    // The eight actual mask patterns
+    qrcodegen_Mask_0 = 0,
+    qrcodegen_Mask_1,
+    qrcodegen_Mask_2,
+    qrcodegen_Mask_3,
+    qrcodegen_Mask_4,
+    qrcodegen_Mask_5,
+    qrcodegen_Mask_6,
+    qrcodegen_Mask_7,
+};
+
+/*
+ * Describes how a segment's data bits are interpreted.
+ */
+enum qrcodegen_Mode {
+    qrcodegen_Mode_NUMERIC      = 0x1,
+    qrcodegen_Mode_ALPHANUMERIC = 0x2,
+    qrcodegen_Mode_BYTE         = 0x4,
+    qrcodegen_Mode_KANJI        = 0x8,
+    qrcodegen_Mode_ECI          = 0x7,
+};
+
+/*
+ * A segment of character/binary/control data in a QR Code symbol.
+ * The mid-level way to create a segment is to take the payload data
+ * and call a factory function such as qrcodegen_makeNumeric().
+ * The low-level way to create a segment is to custom-make the bit buffer
+ * and initialize a qrcodegen_Segment struct with appropriate values.
+ * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
+ * Any segment longer than this is meaningless for the purpose of generating QR Codes.
+ * Moreover, the maximum allowed bit length is 32767 because
+ * the largest QR Code (version 40) has 31329 modules.
+ */
+struct qrcodegen_Segment {
+    // The mode indicator of this segment.
+    enum qrcodegen_Mode mode;
+
+    // The length of this segment's unencoded data. Measured in characters for
+    // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
+    // Always zero or positive. Not the same as the data's bit length.
+    int numChars;
+
+    // The data bits of this segment, packed in bitwise big endian.
+    // Can be null if the bit length is zero.
+    uint8_t *data;
+
+    // The number of valid data bits used in the buffer. Requires
+    // 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8.
+    // The character count (numChars) must agree with the mode and the bit buffer length.
+    int bitLength;
+};
+
+/*---- Macro constants and functions ----*/
+
+#define qrcodegen_VERSION_MIN \
+    1  // The minimum version number supported in the QR Code Model 2 standard
+#define qrcodegen_VERSION_MAX \
+    40  // The maximum version number supported in the QR Code Model 2 standard
+
+// Calculates the number of bytes needed to store any QR Code up to and including the given version number,
+// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];'
+// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16).
+// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX.
+#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) *4 + 17) * ((n) *4 + 17) + 7) / 8 + 1)
+
+// The worst-case number of bytes needed to store one QR Code, up to and including
+// version 40. This value equals 3918, which is just under 4 kilobytes.
+// Use this more convenient value to avoid calculating tighter memory bounds for buffers.
+#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)
+
+/*---- Functions (high level) to generate QR Codes ----*/
+
+/*
+ * Encodes the given text string to a QR Code, returning true if encoding succeeded.
+ * If the data is too long to fit in any version in the given range
+ * at the given ECC level, then false is returned.
+ * - The input text must be encoded in UTF-8 and contain no NULs.
+ * - The variables ecl and mask must correspond to enum constant values.
+ * - Requires 1 <= minVersion <= maxVersion <= 40.
+ * - The arrays tempBuffer and qrcode must each have a length
+ *   of at least qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion).
+ * - After the function returns, tempBuffer contains no useful data.
+ * - If successful, the resulting QR Code may use numeric,
+ *   alphanumeric, or byte mode to encode the text.
+ * - In the most optimistic case, a QR Code at version 40 with low ECC
+ *   can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string
+ *   up to 4296 characters, or any digit string up to 7089 characters.
+ *   These numbers represent the hard upper limit of the QR Code standard.
+ * - Please consult the QR Code specification for information on
+ *   data capacities per version, ECC level, and text encoding mode.
+ */
+bool qrcodegen_encodeText(const char *        text,
+                          uint8_t             tempBuffer[],
+                          uint8_t             qrcode[],
+                          enum qrcodegen_Ecc  ecl,
+                          int                 minVersion,
+                          int                 maxVersion,
+                          enum qrcodegen_Mask mask,
+                          bool                boostEcl);
+
+/*
+ * Encodes the given binary data to a QR Code, returning true if encoding succeeded.
+ * If the data is too long to fit in any version in the given range
+ * at the given ECC level, then false is returned.
+ * - The input array range dataAndTemp[0 : dataLen] should normally be
+ *   valid UTF-8 text, but is not required by the QR Code standard.
+ * - The variables ecl and mask must correspond to enum constant values.
+ * - Requires 1 <= minVersion <= maxVersion <= 40.
+ * - The arrays dataAndTemp and qrcode must each have a length
+ *   of at least qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion).
+ * - After the function returns, the contents of dataAndTemp may have changed,
+ *   and does not represent useful data anymore.
+ * - If successful, the resulting QR Code will use byte mode to encode the data.
+ * - In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte
+ *   sequence up to length 2953. This is the hard upper limit of the QR Code standard.
+ * - Please consult the QR Code specification for information on
+ *   data capacities per version, ECC level, and text encoding mode.
+ */
+bool qrcodegen_encodeBinary(uint8_t             dataAndTemp[],
+                            size_t              dataLen,
+                            uint8_t             qrcode[],
+                            enum qrcodegen_Ecc  ecl,
+                            int                 minVersion,
+                            int                 maxVersion,
+                            enum qrcodegen_Mask mask,
+                            bool                boostEcl);
+
+/*---- Functions (low level) to generate QR Codes ----*/
+
+/*
+ * Renders a QR Code representing the given segments at the given error correction level.
+ * The smallest possible QR Code version is automatically chosen for the output. Returns true if
+ * QR Code creation succeeded, or false if the data is too long to fit in any version. The ECC level
+ * of the result may be higher than the ecl argument if it can be done without increasing the version.
+ * This function allows the user to create a custom sequence of segments that switches
+ * between modes (such as alphanumeric and byte) to encode text in less space.
+ * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
+ * To save memory, the segments' data buffers can alias/overlap tempBuffer, and will
+ * result in them being clobbered, but the QR Code output will still be correct.
+ * But the qrcode array must not overlap tempBuffer or any segment's data buffer.
+ */
+bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[],
+                              size_t                         len,
+                              enum qrcodegen_Ecc             ecl,
+                              uint8_t                        tempBuffer[],
+                              uint8_t                        qrcode[]);
+
+/*
+ * Renders a QR Code representing the given segments with the given encoding parameters.
+ * Returns true if QR Code creation succeeded, or false if the data is too long to fit in the range of versions.
+ * The smallest possible QR Code version within the given range is automatically
+ * chosen for the output. Iff boostEcl is true, then the ECC level of the result
+ * may be higher than the ecl argument if it can be done without increasing the
+ * version. The mask number is either between 0 to 7 (inclusive) to force that
+ * mask, or -1 to automatically choose an appropriate mask (which may be slow).
+ * This function allows the user to create a custom sequence of segments that switches
+ * between modes (such as alphanumeric and byte) to encode text in less space.
+ * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
+ * To save memory, the segments' data buffers can alias/overlap tempBuffer, and will
+ * result in them being clobbered, but the QR Code output will still be correct.
+ * But the qrcode array must not overlap tempBuffer or any segment's data buffer.
+ */
+bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[],
+                                      size_t                         len,
+                                      enum qrcodegen_Ecc             ecl,
+                                      int                            minVersion,
+                                      int                            maxVersion,
+                                      int                            mask,
+                                      bool                           boostEcl,
+                                      uint8_t                        tempBuffer[],
+                                      uint8_t                        qrcode[]);
+
+/*
+ * Tests whether the given string can be encoded as a segment in alphanumeric mode.
+ * A string is encodable iff each character is in the following set: 0 to 9, A to Z
+ * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ */
+bool qrcodegen_isAlphanumeric(const char *text);
+
+/*
+ * Tests whether the given string can be encoded as a segment in numeric mode.
+ * A string is encodable iff each character is in the range 0 to 9.
+ */
+bool qrcodegen_isNumeric(const char *text);
+
+/*
+ * Returns the number of bytes (uint8_t) needed for the data buffer of a segment
+ * containing the given number of characters using the given mode. Notes:
+ * - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or
+ *   the number of needed bits exceeds INT16_MAX (i.e. 32767).
+ * - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096.
+ * - It is okay for the user to allocate more bytes for the buffer than needed.
+ * - For byte mode, numChars measures the number of bytes, not Unicode code points.
+ * - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned.
+ *   An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
+ */
+size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars);
+
+/*
+ * Returns a segment representing the given binary data encoded in
+ * byte mode. All input byte arrays are acceptable. Any text string
+ * can be converted to UTF-8 bytes and encoded as a byte mode segment.
+ */
+struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]);
+
+/*
+ * Returns a segment representing the given string of decimal digits encoded in numeric mode.
+ */
+struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]);
+
+/*
+ * Returns a segment representing the given text string encoded in alphanumeric mode.
+ * The characters allowed are: 0 to 9, A to Z (uppercase only), space,
+ * dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ */
+struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]);
+
+/*
+ * Returns a segment representing an Extended Channel Interpretation
+ * (ECI) designator with the given assignment value.
+ */
+struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]);
+
+/*---- Functions to extract raw data from QR Codes ----*/
+
+/*
+ * Returns the side length of the given QR Code, assuming that encoding succeeded.
+ * The result is in the range [21, 177]. Note that the length of the array buffer
+ * is related to the side length - every 'uint8_t qrcode[]' must have length at least
+ * qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1).
+ */
+int qrcodegen_getSize(const uint8_t qrcode[]);
+
+/*
+ * Returns the color of the module (pixel) at the given coordinates, which is false
+ * for white or true for black. The top left corner has the coordinates (x=0, y=0).
+ * If the given coordinates are out of bounds, then false (white) is returned.
+ */
+bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y);
+
+#ifdef __cplusplus
+}
+#endif