/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2018 - 2019 Red Hat, Inc.
 */

#include "libnm-core-impl/nm-default-libnm-core.h"

#include "nm-setting-wireguard.h"

#include "nm-setting-private.h"
#include "nm-utils-private.h"
#include "nm-connection-private.h"
#include "libnm-glib-aux/nm-secret-utils.h"

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

/**
 * SECTION:nm-setting-wireguard
 * @short_description: Describes connection properties for wireguard related options
 *
 * The #NMSettingWireGuard object is a #NMSetting subclass that contains settings
 * for configuring WireGuard.
 **/

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

static NMWireGuardPeer *_wireguard_peer_dup(const NMWireGuardPeer *self);

G_DEFINE_BOXED_TYPE(NMWireGuardPeer,
                    nm_wireguard_peer,
                    _wireguard_peer_dup,
                    nm_wireguard_peer_unref)

/* NMWireGuardPeer can also track invalid allowed-ip settings, and only reject
 * them later during is_valid(). Such values are marked by a leading 'X' character
 * in the @allowed_ips. It is expected, that such values are the exception, and
 * commonly not present. */
#define ALLOWED_IP_INVALID_X     'X'
#define ALLOWED_IP_INVALID_X_STR "X"

/**
 * NMWireGuardPeer:
 *
 * The settings of one WireGuard peer.
 *
 * Since: 1.16
 */
struct _NMWireGuardPeer {
    NMSockAddrEndpoint  *endpoint;
    char                *public_key;
    char                *preshared_key;
    GPtrArray           *allowed_ips;
    int                  refcount;
    NMSettingSecretFlags preshared_key_flags;
    guint16              persistent_keepalive;
    bool                 public_key_valid : 1;
    bool                 preshared_key_valid : 1;
    bool                 sealed : 1;
};

static gboolean
NM_IS_WIREGUARD_PEER(const NMWireGuardPeer *self, gboolean also_sealed)
{
    return self && self->refcount > 0 && (also_sealed || !self->sealed);
}

/**
 * nm_wireguard_peer_new:
 *
 * Returns: (transfer full): a new, default, unsealed #NMWireGuardPeer instance.
 *
 * Since: 1.16
 */
NMWireGuardPeer *
nm_wireguard_peer_new(void)
{
    NMWireGuardPeer *self;

    self  = g_slice_new(NMWireGuardPeer);
    *self = (NMWireGuardPeer) {
        .refcount            = 1,
        .preshared_key_flags = NM_SETTING_SECRET_FLAG_NOT_REQUIRED,
    };
    return self;
}

/**
 * nm_wireguard_peer_new_clone:
 * @self: the #NMWireGuardPeer instance to copy.
 * @with_secrets: if %TRUE, the preshared-key secrets are copied
 *  as well. Otherwise, they will be removed.
 *
 * Returns: (transfer full): a clone of @self. This instance
 *   is always unsealed.
 *
 * Since: 1.16
 */
NMWireGuardPeer *
nm_wireguard_peer_new_clone(const NMWireGuardPeer *self, gboolean with_secrets)
{
    NMWireGuardPeer *new;
    guint i;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    new  = g_slice_new(NMWireGuardPeer);
    *new = (NMWireGuardPeer) {
        .refcount             = 1,
        .public_key           = g_strdup(self->public_key),
        .public_key_valid     = self->public_key_valid,
        .preshared_key        = with_secrets ? g_strdup(self->preshared_key) : NULL,
        .preshared_key_valid  = self->preshared_key_valid,
        .preshared_key_flags  = self->preshared_key_flags,
        .endpoint             = nm_sock_addr_endpoint_ref(self->endpoint),
        .persistent_keepalive = self->persistent_keepalive,
    };
    if (self->allowed_ips && self->allowed_ips->len > 0) {
        new->allowed_ips = g_ptr_array_new_full(self->allowed_ips->len, g_free);
        for (i = 0; i < self->allowed_ips->len; i++) {
            g_ptr_array_add(new->allowed_ips, g_strdup(self->allowed_ips->pdata[i]));
        }
    }
    return new;
}

/**
 * nm_wireguard_peer_ref:
 * @self: (nullable): the #NMWireGuardPeer instance
 *
 * Returns: returns the input argument @self after incrementing
 *   the reference count.
 *
 * Since 1.42, ref-counting of #NMWireGuardPeer is thread-safe.
 *
 * Since: 1.16
 */
NMWireGuardPeer *
nm_wireguard_peer_ref(NMWireGuardPeer *self)
{
    if (!self)
        return NULL;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    nm_assert(self->refcount < G_MAXINT);

    g_atomic_int_inc(&self->refcount);
    return self;
}

/**
 * nm_wireguard_peer_unref:
 * @self: (nullable): the #NMWireGuardPeer instance
 *
 * Drop a reference to @self. If the last reference is dropped,
 * the instance is freed and all associate data released.
 *
 * Since 1.42, ref-counting of #NMWireGuardPeer is thread-safe.
 *
 * Since: 1.16
 */
void
nm_wireguard_peer_unref(NMWireGuardPeer *self)
{
    if (!self)
        return;

    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE));

    if (!g_atomic_int_dec_and_test(&self->refcount))
        return;

    nm_sock_addr_endpoint_unref(self->endpoint);
    if (self->allowed_ips)
        g_ptr_array_unref(self->allowed_ips);
    g_free(self->public_key);
    nm_free_secret(self->preshared_key);
    g_slice_free(NMWireGuardPeer, self);
}

/**
 * _wireguard_peer_dup:
 * @self: the #NMWireGuardPeer instance
 *
 * Duplicates the #NMWireGuardPeer instance. Note that if @self
 * is already sealed, this increments the reference count and
 * returns it. If the instance is still unsealed, it is copied.
 *
 * Returns: (transfer full): a duplicate of @self, or (if the
 *   instance is sealed and thus immutable) a reference to @self.
 *   As such, the instance will be sealed if and only if @self is
 *   sealed.
 */
static NMWireGuardPeer *
_wireguard_peer_dup(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    if (self->sealed)
        return nm_wireguard_peer_ref((NMWireGuardPeer *) self);
    return nm_wireguard_peer_new_clone(self, TRUE);
}

/**
 * nm_wireguard_peer_seal:
 * @self: the #NMWireGuardPeer instance
 *
 * Seal the #NMWireGuardPeer instance. Afterwards, it is a bug
 * to call all functions that modify the instance (except ref/unref).
 * A sealed instance cannot be unsealed again, but you can create
 * an unsealed copy with nm_wireguard_peer_new_clone().
 *
 * Since: 1.16
 */
void
nm_wireguard_peer_seal(NMWireGuardPeer *self)
{
    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE));

    self->sealed = TRUE;

    if (self->allowed_ips) {
        if (self->allowed_ips->len == 0)
            nm_clear_pointer(&self->allowed_ips, g_ptr_array_unref);
    }
}

/**
 * nm_wireguard_peer_is_sealed:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: whether @self is sealed or not.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_is_sealed(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), FALSE);

    return self->sealed;
}

/**
 * nm_wireguard_peer_get_public_key:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: (transfer none): the public key or %NULL if unset.
 *
 * Since: 1.16
 */
const char *
nm_wireguard_peer_get_public_key(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    return self->public_key;
}

/**
 * nm_wireguard_peer_set_public_key:
 * @self: the unsealed #NMWireGuardPeer instance
 * @public_key: (nullable) (transfer none): the new public
 *   key or %NULL to clear the public key.
 * @accept_invalid: if %TRUE and @public_key is not %NULL and
 *   invalid, then do not modify the instance.
 *
 * Reset the public key. Note that if the public key is valid, it
 * will be normalized (which may or may not modify the set value).
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Returns: %TRUE if the key was valid or %NULL. Returns
 *   %FALSE for invalid keys. Depending on @accept_invalid
 *   will an invalid key be set or not.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_set_public_key(NMWireGuardPeer *self,
                                 const char      *public_key,
                                 gboolean         accept_invalid)
{
    char    *public_key_normalized = NULL;
    gboolean is_valid;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE), FALSE);

    if (!public_key) {
        nm_clear_g_free(&self->public_key);
        return TRUE;
    }

    is_valid = nm_utils_base64secret_normalize(public_key,
                                               NM_WIREGUARD_PUBLIC_KEY_LEN,
                                               &public_key_normalized);
    nm_assert(is_valid == (public_key_normalized != NULL));

    if (!is_valid && !accept_invalid)
        return FALSE;

    self->public_key_valid = is_valid;
    g_free(self->public_key);
    self->public_key = public_key_normalized ?: g_strdup(public_key);
    return is_valid;
}

void
_nm_wireguard_peer_set_public_key_bin(NMWireGuardPeer *self,
                                      const guint8 public_key[static NM_WIREGUARD_PUBLIC_KEY_LEN])
{
    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE));

    nm_assert(public_key);

    nm_clear_g_free(&self->public_key);

    self->public_key       = g_base64_encode(public_key, NM_WIREGUARD_PUBLIC_KEY_LEN);
    self->public_key_valid = TRUE;
}

/**
 * nm_wireguard_peer_get_preshared_key:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: (transfer none): the preshared key or %NULL if unset.
 *
 * Since: 1.16
 */
const char *
nm_wireguard_peer_get_preshared_key(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    return self->preshared_key;
}

/**
 * nm_wireguard_peer_set_preshared_key:
 * @self: the unsealed #NMWireGuardPeer instance
 * @preshared_key: (nullable) (transfer none): the new preshared
 *   key or %NULL to clear the preshared key.
 * @accept_invalid: whether to allow setting the key to an invalid
 *   value. If %FALSE, @self is unchanged if the key is invalid
 *   and if %FALSE is returned.
 *
 * Reset the preshared key. Note that if the preshared key is valid, it
 * will be normalized (which may or may not modify the set value).
 *
 * Note that the preshared-key is a secret and consequently has corresponding
 * preshared-key-flags property. This is so that secrets can be optional
 * and requested on demand from a secret-agent. Also, an invalid  preshared-key
 * may optionally cause nm_wireguard_peer_is_valid() to fail or it may
 * be accepted.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Returns: %TRUE if the preshared-key is valid, otherwise %FALSE.
 *   %NULL is considered a valid value.
 *   If the key is invalid, it depends on @accept_invalid whether the
 *   previous value was reset.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_set_preshared_key(NMWireGuardPeer *self,
                                    const char      *preshared_key,
                                    gboolean         accept_invalid)
{
    char    *preshared_key_normalized = NULL;
    gboolean is_valid;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE), FALSE);

    if (!preshared_key) {
        nm_clear_pointer(&self->preshared_key, nm_free_secret);
        return TRUE;
    }

    is_valid = nm_utils_base64secret_normalize(preshared_key,
                                               NM_WIREGUARD_SYMMETRIC_KEY_LEN,
                                               &preshared_key_normalized);
    nm_assert(is_valid == (preshared_key_normalized != NULL));

    if (!is_valid && !accept_invalid)
        return FALSE;

    self->preshared_key_valid = is_valid;
    nm_free_secret(self->preshared_key);
    self->preshared_key = preshared_key_normalized ?: g_strdup(preshared_key);
    return is_valid;
}

/**
 * nm_wireguard_peer_get_preshared_key_flags:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: get the secret flags for the preshared-key.
 *
 * Since: 1.16
 */
NMSettingSecretFlags
nm_wireguard_peer_get_preshared_key_flags(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), 0);

    return self->preshared_key_flags;
}

/**
 * nm_wireguard_peer_set_preshared_key_flags:
 * @self: the unsealed #NMWireGuardPeer instance
 * @preshared_key_flags: the secret flags to set.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Since: 1.16
 */
void
nm_wireguard_peer_set_preshared_key_flags(NMWireGuardPeer     *self,
                                          NMSettingSecretFlags preshared_key_flags)
{
    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE));

    self->preshared_key_flags = preshared_key_flags;
}

/**
 * nm_wireguard_peer_get_persistent_keepalive:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: get the persistent-keepalive setting in seconds. Set to zero to disable
 *   keep-alive.
 *
 * Since: 1.16
 */
guint16
nm_wireguard_peer_get_persistent_keepalive(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), 0);

    return self->persistent_keepalive;
}

/**
 * nm_wireguard_peer_set_persistent_keepalive:
 * @self: the unsealed #NMWireGuardPeer instance
 * @persistent_keepalive: the keep-alive value to set.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Since: 1.16
 */
void
nm_wireguard_peer_set_persistent_keepalive(NMWireGuardPeer *self, guint16 persistent_keepalive)
{
    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE));

    self->persistent_keepalive = persistent_keepalive;
}

NMSockAddrEndpoint *
_nm_wireguard_peer_get_endpoint(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    return self->endpoint;
}

/**
 * nm_wireguard_peer_get_endpoint:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: (transfer none): the endpoint or %NULL if none was set.
 *
 * Since: 1.16
 */
const char *
nm_wireguard_peer_get_endpoint(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    return self->endpoint ? nm_sock_addr_endpoint_get_endpoint(self->endpoint) : NULL;
}

void
_nm_wireguard_peer_set_endpoint(NMWireGuardPeer *self, NMSockAddrEndpoint *endpoint)
{
    NMSockAddrEndpoint *old;

    nm_assert(NM_IS_WIREGUARD_PEER(self, FALSE));

    old            = self->endpoint;
    self->endpoint = nm_sock_addr_endpoint_ref(endpoint);
    nm_sock_addr_endpoint_unref(old);
}

/**
 * nm_wireguard_peer_set_endpoint:
 * @self: the unsealed #NMWireGuardPeer instance
 * @endpoint: the socket address endpoint to set or %NULL.
 * @allow_invalid: if %TRUE, also invalid values are set.
 *   If %FALSE, the function does nothing for invalid @endpoint
 *   arguments.
 *
 * Sets or clears the endpoint of @self.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Returns: %TRUE if the endpoint is %NULL or valid. For an
 *   invalid @endpoint argument, %FALSE is returned. Depending
 *   on @allow_invalid, the instance will be modified.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_set_endpoint(NMWireGuardPeer *self, const char *endpoint, gboolean allow_invalid)
{
    NMSockAddrEndpoint *old;
    NMSockAddrEndpoint *new;
    gboolean is_valid;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE), FALSE);

    if (!endpoint) {
        nm_clear_pointer(&self->endpoint, nm_sock_addr_endpoint_unref);
        return TRUE;
    }

    new = nm_sock_addr_endpoint_new(endpoint);

    is_valid = (nm_sock_addr_endpoint_get_host(new) != NULL);

    if (!allow_invalid && !is_valid) {
        nm_sock_addr_endpoint_unref(new);
        return FALSE;
    }

    old            = self->endpoint;
    self->endpoint = new;
    nm_sock_addr_endpoint_unref(old);
    return is_valid;
}

/**
 * nm_wireguard_peer_get_allowed_ips_len:
 * @self: the #NMWireGuardPeer instance
 *
 * Returns: the number of allowed-ips entries.
 *
 * Since: 1.16
 */
guint
nm_wireguard_peer_get_allowed_ips_len(const NMWireGuardPeer *self)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), 0);

    return self->allowed_ips ? self->allowed_ips->len : 0u;
}

/**
 * nm_wireguard_peer_get_allowed_ip:
 * @self: the #NMWireGuardPeer instance
 * @idx: the index from zero to (allowed-ips-len - 1) to
 *   retrieve.
 * @out_is_valid: (nullable): %TRUE if the returned value is a valid allowed-ip
 *   setting.
 *   This parameter is wrongly not marked as (out) argument, it is
 *   thus not accessible via introspection. This cannot be fixed without
 *   breaking API for introspection users.
 *
 * Returns: (transfer none) (nullable): the allowed-ip setting at index @idx.
 *   If @idx is out of range, %NULL will be returned.
 *
 * Since: 1.16
 */
const char *
nm_wireguard_peer_get_allowed_ip(const NMWireGuardPeer *self, guint idx, gboolean *out_is_valid)
{
    const char *s;

    /* With LTO, the compiler might warn about the g_return_val_if_fail()
     * code path not initializing the output argument. Workaround that by
     * always setting the out argument. */
    NM_SET_OUT(out_is_valid, FALSE);

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), NULL);

    if (!self->allowed_ips || idx >= self->allowed_ips->len)
        return NULL;

    s = self->allowed_ips->pdata[idx];
    NM_SET_OUT(out_is_valid, s[0] != ALLOWED_IP_INVALID_X);
    return s[0] == ALLOWED_IP_INVALID_X ? &s[1] : s;
}

/**
 * nm_wireguard_peer_clear_allowed_ips:
 * @self: the unsealed #NMWireGuardPeer instance
 *
 * Removes all allowed-ip entries.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Since: 1.16
 */
void
nm_wireguard_peer_clear_allowed_ips(NMWireGuardPeer *self)
{
    g_return_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE));

    if (self->allowed_ips)
        g_ptr_array_set_size(self->allowed_ips, 0);
}

static gboolean
_peer_append_allowed_ip(NMWireGuardPeer *self, const char *allowed_ip, gboolean accept_invalid)
{
    int      addr_family;
    int      prefix;
    NMIPAddr addrbin;
    char    *str;
    gboolean is_valid = TRUE;

    nm_assert(NM_IS_WIREGUARD_PEER(self, FALSE));
    nm_assert(allowed_ip);

    /* normalize the address (if it is valid. Otherwise, take it
     * as-is (it will render the instance invalid). */
    if (!nm_inet_parse_with_prefix_bin(AF_UNSPEC, allowed_ip, &addr_family, &addrbin, &prefix)) {
        if (!accept_invalid)
            return FALSE;
        /* mark the entry as invalid by having a "X" prefix. */
        str      = g_strconcat(ALLOWED_IP_INVALID_X_STR, allowed_ip, NULL);
        is_valid = FALSE;
    } else {
        char addrstr[NM_INET_ADDRSTRLEN];

        nm_assert_addr_family(addr_family);

        nm_inet_ntop(addr_family, &addrbin, addrstr);
        if (prefix >= 0)
            str = g_strdup_printf("%s/%d", addrstr, prefix);
        else
            str = g_strdup(addrstr);
        nm_assert(str[0] != ALLOWED_IP_INVALID_X);
    }

    if (!self->allowed_ips)
        self->allowed_ips = g_ptr_array_new_with_free_func(g_free);

    g_ptr_array_add(self->allowed_ips, str);
    return is_valid;
}

/**
 * nm_wireguard_peer_append_allowed_ip:
 * @self: the unsealed #NMWireGuardPeer instance
 * @allowed_ip: the allowed-ip entry to set.
 * @accept_invalid: if %TRUE, also invalid @allowed_ip value
 *   will be appended. Otherwise, the function does nothing
 *   in face of invalid values and returns %FALSE.
 *
 * Appends @allowed_ip setting to the list. This does not check
 * for duplicates and always appends @allowed_ip to the end of the
 * list. If @allowed_ip is valid, it will be normalized and a modified
 * for might be appended. If @allowed_ip is invalid, it will still be
 * appended, but later verification will fail.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Returns: %TRUE if the value is a valid allowed-ips value, %FALSE otherwise.
 *   Depending on @accept_invalid, also invalid values are added.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_append_allowed_ip(NMWireGuardPeer *self,
                                    const char      *allowed_ip,
                                    gboolean         accept_invalid)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE), FALSE);
    g_return_val_if_fail(allowed_ip, FALSE);

    return _peer_append_allowed_ip(self, allowed_ip, accept_invalid);
}

/**
 * nm_wireguard_peer_remove_allowed_ip:
 * @self: the unsealed #NMWireGuardPeer instance
 * @idx: the index from zero to (allowed-ips-len - 1) to
 *   retrieve. If the index is out of range, %FALSE is returned
 *   and nothing is done.
 *
 * Removes the allowed-ip at the given @idx. This shifts all
 * following entries one index down.
 *
 * It is a bug trying to modify a sealed #NMWireGuardPeer instance.
 *
 * Returns: %TRUE if @idx was valid and the allowed-ip was removed.
 *   %FALSE otherwise, and the peer will not be changed.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_remove_allowed_ip(NMWireGuardPeer *self, guint idx)
{
    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, FALSE), FALSE);

    if (!self->allowed_ips || idx >= self->allowed_ips->len)
        return FALSE;

    g_ptr_array_remove_index(self->allowed_ips, idx);
    return TRUE;
}

/**
 * nm_wireguard_peer_is_valid:
 * @self: the #NMWireGuardPeer instance
 * @check_secrets: if %TRUE, non-secret properties are validated.
 *   Otherwise, they are ignored for this purpose.
 * @check_non_secrets: if %TRUE, secret properties are validated.
 *   Otherwise, they are ignored for this purpose.
 * @error: the #GError location for returning the failure reason.
 *
 * Returns:  %TRUE if the peer is valid or fails with an error
 *   reason.
 *
 * Since: 1.16
 */
gboolean
nm_wireguard_peer_is_valid(const NMWireGuardPeer *self,
                           gboolean               check_non_secrets,
                           gboolean               check_secrets,
                           GError               **error)
{
    guint i;

    g_return_val_if_fail(NM_IS_WIREGUARD_PEER(self, TRUE), FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    if (check_non_secrets) {
        if (!self->public_key) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_MISSING_PROPERTY,
                                _("missing public-key for peer"));
            return FALSE;
        } else if (!self->public_key_valid) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("invalid public-key for peer"));
            return FALSE;
        }
    }

    if (check_secrets) {
        if (self->preshared_key && !self->preshared_key_valid) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("invalid preshared-key for peer"));
            return FALSE;
        }
    }

    if (check_non_secrets) {
        if (!_nm_utils_secret_flags_validate(self->preshared_key_flags,
                                             NULL,
                                             NULL,
                                             NM_SETTING_SECRET_FLAG_NONE,
                                             error))
            return FALSE;
    }

    if (check_non_secrets) {
        if (self->endpoint && !nm_sock_addr_endpoint_get_host(self->endpoint)) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("invalid endpoint for peer"));
            return FALSE;
        }

        if (self->allowed_ips) {
            for (i = 0; i < self->allowed_ips->len; i++) {
                const char *s = self->allowed_ips->pdata[i];

                if (s[0] == ALLOWED_IP_INVALID_X) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("invalid IP address \"%s\" for allowed-ip of peer"),
                                &s[1]);
                    return FALSE;
                }
            }
        }

        if (!_nm_setting_secret_flags_valid(self->preshared_key_flags)) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("invalid preshared-key-flags for peer"));
            return FALSE;
        }
    }

    return TRUE;
}

/**
 * nm_wireguard_peer_cmp:
 * @a: (nullable): the #NMWireGuardPeer to compare.
 * @b: (nullable): the other #NMWireGuardPeer to compare.
 * @compare_flags: #NMSettingCompareFlags to affect the comparison.
 *
 * Returns: zero of the two instances are equivalent or
 *   a non-zero integer otherwise. This defines a total ordering
 *   over the peers. Whether a peer is sealed or not, does not
 *   affect the comparison.
 *
 * Since: 1.16
 */
int
nm_wireguard_peer_cmp(const NMWireGuardPeer *a,
                      const NMWireGuardPeer *b,
                      NMSettingCompareFlags  compare_flags)
{
    guint i, n;

    NM_CMP_SELF(a, b);

    /* regardless of the @compare_flags, the public-key is the ID of the peer. It must
     * always be compared. */
    NM_CMP_FIELD_BOOL(a, b, public_key_valid);
    NM_CMP_FIELD_STR0(a, b, public_key);

    if (NM_FLAGS_ANY(compare_flags,
                     NM_SETTING_COMPARE_FLAG_INFERRABLE | NM_SETTING_COMPARE_FLAG_FUZZY))
        return 0;

    NM_CMP_FIELD_BOOL(a, b, endpoint);
    if (a->endpoint) {
        NM_CMP_DIRECT_STRCMP0(nm_sock_addr_endpoint_get_endpoint(a->endpoint),
                              nm_sock_addr_endpoint_get_endpoint(b->endpoint));
    }

    NM_CMP_FIELD(a, b, persistent_keepalive);

    NM_CMP_DIRECT((n = (a->allowed_ips ? a->allowed_ips->len : 0u)),
                  (b->allowed_ips ? b->allowed_ips->len : 0u));
    for (i = 0; i < n; i++)
        NM_CMP_DIRECT_STRCMP0(a->allowed_ips->pdata[i], b->allowed_ips->pdata[i]);

    NM_CMP_FIELD(a, b, preshared_key_flags);

    if (!NM_FLAGS_HAS(compare_flags, NM_SETTING_COMPARE_FLAG_IGNORE_SECRETS)) {
        if (NM_FLAGS_HAS(compare_flags, NM_SETTING_COMPARE_FLAG_IGNORE_AGENT_OWNED_SECRETS)
            && NM_FLAGS_HAS(a->preshared_key_flags, NM_SETTING_SECRET_FLAG_AGENT_OWNED)) {
            /* pass */
        } else if (NM_FLAGS_HAS(compare_flags, NM_SETTING_COMPARE_FLAG_IGNORE_NOT_SAVED_SECRETS)
                   && NM_FLAGS_HAS(a->preshared_key_flags, NM_SETTING_SECRET_FLAG_NOT_SAVED)) {
            /* pass */
        } else {
            NM_CMP_FIELD_BOOL(a, b, preshared_key_valid);
            NM_CMP_FIELD_STR0(a, b, preshared_key);
        }
    }

    return 0;
}

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

typedef struct {
    const char      *public_key;
    NMWireGuardPeer *peer;
    guint            idx;
} PeerData;

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

NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_FWMARK,
                                  PROP_IP4_AUTO_DEFAULT_ROUTE,
                                  PROP_IP6_AUTO_DEFAULT_ROUTE,
                                  PROP_LISTEN_PORT,
                                  PROP_MTU,
                                  PROP_PEER_ROUTES,
                                  PROP_PRIVATE_KEY,
                                  PROP_PRIVATE_KEY_FLAGS, );

typedef struct {
    char       *private_key;
    GPtrArray  *peers_arr;
    GHashTable *peers_hash;
    guint       private_key_flags;
    int         ip4_auto_default_route;
    int         ip6_auto_default_route;
    guint32     fwmark;
    guint32     mtu;
    guint32     listen_port;
    bool        peer_routes;
    bool        private_key_valid : 1;
} NMSettingWireGuardPrivate;

/**
 * NMSettingWireGuard:
 *
 * WireGuard Settings
 *
 * Since: 1.16
 */
struct _NMSettingWireGuard {
    NMSetting                 parent;
    NMSettingWireGuardPrivate _priv;
};

struct _NMSettingWireGuardClass {
    NMSettingClass parent;
};

G_DEFINE_TYPE(NMSettingWireGuard, nm_setting_wireguard, NM_TYPE_SETTING)

#define NM_SETTING_WIREGUARD_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMSettingWireGuard, NM_IS_SETTING_WIREGUARD, NMSetting)

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

#define peers_psk_get_secret_name_a(public_key, to_free)                                        \
    nm_construct_name_a(NM_SETTING_WIREGUARD_PEERS ".%s." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, \
                        (public_key),                                                           \
                        (to_free))

#define peers_psk_get_secret_name_dup(public_key)                                           \
    g_strdup_printf(NM_SETTING_WIREGUARD_PEERS ".%s." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, \
                    (public_key))

#define peers_psk_get_secret_parse_a(secret_public_key, public_key_free)                           \
    ({                                                                                             \
        const char *_secret_public_key = (secret_public_key);                                      \
        char      **_public_key_free   = (public_key_free);                                        \
        const char *_public_key        = NULL;                                                     \
                                                                                                   \
        nm_assert(_public_key_free && !*_public_key_free);                                         \
                                                                                                   \
        if (NM_STR_HAS_PREFIX(_secret_public_key, NM_SETTING_WIREGUARD_PEERS ".")) {               \
            _secret_public_key += NM_STRLEN(NM_SETTING_WIREGUARD_PEERS ".");                       \
            if (NM_STR_HAS_SUFFIX(_secret_public_key, "." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)) { \
                _public_key =                                                                      \
                    nm_strndup_a(300,                                                              \
                                 _secret_public_key,                                               \
                                 strlen(_secret_public_key)                                        \
                                     - NM_STRLEN("." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY),        \
                                 _public_key_free);                                                \
            }                                                                                      \
        }                                                                                          \
                                                                                                   \
        _public_key;                                                                               \
    })

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

/**
 * nm_setting_wireguard_get_private_key:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: (transfer none): the set private-key or %NULL.
 *
 * Since: 1.16
 */
const char *
nm_setting_wireguard_get_private_key(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), NULL);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->private_key;
}

/**
 * nm_setting_wireguard_get_private_key_flags:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the secret-flags for #NMSettingWireGuard:private-key.
 *
 * Since: 1.16
 */
NMSettingSecretFlags
nm_setting_wireguard_get_private_key_flags(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->private_key_flags;
}

/**
 * nm_setting_wireguard_get_fwmark:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the set firewall mark.
 *
 * Since: 1.16
 */
guint32
nm_setting_wireguard_get_fwmark(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->fwmark;
}

/**
 * nm_setting_wireguard_get_listen_port:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the set UDP listen port.
 *
 * Since: 1.16
 */
guint16
nm_setting_wireguard_get_listen_port(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->listen_port;
}

/**
 * nm_setting_wireguard_get_peer_routes:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: whether automatically add peer routes.
 *
 * Since: 1.16
 */
gboolean
nm_setting_wireguard_get_peer_routes(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), TRUE);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->peer_routes;
}

/**
 * nm_setting_wireguard_get_mtu:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the MTU of the setting.
 *
 * Since: 1.16
 */
guint32
nm_setting_wireguard_get_mtu(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->mtu;
}

/**
 * nm_setting_wireguard_get_ip4_auto_default_route:
 * @self: the #NMSettingWireGuard setting.
 *
 * Returns: the "ip4-auto-default-route" property of the setting.
 *
 * Since: 1.20
 */
NMTernary
nm_setting_wireguard_get_ip4_auto_default_route(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), NM_TERNARY_DEFAULT);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->ip4_auto_default_route;
}

/**
 * nm_setting_wireguard_get_ip6_auto_default_route:
 * @self: the #NMSettingWireGuard setting.
 *
 * Returns: the "ip6-auto-default-route" property of the setting.
 *
 * Since: 1.20
 */
NMTernary
nm_setting_wireguard_get_ip6_auto_default_route(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), NM_TERNARY_DEFAULT);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->ip6_auto_default_route;
}

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

static void
_peer_free(PeerData *pd)
{
    nm_assert(pd);

    nm_wireguard_peer_unref(pd->peer);
    g_slice_free(PeerData, pd);
}

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

static void
_peers_notify(gpointer self)
{
    _nm_setting_emit_property_changed(self);
}

static PeerData *
_peers_get(NMSettingWireGuardPrivate *priv, guint idx)
{
    PeerData *pd;

    nm_assert(priv);
    nm_assert(idx < priv->peers_arr->len);

    pd = priv->peers_arr->pdata[idx];

    nm_assert(pd);
    nm_assert(pd->idx == idx);
    nm_assert(NM_IS_WIREGUARD_PEER(pd->peer, TRUE));
    nm_assert(nm_wireguard_peer_is_sealed(pd->peer));
    nm_assert(pd->public_key == nm_wireguard_peer_get_public_key(pd->peer));
    nm_assert(g_hash_table_lookup(priv->peers_hash, pd) == pd);

    return pd;
}

static PeerData *
_peers_get_by_public_key(NMSettingWireGuardPrivate *priv,
                         const char                *public_key,
                         gboolean                   try_with_normalized_key)
{
    gs_free char *public_key_normalized = NULL;
    PeerData     *pd;

again:
    nm_assert(priv);
    nm_assert(public_key);

    pd = g_hash_table_lookup(priv->peers_hash, &public_key);
    if (pd) {
        nm_assert(_peers_get(priv, pd->idx) == pd);
        return pd;
    }
    if (try_with_normalized_key
        && nm_utils_base64secret_normalize(public_key,
                                           NM_WIREGUARD_PUBLIC_KEY_LEN,
                                           &public_key_normalized)) {
        public_key              = public_key_normalized;
        try_with_normalized_key = FALSE;
        goto again;
    }
    return NULL;
}

static void
_peers_remove(NMSettingWireGuardPrivate *priv, PeerData *pd, gboolean do_free)
{
    guint i;

    nm_assert(pd);
    nm_assert(_peers_get(priv, pd->idx) == pd);

    for (i = pd->idx + 1; i < priv->peers_arr->len; i++)
        _peers_get(priv, i)->idx--;

    g_ptr_array_remove_index(priv->peers_arr, pd->idx);
    if (!g_hash_table_remove(priv->peers_hash, pd))
        nm_assert_not_reached();
    if (do_free)
        _peer_free(pd);
}

/**
 * nm_setting_wireguard_get_peers_len:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the number of registered peers.
 *
 * Since: 1.16
 */
guint
nm_setting_wireguard_get_peers_len(NMSettingWireGuard *self)
{
    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    return NM_SETTING_WIREGUARD_GET_PRIVATE(self)->peers_arr->len;
}

/**
 * nm_setting_wireguard_get_peer:
 * @self: the #NMSettingWireGuard instance
 * @idx: the index to lookup.
 *
 * Returns: (transfer none): the #NMWireGuardPeer entry at
 *   index @idx. If the index is out of range, %NULL is returned.
 *
 * Since: 1.16
 */
NMWireGuardPeer *
nm_setting_wireguard_get_peer(NMSettingWireGuard *self, guint idx)
{
    NMSettingWireGuardPrivate *priv;

    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), NULL);

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    if (idx >= priv->peers_arr->len)
        return NULL;

    return _peers_get(priv, idx)->peer;
}

/**
 * nm_setting_wireguard_get_peer_by_public_key:
 * @self: the #NMSettingWireGuard instance
 * @public_key: the public key for looking up the
 *   peer.
 * @out_idx: (out) (optional): optional output argument
 *   for the index of the found peer. If no index is found,
 *   this is set to the nm_setting_wireguard_get_peers_len().
 *
 * Returns: (transfer none) (nullable): the #NMWireGuardPeer instance with a
 *   matching public key. If no such peer exists, %NULL is returned.
 *
 * Since: 1.16
 */
NMWireGuardPeer *
nm_setting_wireguard_get_peer_by_public_key(NMSettingWireGuard *self,
                                            const char         *public_key,
                                            guint              *out_idx)
{
    NMSettingWireGuardPrivate *priv;
    PeerData                  *pd;

    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), NULL);
    g_return_val_if_fail(public_key, NULL);

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    pd = _peers_get_by_public_key(priv, public_key, TRUE);
    if (!pd) {
        NM_SET_OUT(out_idx, priv->peers_arr->len);
        return NULL;
    }
    NM_SET_OUT(out_idx, pd->idx);
    return pd->peer;
}

static gboolean
_peers_set(NMSettingWireGuardPrivate *priv,
           NMWireGuardPeer           *peer,
           guint                      idx,
           gboolean                   check_same_key)
{
    PeerData   *pd_same_key = NULL;
    PeerData   *pd_idx      = NULL;
    const char *public_key;

    nm_assert(idx <= priv->peers_arr->len);

    public_key = nm_wireguard_peer_get_public_key(peer);

    if (idx < priv->peers_arr->len) {
        pd_idx = _peers_get(priv, idx);

        if (pd_idx->peer == peer)
            return FALSE;

        if (check_same_key && nm_streq(public_key, nm_wireguard_peer_get_public_key(pd_idx->peer)))
            check_same_key = FALSE;
    }

    nm_wireguard_peer_seal(peer);
    nm_wireguard_peer_ref(peer);

    if (check_same_key) {
        pd_same_key = _peers_get_by_public_key(priv, public_key, FALSE);
        if (pd_same_key) {
            if (pd_idx) {
                nm_assert(pd_same_key != pd_idx);
                _peers_remove(priv, pd_same_key, TRUE);
                pd_same_key = NULL;
            } else {
                if (pd_same_key->peer == peer && pd_same_key->idx == priv->peers_arr->len - 1) {
                    nm_wireguard_peer_unref(peer);
                    return FALSE;
                }
                _peers_remove(priv, pd_same_key, FALSE);
                nm_wireguard_peer_unref(pd_same_key->peer);
            }
        }
    } else
        nm_assert(_peers_get_by_public_key(priv, public_key, FALSE) == pd_idx);

    if (pd_idx) {
        g_hash_table_remove(priv->peers_hash, pd_idx);
        nm_wireguard_peer_unref(pd_idx->peer);
        pd_idx->public_key = public_key;
        pd_idx->peer       = peer;
        g_hash_table_add(priv->peers_hash, pd_idx);
        return TRUE;
    }

    if (!pd_same_key)
        pd_same_key = g_slice_new(PeerData);

    *pd_same_key = (PeerData) {
        .peer       = peer,
        .public_key = public_key,
        .idx        = priv->peers_arr->len,
    };

    g_ptr_array_add(priv->peers_arr, pd_same_key);
    if (!g_hash_table_add(priv->peers_hash, pd_same_key))
        nm_assert_not_reached();

    nm_assert(_peers_get(priv, pd_same_key->idx) == pd_same_key);

    return TRUE;
}

static gboolean
_peers_append(NMSettingWireGuardPrivate *priv, NMWireGuardPeer *peer, gboolean check_same_key)
{
    return _peers_set(priv, peer, priv->peers_arr->len, check_same_key);
}

/**
 * nm_setting_wireguard_set_peer:
 * @self: the #NMSettingWireGuard instance
 * @peer: the #NMWireGuardPeer instance to set.
 *   This seals @peer and keeps a reference on the
 *   instance.
 * @idx: the index, in the range of 0 to the number of
 *   peers (including). That means, if @idx is one past
 *   the end of the number of peers, this is the same as
 *   nm_setting_wireguard_append_peer(). Otherwise, the
 *   peer at this index is replaced.
 *
 * If @idx is one past the last peer, the behavior is the same
 * as nm_setting_wireguard_append_peer().
 * Otherwise, the peer will be at @idx and replace the peer
 * instance at that index. Note that if a peer with the same
 * public-key exists on another index, then that peer will also
 * be replaced. In that case, the number of peers will shrink
 * by one (because the one at @idx got replace and then one
 * with the same public-key got removed). This also means,
 * that the resulting index afterwards may be one less than
 * @idx (if another peer with a lower index was dropped).
 *
 * Since: 1.16
 */
void
nm_setting_wireguard_set_peer(NMSettingWireGuard *self, NMWireGuardPeer *peer, guint idx)
{
    NMSettingWireGuardPrivate *priv;

    g_return_if_fail(NM_IS_SETTING_WIREGUARD(self));
    g_return_if_fail(NM_IS_WIREGUARD_PEER(peer, TRUE));

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    g_return_if_fail(idx <= priv->peers_arr->len);

    if (_peers_set(priv, peer, idx, TRUE))
        _peers_notify(self);
}

/**
 * nm_setting_wireguard_append_peer:
 * @self: the #NMSettingWireGuard instance
 * @peer: the #NMWireGuardPeer instance to append.
 *   This seals @peer and keeps a reference on the
 *   instance.
 *
 * If a peer with the same public-key already exists, that
 * one is replaced by @peer. The new @peer is always appended
 * (or moved to) the end, so in case a peer is replaced, the
 * indexes are shifted and the number of peers stays unchanged.
 *
 * Since: 1.16
 */
void
nm_setting_wireguard_append_peer(NMSettingWireGuard *self, NMWireGuardPeer *peer)
{
    g_return_if_fail(NM_IS_SETTING_WIREGUARD(self));
    g_return_if_fail(NM_IS_WIREGUARD_PEER(peer, TRUE));

    if (_peers_append(NM_SETTING_WIREGUARD_GET_PRIVATE(self), peer, TRUE))
        _peers_notify(self);
}

/**
 * nm_setting_wireguard_remove_peer
 * @self: the #NMSettingWireGuard instance
 * @idx: the index to remove.
 *
 * Returns: %TRUE if @idx was in range and a peer
 *   was removed. Otherwise, @self is unchanged.
 *
 * Since: 1.16
 */
gboolean
nm_setting_wireguard_remove_peer(NMSettingWireGuard *self, guint idx)
{
    NMSettingWireGuardPrivate *priv;

    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), FALSE);

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    if (idx >= priv->peers_arr->len)
        return FALSE;

    _peers_remove(priv, _peers_get(priv, idx), TRUE);
    _peers_notify(self);
    return TRUE;
}

static guint
_peers_clear(NMSettingWireGuardPrivate *priv)
{
    guint l;

    l = priv->peers_arr->len;
    while (priv->peers_arr->len > 0) {
        _peers_remove(priv, _peers_get(priv, priv->peers_arr->len - 1), TRUE);
    }
    return l;
}

/**
 * nm_setting_wireguard_clear_peers:
 * @self: the #NMSettingWireGuard instance
 *
 * Returns: the number of cleared peers.
 *
 * Since: 1.16
 */
guint
nm_setting_wireguard_clear_peers(NMSettingWireGuard *self)
{
    guint l;

    g_return_val_if_fail(NM_IS_SETTING_WIREGUARD(self), 0);

    l = _peers_clear(NM_SETTING_WIREGUARD_GET_PRIVATE(self));
    if (l > 0)
        _peers_notify(self);
    return l;
}

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

static GVariant *
peers_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
{
    NMSettingWireGuard        *self = NM_SETTING_WIREGUARD(setting);
    NMSettingWireGuardPrivate *priv;
    gboolean                   any_peers = FALSE;
    GVariantBuilder            peers_builder;
    guint                      i_peer, n_peers;
    guint                      i;

    n_peers = nm_setting_wireguard_get_peers_len(self);
    if (n_peers == 0)
        return NULL;

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    for (i_peer = 0; i_peer < n_peers; i_peer++) {
        const NMWireGuardPeer *peer = _peers_get(priv, i_peer)->peer;
        GVariantBuilder        builder;
        gboolean               has_secrets = FALSE;

        if (!peer->public_key)
            continue;

        g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));

        g_variant_builder_add(&builder,
                              "{sv}",
                              NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY,
                              g_variant_new_string(peer->public_key));

        if (_nm_connection_serialize_non_secret(flags) && peer->endpoint)
            g_variant_builder_add(
                &builder,
                "{sv}",
                NM_WIREGUARD_PEER_ATTR_ENDPOINT,
                g_variant_new_string(nm_sock_addr_endpoint_get_endpoint(peer->endpoint)));

        if (_nm_connection_serialize_secrets(flags, peer->preshared_key_flags)
            && peer->preshared_key) {
            g_variant_builder_add(&builder,
                                  "{sv}",
                                  NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY,
                                  g_variant_new_string(peer->preshared_key));
            has_secrets = TRUE;
        }

        if (_nm_connection_serialize_non_secret(flags)
            && peer->preshared_key_flags != NM_SETTING_SECRET_FLAG_NOT_REQUIRED)
            g_variant_builder_add(&builder,
                                  "{sv}",
                                  NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY_FLAGS,
                                  g_variant_new_uint32(peer->preshared_key_flags));

        if (_nm_connection_serialize_non_secret(flags) && peer->persistent_keepalive != 0)
            g_variant_builder_add(&builder,
                                  "{sv}",
                                  NM_WIREGUARD_PEER_ATTR_PERSISTENT_KEEPALIVE,
                                  g_variant_new_uint32(peer->persistent_keepalive));

        if (_nm_connection_serialize_non_secret(flags) && peer->allowed_ips
            && peer->allowed_ips->len > 0) {
            const char *const   *strv       = (const char *const *) peer->allowed_ips->pdata;
            gs_free const char **strv_fixed = NULL;

            for (i = 0; i < peer->allowed_ips->len; i++) {
                /* This is a bit odd. The original peer->allowed_ips is a strv array
                 * of all allowed-ips, but some of them may have an ALLOWED_IP_INVALID_X prefix
                 * (as first character).
                 *
                 * When we serialize the array to GVariant, we need to get rid of this
                 * character, by pointing to the second character.
                 *
                 * In that case, first clone the strv array to strv_fixed. Once we cloned
                 * it, adjust the pointer to drop the first character. */
                if (strv[i][0] != ALLOWED_IP_INVALID_X) {
                    /* this string is valid, no need for adjustment. */
                    continue;
                }
                if (!strv_fixed) {
                    strv_fixed = nm_memdup(strv, sizeof(strv[0]) * peer->allowed_ips->len);
                    strv       = strv_fixed;
                }
                ((const char **) strv)[i]++;
            }
            g_variant_builder_add(&builder,
                                  "{sv}",
                                  NM_WIREGUARD_PEER_ATTR_ALLOWED_IPS,
                                  g_variant_new_strv(strv, peer->allowed_ips->len));
        }

        if (NM_FLAGS_ANY(flags,
                         NM_CONNECTION_SERIALIZE_ONLY_SECRETS
                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED
                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_SYSTEM_OWNED
                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_NOT_SAVED)
            && !_nm_connection_serialize_non_secret(flags)) {
            /* The flags indicate that only secrets must be serialized and this
             * peer doesn't contain any. Skip the peer. */
            if (!has_secrets) {
                g_variant_builder_clear(&builder);
                continue;
            }
        }

        if (!any_peers) {
            g_variant_builder_init(&peers_builder, G_VARIANT_TYPE("aa{sv}"));
            any_peers = TRUE;
        }
        g_variant_builder_add(&peers_builder, "a{sv}", &builder);
    }

    return any_peers ? g_variant_builder_end(&peers_builder) : NULL;
}

static gboolean
peers_from_dbus(_NM_SETT_INFO_PROP_FROM_DBUS_FCN_ARGS _nm_nil)
{
    GVariantIter iter_peers;
    GVariant    *peer_var;
    guint        i_peer;
    gboolean     success       = FALSE;
    gboolean     peers_changed = FALSE;

    nm_assert(g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")));

    g_variant_iter_init(&iter_peers, value);

    i_peer = 0;
    while (g_variant_iter_next(&iter_peers, "@a{sv}", &peer_var)) {
        _nm_unused gs_unref_variant GVariant *peer_var_unref = peer_var;
        nm_auto_unref_wgpeer NMWireGuardPeer *peer           = NULL;
        const char                           *cstr;
        guint32                               u32;
        GVariant                             *var;

        i_peer++;

        if (!g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY, "&s", &cstr)) {
            if (NM_FLAGS_HAS(parse_flags, NM_SETTING_PARSE_FLAGS_STRICT)) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_MISSING_PROPERTY,
                            _("peer #%u has no public-key"),
                            i_peer);
                goto out;
            }
            continue;
        }

        peer = nm_wireguard_peer_new();
        if (!nm_wireguard_peer_set_public_key(peer, cstr, TRUE)) {
            if (NM_FLAGS_HAS(parse_flags, NM_SETTING_PARSE_FLAGS_STRICT)) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_MISSING_PROPERTY,
                            _("peer #%u has invalid public-key"),
                            i_peer);
                goto out;
            }
            continue;
        }

        if (g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_ENDPOINT, "&s", &cstr)) {
            nm_auto_unref_sockaddrendpoint NMSockAddrEndpoint *ep = NULL;

            ep = nm_sock_addr_endpoint_new(cstr);
            if (!nm_sock_addr_endpoint_get_host(ep)) {
                if (NM_FLAGS_HAS(parse_flags, NM_SETTING_PARSE_FLAGS_STRICT)) {
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_MISSING_PROPERTY,
                                _("peer #%u has invalid endpoint"),
                                i_peer);
                    goto out;
                }
            } else
                _nm_wireguard_peer_set_endpoint(peer, ep);
        }

        if (g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, "&s", &cstr))
            nm_wireguard_peer_set_preshared_key(peer, cstr, TRUE);

        if (g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY_FLAGS, "u", &u32))
            nm_wireguard_peer_set_preshared_key_flags(peer, u32);

        if (g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PERSISTENT_KEEPALIVE, "u", &u32))
            nm_wireguard_peer_set_persistent_keepalive(peer, u32);

        if (g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_ALLOWED_IPS, "@as", &var)) {
            _nm_unused gs_unref_variant GVariant *var_free    = var;
            gs_free const char                  **allowed_ips = NULL;
            gsize                                 i, l;

            allowed_ips = g_variant_get_strv(var, &l);
            if (allowed_ips) {
                for (i = 0; i < l; i++) {
                    if (_peer_append_allowed_ip(peer, allowed_ips[i], FALSE))
                        continue;
                    if (!NM_FLAGS_HAS(parse_flags, NM_SETTING_PARSE_FLAGS_STRICT))
                        continue;
                    g_set_error(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_MISSING_PROPERTY,
                                _("peer #%u has invalid allowed-ips setting"),
                                i_peer);
                    goto out;
                }
            }
        }

        if (NM_FLAGS_HAS(parse_flags, NM_SETTING_PARSE_FLAGS_STRICT)) {
            gs_free_error GError *local = NULL;

            if (!nm_wireguard_peer_is_valid(peer, TRUE, FALSE, &local)) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_MISSING_PROPERTY,
                            _("peer #%u is invalid: %s"),
                            i_peer,
                            local->message);
                goto out;
            }
        }

        /* we could easily reject duplicate peers (by public-key) or duplicate GVariant attributes.
         * However, don't do that. In case of duplicate values, the latter peer overwrite the earlier
         * and GVariant attributes are ignored by g_variant_lookup() above. */
        if (_peers_append(NM_SETTING_WIREGUARD_GET_PRIVATE(setting), peer, TRUE))
            peers_changed = TRUE;
    }

    success = TRUE;

out:
    *out_is_modified = peers_changed;
    if (peers_changed)
        _peers_notify(setting);
    return success;
}

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

static gboolean
verify(NMSetting *setting, NMConnection *connection, GError **error)
{
    NMSettingWireGuard        *s_wg = NM_SETTING_WIREGUARD(setting);
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
    guint                      i;

    if (!_nm_connection_verify_required_interface_name(connection, error))
        return FALSE;

    if (!_nm_utils_secret_flags_validate(nm_setting_wireguard_get_private_key_flags(s_wg),
                                         NM_SETTING_WIREGUARD_SETTING_NAME,
                                         NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS,
                                         NM_SETTING_SECRET_FLAG_NOT_REQUIRED,
                                         error))
        return FALSE;

    for (i = 0; i < priv->peers_arr->len; i++) {
        NMWireGuardPeer *peer = _peers_get(priv, i)->peer;

        if (!nm_wireguard_peer_is_valid(peer, TRUE, FALSE, error)) {
            g_prefix_error(error,
                           "%s.%s[%u]: ",
                           NM_SETTING_WIREGUARD_SETTING_NAME,
                           NM_SETTING_WIREGUARD_PEERS,
                           i);
            return FALSE;
        }
    }

    if (connection) {
        NMSettingIPConfig *s_ip4;
        NMSettingIPConfig *s_ip6;
        const char        *method;

        /* WireGuard is Layer 3 only. For the moment, we only support a restricted set of
         * IP methods. We may relax that later, once we fix the implementations so they
         * actually work. */

        if ((s_ip4 = nm_connection_get_setting_ip4_config(connection))
            && (method = nm_setting_ip_config_get_method(s_ip4))
            && !NM_IN_STRSET(method,
                             NM_SETTING_IP4_CONFIG_METHOD_DISABLED,
                             NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
                        _("method \"%s\" is not supported for WireGuard"),
                        method);
            g_prefix_error(error,
                           "%s.%s: ",
                           NM_SETTING_IP4_CONFIG_SETTING_NAME,
                           NM_SETTING_IP_CONFIG_METHOD);
            return FALSE;
        }

        if ((s_ip6 = nm_connection_get_setting_ip6_config(connection))
            && (method = nm_setting_ip_config_get_method(s_ip6))
            && !NM_IN_STRSET(method,
                             NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
                             NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL,
                             NM_SETTING_IP6_CONFIG_METHOD_MANUAL,
                             NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) {
            g_set_error(error,
                        NM_CONNECTION_ERROR,
                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
                        _("method \"%s\" is not supported for WireGuard"),
                        method);
            g_prefix_error(error,
                           "%s.%s: ",
                           NM_SETTING_IP6_CONFIG_SETTING_NAME,
                           NM_SETTING_IP_CONFIG_METHOD);
            return FALSE;
        }
    }

    /* private-key is a secret, hence we cannot verify it like a regular property. */
    return TRUE;
}

static gboolean
verify_secrets(NMSetting *setting, NMConnection *connection, GError **error)
{
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
    guint                      i;

    if (priv->private_key && !priv->private_key_valid) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
                            _("key must be 32 bytes base64 encoded"));
        g_prefix_error(error,
                       "%s.%s: ",
                       NM_SETTING_WIREGUARD_SETTING_NAME,
                       NM_SETTING_WIREGUARD_PRIVATE_KEY);
        return FALSE;
    }

    for (i = 0; i < priv->peers_arr->len; i++) {
        NMWireGuardPeer *peer = _peers_get(priv, i)->peer;

        if (!nm_wireguard_peer_is_valid(peer, FALSE, TRUE, error)) {
            g_prefix_error(error,
                           "%s.%s[%u]: ",
                           NM_SETTING_WIREGUARD_SETTING_NAME,
                           NM_SETTING_WIREGUARD_PEERS,
                           i);
            return FALSE;
        }
    }

    return TRUE;
}

static GPtrArray *
need_secrets(NMSetting *setting, gboolean check_rerequest)
{
    NMSettingWireGuardPrivate *priv    = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
    GPtrArray                 *secrets = NULL;
    guint                      i;

    if (check_rerequest || !priv->private_key_valid) {
        secrets = g_ptr_array_new_full(1, g_free);
        g_ptr_array_add(secrets, g_strdup(NM_SETTING_WIREGUARD_PRIVATE_KEY));
    }

    for (i = 0; i < priv->peers_arr->len; i++) {
        NMWireGuardPeer *peer = _peers_get(priv, i)->peer;

        if (NM_FLAGS_HAS(peer->preshared_key_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED))
            continue;

        if (peer->preshared_key_valid)
            continue;

        if (!peer->public_key_valid)
            continue;

        if (!secrets)
            secrets = g_ptr_array_new_full(1, g_free);
        g_ptr_array_add(secrets, peers_psk_get_secret_name_dup(peer->public_key));
    }

    return secrets;
}

static gboolean
clear_secrets(const NMSettInfoSetting         *sett_info,
              const NMSettInfoProperty        *property_info,
              NMSetting                       *setting,
              NMSettingClearSecretsWithFlagsFn func,
              gpointer                         user_data)
{
    if (nm_streq(property_info->name, NM_SETTING_WIREGUARD_PEERS)) {
        NMSettingWireGuardPrivate *priv          = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
        gboolean                   peers_changed = FALSE;
        guint                      i, j;

        j = 0;
        for (i = 0; i < priv->peers_arr->len; i++) {
            NMWireGuardPeer *peer = _peers_get(priv, i)->peer;

            if (!peer->preshared_key)
                continue;

            if (func) {
                gs_free char *name_free = NULL;
                const char   *name;

                if (j++ < 5) {
                    /* we use alloca() inside a loop here, but it is guarded to happen at most
                     * a few times. */
                    name = peers_psk_get_secret_name_a(peer->public_key, &name_free);
                } else {
                    name_free = peers_psk_get_secret_name_dup(peer->public_key);
                    name      = name_free;
                }

                if (!func(setting, name, peer->preshared_key_flags, user_data))
                    continue;
            }

            {
                nm_auto_unref_wgpeer NMWireGuardPeer *peer2 = NULL;

                peer2 = nm_wireguard_peer_new_clone(peer, FALSE);

                if (_peers_set(priv, peer2, i, FALSE))
                    peers_changed = TRUE;
            }
        }

        if (peers_changed)
            _peers_notify(setting);
        return peers_changed;
    }

    return NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
        ->clear_secrets(sett_info, property_info, setting, func, user_data);
}

static int
update_one_secret(NMSetting *setting, const char *key, GVariant *value, GError **error)
{
    NMSettingWireGuard        *self = NM_SETTING_WIREGUARD(setting);
    NMSettingWireGuardPrivate *priv;
    gboolean                   has_changes = FALSE;
    gboolean                   has_error   = FALSE;
    GVariantIter               iter_peers;
    GVariant                  *peer_var;
    guint                      i_peer;

    if (!nm_streq(key, NM_SETTING_WIREGUARD_PEERS)) {
        return NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
            ->update_one_secret(setting, key, value, error);
    }

    if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}"))) {
        g_set_error_literal(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET,
                            _("invalid peer secrets"));
        g_prefix_error(error,
                       "%s.%s: ",
                       NM_SETTING_WIREGUARD_SETTING_NAME,
                       NM_SETTING_WIREGUARD_PEERS);
        return NM_SETTING_UPDATE_SECRET_ERROR;
    }

    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(self);

    g_variant_iter_init(&iter_peers, value);

    i_peer = 0;
    while (g_variant_iter_next(&iter_peers, "@a{sv}", &peer_var)) {
        _nm_unused gs_unref_variant GVariant *peer_var_unref = peer_var;
        nm_auto_unref_wgpeer NMWireGuardPeer *peer           = NULL;
        PeerData                             *pd;
        const char                           *cstr;

        i_peer++;

        if (!g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY, "&s", &cstr)) {
            if (!has_error) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET,
                            _("peer #%u lacks public-key"),
                            i_peer - 1);
                g_prefix_error(error,
                               "%s.%s: ",
                               NM_SETTING_WIREGUARD_SETTING_NAME,
                               NM_SETTING_WIREGUARD_PEERS);
                has_error = TRUE;
            }
            continue;
        }

        pd = _peers_get_by_public_key(priv, cstr, TRUE);
        if (!pd) {
            if (!has_error) {
                g_set_error(error,
                            NM_CONNECTION_ERROR,
                            NM_CONNECTION_ERROR_PROPERTY_NOT_SECRET,
                            _("non-existing peer '%s'"),
                            cstr);
                g_prefix_error(error,
                               "%s.%s: ",
                               NM_SETTING_WIREGUARD_SETTING_NAME,
                               NM_SETTING_WIREGUARD_PEERS);
                has_error = TRUE;
            }
            continue;
        }

        if (!g_variant_lookup(peer_var, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, "&s", &cstr)) {
            /* no preshared-key. Ignore the rest.
             *
             * In particular, we don't reject all unknown fields. */
            continue;
        }

        if (nm_streq0(cstr, nm_wireguard_peer_get_preshared_key(pd->peer)))
            continue;

        peer = nm_wireguard_peer_new_clone(pd->peer, FALSE);
        nm_wireguard_peer_set_preshared_key(peer, cstr, TRUE);

        if (!_peers_set(priv, peer, pd->idx, FALSE))
            nm_assert_not_reached();
        has_changes = TRUE;
    }

    if (has_error)
        return NM_SETTING_UPDATE_SECRET_ERROR;
    if (has_changes)
        return NM_SETTING_UPDATE_SECRET_SUCCESS_MODIFIED;
    return NM_SETTING_UPDATE_SECRET_SUCCESS_UNCHANGED;
}

static NMTernary
compare_fcn_peers(_NM_SETT_INFO_PROP_COMPARE_FCN_ARGS _nm_nil)
{
    NMSettingWireGuardPrivate *a_priv;
    NMSettingWireGuardPrivate *b_priv;
    guint                      i;

    if (NM_FLAGS_HAS(flags, NM_SETTING_COMPARE_FLAG_INFERRABLE))
        return NM_TERNARY_DEFAULT;

    if (!set_b)
        return TRUE;

    a_priv = NM_SETTING_WIREGUARD_GET_PRIVATE(set_a);
    b_priv = NM_SETTING_WIREGUARD_GET_PRIVATE(set_b);

    if (a_priv->peers_arr->len != b_priv->peers_arr->len)
        return FALSE;
    for (i = 0; i < a_priv->peers_arr->len; i++) {
        NMWireGuardPeer *a_peer = _peers_get(a_priv, i)->peer;
        NMWireGuardPeer *b_peer = _peers_get(b_priv, i)->peer;

        if (nm_wireguard_peer_cmp(a_peer, b_peer, flags) != 0)
            return FALSE;
    }

    return TRUE;
}

static void
duplicate_copy_properties(const NMSettInfoSetting *sett_info, NMSetting *src, NMSetting *dst)
{
    NMSettingWireGuardPrivate *priv_src = NM_SETTING_WIREGUARD_GET_PRIVATE(src);
    NMSettingWireGuardPrivate *priv_dst = NM_SETTING_WIREGUARD_GET_PRIVATE(dst);
    guint                      i;
    gboolean                   peers_changed = FALSE;

    NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
        ->duplicate_copy_properties(sett_info, src, dst);

    /* We don't bother comparing the existing peers with what we are about to set.
     * Always reset all. */
    if (_peers_clear(priv_dst) > 0)
        peers_changed = TRUE;
    for (i = 0; i < priv_src->peers_arr->len; i++) {
        if (_peers_append(priv_dst, _peers_get(priv_src, i)->peer, FALSE))
            peers_changed = TRUE;
    }
    if (peers_changed)
        _peers_notify(dst);
}

static void
enumerate_values(const NMSettInfoProperty *property_info,
                 NMSetting                *setting,
                 NMSettingValueIterFn      func,
                 gpointer                  user_data)
{
    if (nm_streq(property_info->name, NM_SETTING_WIREGUARD_PEERS)) {
        NMSettingWireGuardPrivate  *priv  = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
        nm_auto_unset_gvalue GValue value = G_VALUE_INIT;
        GPtrArray                  *ptr   = NULL;
        guint                       i;

        if (priv->peers_arr && priv->peers_arr->len > 0) {
            ptr = g_ptr_array_new_with_free_func((GDestroyNotify) nm_wireguard_peer_unref);
            for (i = 0; i < priv->peers_arr->len; i++)
                g_ptr_array_add(ptr, nm_wireguard_peer_ref(_peers_get(priv, i)->peer));
        }
        g_value_init(&value, G_TYPE_PTR_ARRAY);
        g_value_take_boxed(&value, ptr);
        func(setting, property_info->name, &value, 0, user_data);
        return;
    }

    NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
        ->enumerate_values(property_info, setting, func, user_data);
}

static gboolean
aggregate(NMSetting *setting, int type_i, gpointer arg)
{
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
    NMConnectionAggregateType  type = type_i;
    NMSettingSecretFlags       secret_flags;
    guint                      i;

    nm_assert(NM_IN_SET(type,
                        NM_CONNECTION_AGGREGATE_ANY_SECRETS,
                        NM_CONNECTION_AGGREGATE_ANY_SYSTEM_SECRET_FLAGS));

    switch (type) {
    case NM_CONNECTION_AGGREGATE_ANY_SECRETS:
        if (priv->private_key)
            goto out_done;
        for (i = 0; i < priv->peers_arr->len; i++) {
            if (nm_wireguard_peer_get_preshared_key(_peers_get(priv, i)->peer))
                goto out_done;
        }
        break;

    case NM_CONNECTION_AGGREGATE_ANY_SYSTEM_SECRET_FLAGS:
#if NM_MORE_ASSERTS
        if (!nm_setting_get_secret_flags(setting,
                                         NM_SETTING_WIREGUARD_PRIVATE_KEY,
                                         &secret_flags,
                                         NULL))
            nm_assert_not_reached();
        nm_assert(secret_flags == priv->private_key_flags);
#endif
        if (priv->private_key_flags == NM_SETTING_SECRET_FLAG_NONE)
            goto out_done;
        for (i = 0; i < priv->peers_arr->len; i++) {
            secret_flags = nm_wireguard_peer_get_preshared_key_flags(_peers_get(priv, i)->peer);
            if (secret_flags == NM_SETTING_SECRET_FLAG_NONE)
                goto out_done;
        }
        break;
    }

    return FALSE;

out_done:
    *((gboolean *) arg) = TRUE;
    return TRUE;
}

static gboolean
get_secret_flags(NMSetting            *setting,
                 const char           *secret_name,
                 NMSettingSecretFlags *out_flags,
                 GError              **error)
{
    if (NM_STR_HAS_PREFIX(secret_name, NM_SETTING_WIREGUARD_PEERS ".")) {
        NMSettingWireGuardPrivate *priv            = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
        gs_free char              *public_key_free = NULL;
        const char                *public_key;
        PeerData                  *pd;

        public_key = peers_psk_get_secret_parse_a(secret_name, &public_key_free);
        if (public_key && (pd = _peers_get_by_public_key(priv, public_key, FALSE))) {
            NM_SET_OUT(out_flags, nm_wireguard_peer_get_preshared_key_flags(pd->peer));
            return TRUE;
        }
    }

    return NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
        ->get_secret_flags(setting, secret_name, out_flags, error);
}

static gboolean
set_secret_flags(NMSetting           *setting,
                 const char          *secret_name,
                 NMSettingSecretFlags flags,
                 GError             **error)
{
    if (NM_STR_HAS_PREFIX(secret_name, NM_SETTING_WIREGUARD_PEERS ".")) {
        NMSettingWireGuard        *self            = NM_SETTING_WIREGUARD(setting);
        NMSettingWireGuardPrivate *priv            = NM_SETTING_WIREGUARD_GET_PRIVATE(self);
        gs_free char              *public_key_free = NULL;
        const char                *public_key;
        PeerData                  *pd;

        public_key = peers_psk_get_secret_parse_a(secret_name, &public_key_free);
        if (public_key && (pd = _peers_get_by_public_key(priv, public_key, FALSE))) {
            if (nm_wireguard_peer_get_preshared_key_flags(pd->peer) != flags) {
                nm_auto_unref_wgpeer NMWireGuardPeer *peer = NULL;

                peer                      = nm_wireguard_peer_new_clone(pd->peer, TRUE);
                peer->preshared_key_flags = flags;
                if (_peers_set(priv, peer, pd->idx, FALSE))
                    _peers_notify(self);
            }

            return TRUE;
        }
    }

    return NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
        ->set_secret_flags(setting, secret_name, flags, error);
}

static void
for_each_secret(NMSetting                     *setting,
                const char                    *data_key,
                GVariant                      *data_val,
                gboolean                       remove_non_secrets,
                _NMConnectionForEachSecretFunc callback,
                gpointer                       callback_data,
                GVariantBuilder               *setting_builder)
{
    NMSettingWireGuard        *s_wg;
    NMSettingWireGuardPrivate *priv;
    GVariantBuilder            peers_builder;
    GVariantIter              *peer_iter;
    GVariantIter               data_iter;
    const char                *key;

    if (!nm_streq(data_key, NM_SETTING_WIREGUARD_PEERS)) {
        NM_SETTING_CLASS(nm_setting_wireguard_parent_class)
            ->for_each_secret(setting,
                              data_key,
                              data_val,
                              remove_non_secrets,
                              callback,
                              callback_data,
                              setting_builder);
        return;
    }

    if (!g_variant_is_of_type(data_val, G_VARIANT_TYPE("aa{sv}"))) {
        /* invalid type. Silently ignore content as we cannot find secret-keys
         * here. */
        return;
    }

    s_wg = NM_SETTING_WIREGUARD(setting);
    priv = NM_SETTING_WIREGUARD_GET_PRIVATE(s_wg);

    g_variant_builder_init(&peers_builder, G_VARIANT_TYPE("aa{sv}"));
    g_variant_iter_init(&data_iter, data_val);
    while (g_variant_iter_next(&data_iter, "a{sv}", &peer_iter)) {
        _nm_unused nm_auto_free_variant_iter GVariantIter *peer_iter_free = peer_iter;
        gs_unref_variant GVariant                         *preshared_key  = NULL;
        PeerData                                          *pd             = NULL;
        NMSettingSecretFlags                               secret_flags;
        GVariant                                          *val;
        GVariantBuilder                                    peer_builder;

        g_variant_builder_init(&peer_builder, G_VARIANT_TYPE("a{sv}"));

        while (g_variant_iter_next(peer_iter, "{&sv}", &key, &val)) {
            _nm_unused gs_unref_variant GVariant *val_free = val;

            if (nm_streq(key, NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)) {
                if (!preshared_key && g_variant_is_of_type(val, G_VARIANT_TYPE_STRING))
                    preshared_key = g_variant_ref(val);
                continue;
            }

            if (nm_streq(key, NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY)) {
                if (!pd && g_variant_is_of_type(val, G_VARIANT_TYPE_STRING))
                    pd = _peers_get_by_public_key(priv, g_variant_get_string(val, NULL), TRUE);
            } else if (remove_non_secrets)
                continue;

            g_variant_builder_add(&peer_builder, "{sv}", key, val);
        }

        if (pd && preshared_key) {
            /* without specifying a public-key of an existing peer, the secret is
             * ignored. */
            secret_flags = nm_wireguard_peer_get_preshared_key_flags(pd->peer);
            if (callback(secret_flags, callback_data))
                g_variant_builder_add(&peer_builder,
                                      "{sv}",
                                      NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY,
                                      preshared_key);
        }

        g_variant_builder_add(&peers_builder, "a{sv}", &peer_builder);
    }

    g_variant_builder_add(setting_builder,
                          "{sv}",
                          NM_SETTING_WIREGUARD_PEERS,
                          g_variant_builder_end(&peers_builder));
}

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

static gboolean
_set_string_fcn_public_key(const NMSettInfoSetting  *sett_info,
                           const NMSettInfoProperty *property_info,
                           NMSetting                *setting,
                           const char               *str)
{
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);
    gboolean                   valid;
    char *new = NULL;
    char *old;

    if (str)
        valid = nm_utils_base64secret_normalize(str, NM_WIREGUARD_PUBLIC_KEY_LEN, &new);
    else
        valid = FALSE;

    if (nm_streq0(new ?: str, priv->private_key)) {
        nm_assert(priv->private_key_valid == valid);
        nm_free_secret(new);
        return FALSE;
    }

    priv->private_key_valid = valid;

    old               = priv->private_key;
    priv->private_key = new ?: g_strdup(str);
    nm_free_secret(old);

    return TRUE;
}

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

static void
nm_setting_wireguard_init(NMSettingWireGuard *setting)
{
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(setting);

    priv->peers_arr  = g_ptr_array_new();
    priv->peers_hash = g_hash_table_new(nm_pstr_hash, nm_pstr_equal);
}

/**
 * nm_setting_wireguard_new:
 *
 * Creates a new #NMSettingWireGuard object with default values.
 *
 * Returns: (transfer full): the new empty #NMSettingWireGuard object
 *
 * Since: 1.16
 **/
NMSetting *
nm_setting_wireguard_new(void)
{
    return g_object_new(NM_TYPE_SETTING_WIREGUARD, NULL);
}

static void
finalize(GObject *object)
{
    NMSettingWireGuardPrivate *priv = NM_SETTING_WIREGUARD_GET_PRIVATE(object);

    _peers_clear(priv);
    g_ptr_array_unref(priv->peers_arr);
    g_hash_table_unref(priv->peers_hash);

    G_OBJECT_CLASS(nm_setting_wireguard_parent_class)->finalize(object);
}

static void
nm_setting_wireguard_class_init(NMSettingWireGuardClass *klass)
{
    GObjectClass   *object_class        = G_OBJECT_CLASS(klass);
    NMSettingClass *setting_class       = NM_SETTING_CLASS(klass);
    GArray         *properties_override = _nm_sett_info_property_override_create_array();

    object_class->get_property = _nm_setting_property_get_property_direct;
    object_class->set_property = _nm_setting_property_set_property_direct;
    object_class->finalize     = finalize;

    setting_class->verify                    = verify;
    setting_class->verify_secrets            = verify_secrets;
    setting_class->need_secrets              = need_secrets;
    setting_class->clear_secrets             = clear_secrets;
    setting_class->update_one_secret         = update_one_secret;
    setting_class->duplicate_copy_properties = duplicate_copy_properties;
    setting_class->enumerate_values          = enumerate_values;
    setting_class->aggregate                 = aggregate;
    setting_class->get_secret_flags          = get_secret_flags;
    setting_class->set_secret_flags          = set_secret_flags;
    setting_class->for_each_secret           = for_each_secret;

    /**
     * NMSettingWireGuard:private-key:
     *
     * The 256 bit private-key in base64 encoding.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_string(properties_override,
                                              obj_properties,
                                              NM_SETTING_WIREGUARD_PRIVATE_KEY,
                                              PROP_PRIVATE_KEY,
                                              NM_SETTING_PARAM_SECRET,
                                              NMSettingWireGuard,
                                              _priv.private_key,
                                              .direct_data.set_string = _set_string_fcn_public_key,
                                              .direct_string_allow_empty = TRUE);

    /**
     * NMSettingWireGuard:private-key-flags:
     *
     * Flags indicating how to handle the #NMSettingWirelessSecurity:private-key
     * property.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_secret_flags(properties_override,
                                                    obj_properties,
                                                    NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS,
                                                    PROP_PRIVATE_KEY_FLAGS,
                                                    NMSettingWireGuard,
                                                    _priv.private_key_flags);

    /**
     * NMSettingWireGuard:fwmark:
     *
     * The use of fwmark is optional and is by default off. Setting it to 0
     * disables it. Otherwise, it is a 32-bit fwmark for outgoing packets.
     *
     * Note that "ip4-auto-default-route" or "ip6-auto-default-route" enabled,
     * implies to automatically choose a fwmark.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_uint32(properties_override,
                                              obj_properties,
                                              NM_SETTING_WIREGUARD_FWMARK,
                                              PROP_FWMARK,
                                              0,
                                              G_MAXUINT32,
                                              0,
                                              NM_SETTING_PARAM_INFERRABLE,
                                              NMSettingWireGuard,
                                              _priv.fwmark);

    /**
     * NMSettingWireGuard:listen-port:
     *
     * The listen-port. If listen-port is not specified, the port will be chosen
     * randomly when the interface comes up.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_uint32(properties_override,
                                              obj_properties,
                                              NM_SETTING_WIREGUARD_LISTEN_PORT,
                                              PROP_LISTEN_PORT,
                                              0,
                                              65535,
                                              0,
                                              NM_SETTING_PARAM_INFERRABLE,
                                              NMSettingWireGuard,
                                              _priv.listen_port);

    /**
     * NMSettingWireGuard:peer-routes:
     *
     * Whether to automatically add routes for the AllowedIPs ranges
     * of the peers. If %TRUE (the default), NetworkManager will automatically
     * add routes in the routing tables according to ipv4.route-table and
     * ipv6.route-table. Usually you want this automatism enabled.
     * If %FALSE, no such routes are added automatically. In this case, the
     * user may want to configure static routes in ipv4.routes and ipv6.routes,
     * respectively.
     *
     * Note that if the peer's AllowedIPs is "0.0.0.0/0" or "::/0" and the profile's
     * ipv4.never-default or ipv6.never-default setting is enabled, the peer route for
     * this peer won't be added automatically.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_boolean(properties_override,
                                               obj_properties,
                                               NM_SETTING_WIREGUARD_PEER_ROUTES,
                                               PROP_PEER_ROUTES,
                                               TRUE,
                                               NM_SETTING_PARAM_INFERRABLE,
                                               NMSettingWireGuard,
                                               _priv.peer_routes);

    /**
     * NMSettingWireGuard:mtu:
     *
     * If non-zero, only transmit packets of the specified size or smaller,
     * breaking larger packets up into multiple fragments.
     *
     * If zero a default MTU is used. Note that contrary to wg-quick's MTU
     * setting, this does not take into account the current routes at the
     * time of activation.
     *
     * Since: 1.16
     **/
    _nm_setting_property_define_direct_uint32(properties_override,
                                              obj_properties,
                                              NM_SETTING_WIREGUARD_MTU,
                                              PROP_MTU,
                                              0,
                                              G_MAXUINT32,
                                              0,
                                              NM_SETTING_PARAM_INFERRABLE,
                                              NMSettingWireGuard,
                                              _priv.mtu);

    /**
     * NMSettingWireGuard:ip4-auto-default-route:
     *
     * Whether to enable special handling of the IPv4 default route.
     * If enabled, the IPv4 default route from wireguard.peer-routes
     * will be placed to a dedicated routing-table and two policy routing rules
     * will be added. The fwmark number is also used as routing-table for the default-route,
     * and if fwmark is zero, an unused fwmark/table is chosen automatically.
     * This corresponds to what wg-quick does with Table=auto and what WireGuard
     * calls "Improved Rule-based Routing".
     *
     * Note that for this automatism to work, you usually don't want to set
     * ipv4.gateway, because that will result in a conflicting default route.
     *
     * Leaving this at the default will enable this option automatically
     * if ipv4.never-default is not set and there are any peers that use
     * a default-route as allowed-ips. Since this automatism only makes
     * sense if you also have a peer with an /0 allowed-ips, it is usually
     * not necessary to enable this explicitly. However, you can disable
     * it if you want to configure your own routing and rules.
     *
     * Since: 1.20
     **/
    _nm_setting_property_define_direct_ternary_enum(properties_override,
                                                    obj_properties,
                                                    NM_SETTING_WIREGUARD_IP4_AUTO_DEFAULT_ROUTE,
                                                    PROP_IP4_AUTO_DEFAULT_ROUTE,
                                                    NM_SETTING_PARAM_FUZZY_IGNORE,
                                                    NMSettingWireGuard,
                                                    _priv.ip4_auto_default_route);

    /**
     * NMSettingWireGuard:ip6-auto-default-route:
     *
     * Like ip4-auto-default-route, but for the IPv6 default route.
     *
     * Since: 1.20
     **/
    _nm_setting_property_define_direct_ternary_enum(properties_override,
                                                    obj_properties,
                                                    NM_SETTING_WIREGUARD_IP6_AUTO_DEFAULT_ROUTE,
                                                    PROP_IP6_AUTO_DEFAULT_ROUTE,
                                                    NM_SETTING_PARAM_FUZZY_IGNORE,
                                                    NMSettingWireGuard,
                                                    _priv.ip6_auto_default_route);

    /* ---nmcli---
     * property: peers
     * format: a comma-separated list of WireGuard peers
     * description:
     *  A comma-separated list of WireGuard peers. Each peer has the following syntax:
     *
     *   PUBLIC_KEY [ATTRIBUTE=VALUE [ATTRIBUTE=VALUE]...]
     *
     *   The supported attributes are: endpoint, allowed-ips, persistent-keepalive,
     *   preshared-key, preshared-key-flags.
     * description-docbook:
     *   <para>
     *   A comma-separated list of WireGuard peers. Each peer has the following syntax:
     *   </para>
     *   <para>
     *   <literal>
     *   <replaceable>public-key</replaceable>
     *   [<replaceable>attribute</replaceable>=<replaceable>value</replaceable>
     *   [<replaceable>attribute</replaceable>=<replaceable>value</replaceable>]...]
     *   </literal>
     *   </para>
     *   <para>
     *   The public key is required and must be encoded as base64; it can be
     *   calculated by running <command>wg pubkey</command> on the private key,
     *   and it is usually transmitted out of band to the author of the configuration
     *   file.
     *   </para>
     *   <para>
     *   The supported attributes are:
     *   <variablelist>
     *     <varlistentry>
     *       <term><varname>endpoint</varname></term>
     *       <listitem><para>An endpoint IP or hostname, followed by a colon, and then
     *       a port number.</para></listitem>
     *     </varlistentry>
     *
     *     <varlistentry>
     *       <term><varname></varname></term>
     *       <listitem><para></para></listitem>
     *     </varlistentry>
     *     <varlistentry>
     *       <term><varname>allowed-ips</varname></term>
     *       <listitem><para>A semicolon-separated list of IP (v4 or v6) addresses
     *       with CIDR masks from which incoming traffic for this peer is allowed
     *       and to which outgoing traffic for this peer is directed
     *       </para></listitem>
     *     </varlistentry>
     *     <varlistentry>
     *       <term><varname>persistent-keepalive</varname></term>
     *       <listitem><para>An interval in seconds, between 1 and 65535, of
     *       how often to send an authenticated empty packet to the peer for the
     *       purpose of keeping a stateful firewall or NAT mapping valid persistently.
     *       </para></listitem>
     *     </varlistentry>
     *     <varlistentry>
     *       <term><varname>preshared-key</varname></term>
     *       <listitem><para>A base64 preshared key generated by "wg genpsk". Optional,
     *       and may be omitted.</para></listitem>
     *     </varlistentry>
     *     <varlistentry>
     *       <term><varname>preshared-key-flags</varname></term>
     *       <listitem><para>The secret flags for the preshared-key.</para></listitem>
     *     </varlistentry>
     *   </variablelist>
     *   </para>
     * ---end---
     */
    /* ---dbus---
     * property: peers
     * format: array of 'a{sv}'
     * description: Array of dictionaries for the WireGuard peers.
     * ---end---
     */
    _nm_properties_override_dbus(
        properties_override,
        NM_SETTING_WIREGUARD_PEERS,
        NM_SETT_INFO_PROPERT_TYPE_DBUS(NM_G_VARIANT_TYPE("aa{sv}"),
                                       .to_dbus_fcn   = peers_to_dbus,
                                       .compare_fcn   = compare_fcn_peers,
                                       .from_dbus_fcn = peers_from_dbus, ));

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    _nm_setting_class_commit(setting_class,
                             NM_META_SETTING_TYPE_WIREGUARD,
                             NULL,
                             properties_override,
                             0);
}
