/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2018 Javier Arteaga <jarteaga@jbeta.is>
 */

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

#include "nm-device-wireguard.h"

#include <linux/rtnetlink.h>
#include <linux/fib_rules.h>

#include "nm-setting-wireguard.h"
#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
#include "nm-l3-config-data.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-glib-aux/nm-secret-utils.h"
#include "nm-device-private.h"
#include "libnm-platform/nm-platform.h"
#include "libnm-platform/nmp-object.h"
#include "libnm-platform/nmp-global-tracker.h"
#include "nm-device-factory.h"
#include "nm-active-connection.h"
#include "nm-act-request.h"
#include "dns/nm-dns-manager.h"
#include "nm-firewall-utils.h"

#define _NMLOG_DEVICE_TYPE NMDeviceWireGuard
#include "nm-device-logging.h"

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

/* TODO: activate profile with peer preshared-key-flags=2. On first activation, the secret is
 *   requested (good). Enter it and connect. Reactivate the profile, now there is no password
 *   prompt, as the secret is cached (good??). */

/* TODO: unlike for other VPNs, we don't inject a direct route to the peers. That means,
 *   you might get a routing scenario where the peer (VPN server) is reachable via the VPN.
 *   How we handle adding routes to external gateway for other peers, has severe issues
 *   as well. We may use policy-routing like wg-quick does. See also discussions at
 *   https://www.wireguard.com/netns/#improving-the-classic-solutions */

/* TODO: honor the TTL of DNS to determine when to retry resolving endpoints. */

/* TODO: when we get multiple IP addresses when resolving a peer endpoint. We currently
 *   just take the first from GAI. We should only accept AAAA/IPv6 if we also have a suitable
 *   IPv6 address. The problem is, that we have to recheck that when IP addressing on other
 *   interfaces changes. This makes it almost too cumbersome to implement. */

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

G_STATIC_ASSERT(NM_WIREGUARD_PUBLIC_KEY_LEN == NMP_WIREGUARD_PUBLIC_KEY_LEN);
G_STATIC_ASSERT(NM_WIREGUARD_SYMMETRIC_KEY_LEN == NMP_WIREGUARD_SYMMETRIC_KEY_LEN);

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

#define LINK_CONFIG_RATE_LIMIT_NSEC (50 * NM_UTILS_NSEC_PER_MSEC)

/* a special @next_try_at_nsec timestamp indicating that we should try again as soon as possible. */
#define NEXT_TRY_AT_NSEC_ASAP ((gint64) G_MAXINT64)

/* a special @next_try_at_nsec timestamp that is
 *  - positive (indicating resolve-checks are enabled)
 *  - already in the past (we use the absolute timestamp of 1nsec for that). */
#define NEXT_TRY_AT_NSEC_PAST ((gint64) 1)

/* like %NEXT_TRY_AT_NSEC_ASAP, but used for indicating to retry ASAP for a @retry_in_msec value.
 * That is a relative time duration, contrary to @next_try_at_nsec which is an absolute
 * timestamp. */
#define RETRY_IN_MSEC_ASAP ((gint64) G_MAXINT64)

#define RETRY_IN_MSEC_MAX ((gint64) (30 * 60 * 1000))

typedef enum {
    LINK_CONFIG_MODE_FULL,
    LINK_CONFIG_MODE_REAPPLY,
    LINK_CONFIG_MODE_ASSUME,
    LINK_CONFIG_MODE_ENDPOINTS,
} LinkConfigMode;

typedef struct {
    GCancellable *cancellable;

    NMSockAddrUnion sockaddr;

    /* the timestamp (in nm_utils_get_monotonic_timestamp_nsec() scale) when we want
     * to retry resolving the endpoint (again).
     *
     * It may be set to %NEXT_TRY_AT_NSEC_ASAP to indicate to re-resolve as soon as possible.
     *
     * A @sockaddr is either fixed or it has
     *   - @cancellable set to indicate an ongoing request
     *   - @next_try_at_nsec set to a positive value, indicating when
     *     we ought to retry. */
    gint64 next_try_at_nsec;

    guint resolv_fail_count;
} PeerEndpointResolveData;

typedef struct {
    NMWireGuardPeer *peer;

    NMDeviceWireGuard *self;

    CList lst_peers;

    PeerEndpointResolveData ep_resolv;

    /* dirty flag used during _peers_update_all(). */
    bool dirty_update_all : 1;
} PeerData;

NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWireGuard, PROP_PUBLIC_KEY, PROP_LISTEN_PORT, PROP_FWMARK, );

typedef struct {
    NMDnsManager *dns_manager;

    NMPlatformLnkWireGuard        lnk_curr;
    NMActRequestGetSecretsCallId *secrets_call_id;

    CList       lst_peers_head;
    GHashTable *peers;

    /* counts the numbers of peers that are currently resolving. */
    guint peers_resolving_cnt;

    gint64 resolve_next_try_at;
    gint64 link_config_last_at;

    guint resolve_next_try_id;
    guint link_config_delayed_id;

    guint32 auto_default_route_fwmark;

    guint32 auto_default_route_priority;

    bool auto_default_route_enabled_4 : 1;
    bool auto_default_route_enabled_6 : 1;
    bool auto_default_route_initialized : 1;
    bool auto_default_route_refresh : 1;
    bool auto_default_route_priority_initialized : 1;

} NMDeviceWireGuardPrivate;

struct _NMDeviceWireGuard {
    NMDevice                 parent;
    NMDeviceWireGuardPrivate _priv;
};

struct _NMDeviceWireGuardClass {
    NMDeviceClass parent;
};

G_DEFINE_TYPE(NMDeviceWireGuard, nm_device_wireguard, NM_TYPE_DEVICE)

#define NM_DEVICE_WIREGUARD_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMDeviceWireGuard, NM_IS_DEVICE_WIREGUARD, NMDevice)

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

static void _peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data);

static void _peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec);

static gboolean link_config_delayed_resolver_cb(gpointer user_data);

static gboolean link_config_delayed_ratelimit_cb(gpointer user_data);

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

static NM_UTILS_LOOKUP_STR_DEFINE(_link_config_mode_to_string,
                                  LinkConfigMode,
                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL),
                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_FULL, "full"),
                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_REAPPLY, "reapply"),
                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ASSUME, "assume"),
                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ENDPOINTS, "endpoints"), );

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

static void
_auto_default_route_get_enabled(NMSettingWireGuard *s_wg,
                                NMConnection       *connection,
                                gboolean           *out_enabled_v4,
                                gboolean           *out_enabled_v6)
{
    NMTernary enabled_v4;
    NMTernary enabled_v6;

    enabled_v4 = nm_setting_wireguard_get_ip4_auto_default_route(s_wg);
    enabled_v6 = nm_setting_wireguard_get_ip6_auto_default_route(s_wg);

    if (enabled_v4 == NM_TERNARY_DEFAULT) {
        if (nm_setting_ip_config_get_never_default(
                nm_connection_get_setting_ip_config(connection, AF_INET)))
            enabled_v4 = FALSE;
    }
    if (enabled_v6 == NM_TERNARY_DEFAULT) {
        if (nm_setting_ip_config_get_never_default(
                nm_connection_get_setting_ip_config(connection, AF_INET6)))
            enabled_v6 = FALSE;
    }

    if (enabled_v4 == NM_TERNARY_DEFAULT || enabled_v6 == NM_TERNARY_DEFAULT) {
        guint i, n_peers;

        n_peers = nm_setting_wireguard_get_peers_len(s_wg);
        for (i = 0; i < n_peers; i++) {
            NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i);
            guint            n_aips;
            guint            j;

            n_aips = nm_wireguard_peer_get_allowed_ips_len(peer);
            for (j = 0; j < n_aips; j++) {
                const char *aip;
                gboolean    valid;
                int         prefix;
                int         addr_family;

                aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
                if (!valid)
                    continue;
                if (!nm_inet_parse_with_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix))
                    continue;
                if (prefix != 0)
                    continue;

                if (addr_family == AF_INET) {
                    if (enabled_v4 == NM_TERNARY_DEFAULT) {
                        enabled_v4 = TRUE;
                        if (enabled_v6 != NM_TERNARY_DEFAULT)
                            goto done;
                    }
                } else {
                    if (enabled_v6 == NM_TERNARY_DEFAULT) {
                        enabled_v6 = TRUE;
                        if (enabled_v4 != NM_TERNARY_DEFAULT)
                            goto done;
                    }
                }
            }
        }
done:;
    }

    *out_enabled_v4 = (enabled_v4 == TRUE);
    *out_enabled_v6 = (enabled_v6 == TRUE);
}

#define AUTO_RANDOM_RANGE 500u

static guint32
_auto_default_route_get_auto_fwmark(const char *uuid)
{
    guint64 rnd_seed;

    /* we use the generated number as fwmark but also as routing table for
     * the default-route.
     *
     * We pick a number
     *
     * - based on the connection's UUID (as stable seed).
     * - larger than 51820u (arbitrarily)
     * - one out of AUTO_RANDOM_RANGE
     */

    rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0xb9,
                                              0x39,
                                              0x8e,
                                              0xed,
                                              0x15,
                                              0xb3,
                                              0xd1,
                                              0xc4,
                                              0x5f,
                                              0x45,
                                              0x00,
                                              0x4f,
                                              0xec,
                                              0xc2,
                                              0x2b,
                                              0x7e),
                              (const guint8 *) uuid,
                              uuid ? strlen(uuid) + 1u : 0u);

    return 51820u + (rnd_seed % AUTO_RANDOM_RANGE);
}

#define PRIO_WIDTH 2u

static guint32
_auto_default_route_get_auto_priority(const char *uuid)
{
    const guint32 RANGE_TOP = 32766u - 1000u;
    guint64       rnd_seed;

    /* we pick a priority for the routing rules as follows:
     *
     * - use the connection's UUID as stable seed for the "random" number.
     * - have it smaller than RANGE_TOP (32766u - 1000u), where 32766u is the priority of the default
     *   rules
     * - we add 2 rules (PRIO_WIDTH). Hence only pick even priorities.
     * - pick one out of AUTO_RANDOM_RANGE. */

    rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0x99,
                                              0x22,
                                              0x4d,
                                              0x7c,
                                              0x37,
                                              0xda,
                                              0x8e,
                                              0x7b,
                                              0x2f,
                                              0x55,
                                              0x16,
                                              0x7b,
                                              0x75,
                                              0xda,
                                              0x42,
                                              0xdc),
                              (const guint8 *) uuid,
                              uuid ? strlen(uuid) + 1u : 0u);

    return RANGE_TOP - (((rnd_seed % (PRIO_WIDTH * AUTO_RANDOM_RANGE)) / PRIO_WIDTH) * PRIO_WIDTH);
}

static void
_auto_default_route_init(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    NMConnection             *connection;
    gboolean                  enabled_v4 = FALSE;
    gboolean                  enabled_v6 = FALSE;
    gboolean                  refreshing_only;
    guint32                   new_fwmark = 0;
    guint32                   old_fwmark;
    char                      sbuf1[100];

    if (G_LIKELY(priv->auto_default_route_initialized && !priv->auto_default_route_refresh))
        return;

    refreshing_only = priv->auto_default_route_initialized && priv->auto_default_route_refresh;

    old_fwmark = priv->auto_default_route_fwmark;

    connection = nm_device_get_applied_connection(NM_DEVICE(self));
    if (connection) {
        NMSettingWireGuard *s_wg;

        s_wg = _nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD);

        new_fwmark = nm_setting_wireguard_get_fwmark(s_wg);

        _auto_default_route_get_enabled(s_wg, connection, &enabled_v4, &enabled_v6);
    }

    if ((enabled_v4 || enabled_v6) && new_fwmark == 0u) {
        if (refreshing_only)
            new_fwmark = old_fwmark;
        else
            new_fwmark = _auto_default_route_get_auto_fwmark(nm_connection_get_uuid(connection));
    }

    priv->auto_default_route_refresh     = FALSE;
    priv->auto_default_route_fwmark      = new_fwmark;
    priv->auto_default_route_enabled_4   = enabled_v4;
    priv->auto_default_route_enabled_6   = enabled_v6;
    priv->auto_default_route_initialized = TRUE;

    if (connection) {
        _LOGT(LOGD_DEVICE,
              "auto-default-route is %s for IPv4 and %s for IPv6%s",
              priv->auto_default_route_enabled_4 ? "enabled" : "disabled",
              priv->auto_default_route_enabled_6 ? "enabled" : "disabled",
              priv->auto_default_route_enabled_4 || priv->auto_default_route_enabled_6
                  ? nm_sprintf_buf(sbuf1, " (fwmark 0x%x)", priv->auto_default_route_fwmark)
                  : "");
    }
}

static GPtrArray *
get_extra_rules(NMDevice *device)
{
    NMDeviceWireGuard           *self        = NM_DEVICE_WIREGUARD(device);
    NMDeviceWireGuardPrivate    *priv        = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    gs_unref_ptrarray GPtrArray *extra_rules = NULL;
    guint32                      priority    = 0;
    int                          is_ipv4;
    NMConnection                *connection;

    _auto_default_route_init(self);

    connection = nm_device_get_applied_connection(device);
    if (!connection)
        return NULL;

    for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
        NMSettingIPConfig *s_ip;
        int                addr_family = is_ipv4 ? AF_INET : AF_INET6;
        guint32            table_main;
        guint32            fwmark;

        if (is_ipv4) {
            if (!priv->auto_default_route_enabled_4)
                continue;
        } else {
            if (!priv->auto_default_route_enabled_6)
                continue;
        }

        if (!extra_rules) {
            if (priv->auto_default_route_priority_initialized)
                priority = priv->auto_default_route_priority;
            else {
                priority =
                    _auto_default_route_get_auto_priority(nm_connection_get_uuid(connection));
                priv->auto_default_route_priority             = priority;
                priv->auto_default_route_priority_initialized = TRUE;
            }
            extra_rules = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
        }

        s_ip       = nm_connection_get_setting_ip_config(connection, addr_family);
        table_main = nm_setting_ip_config_get_route_table(s_ip);
        if (table_main == 0)
            table_main = RT_TABLE_MAIN;

        fwmark = priv->auto_default_route_fwmark;

        G_STATIC_ASSERT_EXPR(PRIO_WIDTH == 2);

        g_ptr_array_add(extra_rules,
                        nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE,
                                       &((const NMPlatformRoutingRule) {
                                           .priority                   = priority,
                                           .addr_family                = addr_family,
                                           .action                     = FR_ACT_TO_TBL,
                                           .table                      = table_main,
                                           .suppress_prefixlen_inverse = ~((guint32) 0u),
                                       })));

        g_ptr_array_add(extra_rules,
                        nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE,
                                       &((const NMPlatformRoutingRule) {
                                           .priority    = priority + 1u,
                                           .addr_family = addr_family,
                                           .action      = FR_ACT_TO_TBL,
                                           .table       = fwmark,
                                           .flags       = FIB_RULE_INVERT,
                                           .fwmark      = fwmark,
                                           .fwmask      = 0xFFFFFFFFu,
                                       })));
    }

    return g_steal_pointer(&extra_rules);
}

static guint32
coerce_route_table(NMDevice *device, int addr_family, guint32 route_table, gboolean is_user_config)
{
    NMDeviceWireGuard        *self = NM_DEVICE_WIREGUARD(device);
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    gboolean                  auto_default_route_enabled;

    if (route_table != 0u)
        return route_table;

    _auto_default_route_init(self);

    auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4
                                                          : priv->auto_default_route_enabled_6;

    if (auto_default_route_enabled) {
        /* we need to enable full-sync mode of all routing tables. */
        _LOGT(LOGD_DEVICE,
              "coerce ipv%c.route-table setting to \"main\" (table 254) as we enable "
              "auto-default-route handling",
              nm_utils_addr_family_to_char(addr_family));
        return RT_TABLE_MAIN;
    }

    return 0;
}

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

static gboolean
_peer_data_equal(gconstpointer ptr_a, gconstpointer ptr_b)
{
    const PeerData *peer_data_a = ptr_a;
    const PeerData *peer_data_b = ptr_b;

    return nm_streq(nm_wireguard_peer_get_public_key(peer_data_a->peer),
                    nm_wireguard_peer_get_public_key(peer_data_b->peer));
}

static guint
_peer_data_hash(gconstpointer ptr)
{
    const PeerData *peer_data = ptr;

    return nm_hash_str(nm_wireguard_peer_get_public_key(peer_data->peer));
}

static PeerData *
_peers_find(NMDeviceWireGuardPrivate *priv, NMWireGuardPeer *peer)
{
    nm_assert(peer);

    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(PeerData, peer) == 0);

    return g_hash_table_lookup(priv->peers, &peer);
}

static guint
_peers_resolving_cnt(NMDeviceWireGuardPrivate *priv)
{
    nm_assert(priv);
#if NM_MORE_ASSERTS > 3
    {
        PeerData *peer_data;
        guint     cnt = 0;

        c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
            if (peer_data->ep_resolv.cancellable)
                cnt++;
        }
        nm_assert(cnt == priv->peers_resolving_cnt);
    }
#endif

    return priv->peers_resolving_cnt;
}

static void
_peers_resolving_cnt_decrement(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    nm_assert(priv);
    nm_assert(priv->peers_resolving_cnt > 0);

    priv->peers_resolving_cnt--;

    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);

    if (priv->peers_resolving_cnt == 0) {
        if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) {
            _LOGT(LOGD_DEVICE,
                  "activation delayed to resolve DNS names of peers: completed, proceed now");
            nm_device_activate_schedule_stage2_device_config(NM_DEVICE(self), FALSE);
        }
    }
}

static void
_peers_remove(NMDeviceWireGuard *self, PeerData *peer_data)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    nm_assert(peer_data);
    nm_assert(g_hash_table_lookup(priv->peers, peer_data) == peer_data);

    if (!g_hash_table_remove(priv->peers, peer_data))
        nm_assert_not_reached();

    c_list_unlink_stale(&peer_data->lst_peers);
    nm_wireguard_peer_unref(peer_data->peer);
    if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable))
        _peers_resolving_cnt_decrement(self);
    g_slice_free(PeerData, peer_data);

    if (c_list_is_empty(&priv->lst_peers_head)) {
        nm_clear_g_source(&priv->resolve_next_try_id);
        nm_clear_g_source(&priv->link_config_delayed_id);
    }

    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
}

static PeerData *
_peers_add(NMDeviceWireGuard *self, NMWireGuardPeer *peer)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    PeerData                 *peer_data;

    nm_assert(peer);
    nm_assert(nm_wireguard_peer_is_sealed(peer));
    nm_assert(!_peers_find(priv, peer));

    peer_data  = g_slice_new(PeerData);
    *peer_data = (PeerData) {
        .self = self,
        .peer = nm_wireguard_peer_ref(peer),
        .ep_resolv =
            {
                .sockaddr = NM_SOCK_ADDR_UNION_INIT_UNSPEC,
            },
    };

    c_list_link_tail(&priv->lst_peers_head, &peer_data->lst_peers);
    if (!g_hash_table_add(priv->peers, peer_data))
        nm_assert_not_reached();
    return peer_data;
}

static gboolean
_peers_resolve_retry_timeout(gpointer user_data)
{
    NMDeviceWireGuard        *self = user_data;
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    PeerData                 *peer_data;
    gint64                    now;
    gint64                    next;

    priv->resolve_next_try_id = 0;

    _LOGT(LOGD_DEVICE, "wireguard-peers: rechecking peer endpoints...");

    now  = nm_utils_get_monotonic_timestamp_nsec();
    next = G_MAXINT64;
    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
        if (peer_data->ep_resolv.next_try_at_nsec <= 0)
            continue;

        if (peer_data->ep_resolv.cancellable) {
            /* we are currently resolving a name. We don't need the global
             * watchdog to guard this peer. No need to adjust @next for
             * this one, when the currently ongoing resolving completes, we
             * may reschedule. Skip. */
            continue;
        }

        if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP
            || now >= peer_data->ep_resolv.next_try_at_nsec) {
            _peers_resolve_start(self, peer_data);
            /* same here. Now we are resolving. We don't need the global
             * watchdog. Skip w.r.t. finding @next. */
            continue;
        }

        if (next > peer_data->ep_resolv.next_try_at_nsec)
            next = peer_data->ep_resolv.next_try_at_nsec;
    }
    if (next < G_MAXINT64)
        _peers_resolve_retry_reschedule(self, next);

    return G_SOURCE_REMOVE;
}

static void
_peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    guint32                   interval_ms;
    gint64                    now;

    nm_assert(new_next_try_at_nsec > 0);
    nm_assert(new_next_try_at_nsec != NEXT_TRY_AT_NSEC_ASAP);

    if (priv->resolve_next_try_id && priv->resolve_next_try_at <= new_next_try_at_nsec) {
        /* we already have an earlier timeout scheduled (possibly for
         * another peer that expires sooner). Don't reschedule now.
         * Even if the scheduled timeout expires too early, we will
         * compute the right next-timeout and reschedule then. */
        return;
    }

    now = nm_utils_get_monotonic_timestamp_nsec();

    /* schedule at most one day ahead. No problem if we expire earlier
     * than expected. Also, rate-limit to 500 msec. */
    interval_ms = NM_CLAMP((new_next_try_at_nsec - now) / NM_UTILS_NSEC_PER_MSEC,
                           (gint64) 500,
                           (gint64) (24 * 60 * 60 * 1000));

    _LOGT(LOGD_DEVICE,
          "wireguard-peers: schedule rechecking peer endpoints in %u msec",
          interval_ms);

    nm_clear_g_source(&priv->resolve_next_try_id);
    priv->resolve_next_try_at = new_next_try_at_nsec;
    priv->resolve_next_try_id = g_timeout_add(interval_ms, _peers_resolve_retry_timeout, self);
}

static void
_peers_resolve_retry_reschedule_for_peer(NMDeviceWireGuard *self,
                                         PeerData          *peer_data,
                                         gint64             retry_in_msec)
{
    nm_assert(retry_in_msec >= 0);

    if (retry_in_msec == RETRY_IN_MSEC_ASAP) {
        _peers_resolve_start(self, peer_data);
        return;
    }

    peer_data->ep_resolv.next_try_at_nsec =
        nm_utils_get_monotonic_timestamp_nsec() + (retry_in_msec * NM_UTILS_NSEC_PER_MSEC);
    _peers_resolve_retry_reschedule(self, peer_data->ep_resolv.next_try_at_nsec);
}

static gint64
_peers_retry_in_msec(PeerData *peer_data, gboolean after_failure)
{
    if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP) {
        peer_data->ep_resolv.resolv_fail_count = 0;
        return RETRY_IN_MSEC_ASAP;
    }

    if (after_failure) {
        if (peer_data->ep_resolv.resolv_fail_count < G_MAXUINT)
            peer_data->ep_resolv.resolv_fail_count++;
    } else
        peer_data->ep_resolv.resolv_fail_count = 0;

    if (!after_failure)
        return RETRY_IN_MSEC_MAX;

    if (peer_data->ep_resolv.resolv_fail_count > 20)
        return RETRY_IN_MSEC_MAX;

    /* double the retry-time, starting with one second. */
    return NM_MIN(RETRY_IN_MSEC_MAX, (1l << peer_data->ep_resolv.resolv_fail_count) * 500);
}

static void
_peers_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMDeviceWireGuard        *self;
    NMDeviceWireGuardPrivate *priv;
    PeerData                 *peer_data;
    gs_free_error GError     *resolv_error = NULL;
    GList                    *list;
    gboolean                  changed;
    NMSockAddrUnion           sockaddr;
    gint64                    retry_in_msec;
    char                      s_sockaddr[100];
    char                      s_retry[100];

    list = g_resolver_lookup_by_name_finish(G_RESOLVER(source_object), res, &resolv_error);

    if (nm_utils_error_is_cancelled(resolv_error))
        return;

    peer_data = user_data;
    self      = peer_data->self;
    priv      = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    if (nm_clear_g_object(&peer_data->ep_resolv.cancellable))
        _peers_resolving_cnt_decrement(self);

    nm_assert((!resolv_error) != (!list));
    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);

#define _retry_in_msec_to_string(retry_in_msec, s_retry)                               \
    ({                                                                                 \
        gint64 _retry_in_msec = (retry_in_msec);                                       \
                                                                                       \
        _retry_in_msec == RETRY_IN_MSEC_ASAP                                           \
            ? "right away"                                                             \
            : nm_sprintf_buf(s_retry, "in %" G_GINT64_FORMAT " msec", _retry_in_msec); \
    })

    if (resolv_error
        && !g_error_matches(resolv_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND)) {
        retry_in_msec = _peers_retry_in_msec(peer_data, TRUE);

        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: failure to resolve endpoint \"%s\": %s (retry %s)",
              nm_wireguard_peer_get_public_key(peer_data->peer),
              nm_wireguard_peer_get_endpoint(peer_data->peer),
              resolv_error->message,
              _retry_in_msec_to_string(retry_in_msec, s_retry));

        _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec);
        return;
    }

    sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC;
    changed  = FALSE;

    if (!resolv_error) {
        GList *iter;

        for (iter = list; iter; iter = iter->next) {
            GInetAddress    *a = iter->data;
            NMSockAddrUnion  sockaddr_tmp;
            NMSockAddrUnion *s;

            s = sockaddr.sa.sa_family == AF_UNSPEC ? &sockaddr : &sockaddr_tmp;

            switch (g_inet_address_get_family(a)) {
            case G_SOCKET_FAMILY_IPV4:
                nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in_addr));
                s->in = (struct sockaddr_in) {
                    .sin_family = AF_INET,
                    .sin_port   = htons(nm_sock_addr_endpoint_get_port(
                        _nm_wireguard_peer_get_endpoint(peer_data->peer))),
                };
                memcpy(&s->in.sin_addr, g_inet_address_to_bytes(a), sizeof(struct in_addr));
                break;
            case G_SOCKET_FAMILY_IPV6:
                nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in6_addr));
                s->in6 = (struct sockaddr_in6) {
                    .sin6_family   = AF_INET6,
                    .sin6_port     = htons(nm_sock_addr_endpoint_get_port(
                        _nm_wireguard_peer_get_endpoint(peer_data->peer))),
                    .sin6_scope_id = 0,
                    .sin6_flowinfo = 0,
                };
                memcpy(&s->in6.sin6_addr, g_inet_address_to_bytes(a), sizeof(struct in6_addr));
                break;
            default:
                continue;
            }

            changed = TRUE;
            if (peer_data->ep_resolv.sockaddr.sa.sa_family == AF_UNSPEC)
                break;

            if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) == 0) {
                changed = FALSE;
                break;
            }
        }

        g_list_free_full(list, g_object_unref);
    }

    if (sockaddr.sa.sa_family == AF_UNSPEC) {
        /* we failed to resolve the name. There is no need to reset the previous
         * sockaddr. Either it was already AF_UNSPEC, or we had a good name
         * from resolving before. In that case, we don't want to throw away
         * a possibly good IP address, since WireGuard supports automatic roaming
         * anyway. Either the IP address is still good (and we would wrongly
         * reject it), or it isn't -- in which case it does not hurt much. */
    } else if (changed)
        peer_data->ep_resolv.sockaddr = sockaddr;

    if (resolv_error || peer_data->ep_resolv.sockaddr.sa.sa_family == AF_UNSPEC) {
        /* while it technically did not fail, something is probably odd. Retry frequently to
         * resolve the name, like we would do for normal failures. */
        retry_in_msec = _peers_retry_in_msec(peer_data, TRUE);
        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: no %sresults for endpoint \"%s\" (retry %s)",
              nm_wireguard_peer_get_public_key(peer_data->peer),
              resolv_error ? "" : "suitable ",
              nm_wireguard_peer_get_endpoint(peer_data->peer),
              _retry_in_msec_to_string(retry_in_msec, s_retry));
    } else {
        retry_in_msec = _peers_retry_in_msec(peer_data, FALSE);
        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: endpoint \"%s\" resolved to %s (retry %s)",
              nm_wireguard_peer_get_public_key(peer_data->peer),
              nm_wireguard_peer_get_endpoint(peer_data->peer),
              nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr,
                                           s_sockaddr,
                                           sizeof(s_sockaddr)),
              _retry_in_msec_to_string(retry_in_msec, s_retry));
    }

    _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec);

    if (changed) {
        /* schedule the job in the background, to give multiple resolve events time
         * to complete. */
        nm_clear_g_source(&priv->link_config_delayed_id);
        priv->link_config_delayed_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1,
                                                       link_config_delayed_resolver_cb,
                                                       self,
                                                       NULL);
    }
}

static void
_peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data)
{
    NMDeviceWireGuardPrivate  *priv     = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    gs_unref_object GResolver *resolver = NULL;
    const char                *host;

    resolver = g_resolver_get_default();

    nm_assert(!peer_data->ep_resolv.cancellable);

    peer_data->ep_resolv.cancellable = g_cancellable_new();
    priv->peers_resolving_cnt++;

    /* set a special next-try timestamp. It is positive, and indicates
     * that we are in the process of trying.
     * This timestamp however already lies in the past, but that is correct,
     * because we are currently in the process of trying. We will determine
     * a next-try timestamp once the try completes. */
    peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_PAST;

    host = nm_sock_addr_endpoint_get_host(_nm_wireguard_peer_get_endpoint(peer_data->peer));

    g_resolver_lookup_by_name_async(resolver,
                                    host,
                                    peer_data->ep_resolv.cancellable,
                                    _peers_resolve_cb,
                                    peer_data);

    _LOGT(LOGD_DEVICE,
          "wireguard-peer[%s]: resolving name \"%s\" for endpoint \"%s\"...",
          nm_wireguard_peer_get_public_key(peer_data->peer),
          host,
          nm_wireguard_peer_get_endpoint(peer_data->peer));

    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
}

static void
_peers_resolve_reresolve_all(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    PeerData                 *peer_data;

    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
        if (peer_data->ep_resolv.cancellable) {
            /* remember to retry when the currently ongoing request completes. */
            peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_ASAP;
        } else if (peer_data->ep_resolv.next_try_at_nsec <= 0) {
            /* this peer does not require resolving the name. Skip it. */
        } else {
            /* we have a next-try scheduled. Restart right away. */
            peer_data->ep_resolv.resolv_fail_count = 0;
            _peers_resolve_start(self, peer_data);
        }
    }
}

static gboolean
_peers_update(NMDeviceWireGuard *self,
              PeerData          *peer_data,
              NMWireGuardPeer   *peer,
              gboolean           force_update)
{
    nm_auto_unref_wgpeer NMWireGuardPeer *old_peer = NULL;
    NMSockAddrEndpoint                   *old_endpoint;
    NMSockAddrEndpoint                   *endpoint;
    gboolean                              endpoint_changed = FALSE;
    gboolean                              changed;
    NMSockAddrUnion                       sockaddr;
    gboolean                              sockaddr_fixed;
    char                                  sockaddr_sbuf[100];

    nm_assert(peer);
    nm_assert(nm_wireguard_peer_is_sealed(peer));

    if (peer == peer_data->peer && !force_update)
        return FALSE;

    changed = (nm_wireguard_peer_cmp(peer, peer_data->peer, NM_SETTING_COMPARE_FLAG_EXACT) != 0);

    old_peer        = peer_data->peer;
    peer_data->peer = nm_wireguard_peer_ref(peer);

    old_endpoint = old_peer ? _nm_wireguard_peer_get_endpoint(old_peer) : NULL;
    endpoint     = peer ? _nm_wireguard_peer_get_endpoint(peer) : NULL;

    endpoint_changed = (endpoint != old_endpoint
                        && (!old_endpoint || !endpoint
                            || !nm_streq(nm_sock_addr_endpoint_get_endpoint(old_endpoint),
                                         nm_sock_addr_endpoint_get_endpoint(endpoint))));

    if (!force_update && !endpoint_changed) {
        /* nothing to do. */
        return changed;
    }

    sockaddr       = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC;
    sockaddr_fixed = TRUE;
    if (endpoint && nm_sock_addr_endpoint_get_host(endpoint)) {
        if (!nm_sock_addr_endpoint_get_fixed_sockaddr(endpoint, &sockaddr)) {
            /* we have an endpoint, but it's not a static IP address. We need to resolve
             * the names. */
            sockaddr_fixed = FALSE;
        }
    }

    if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) != 0)
        changed = TRUE;

    if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable))
        _peers_resolving_cnt_decrement(self);

    peer_data->ep_resolv = (PeerEndpointResolveData) {
        .sockaddr          = sockaddr,
        .resolv_fail_count = 0,
        .cancellable       = NULL,
        .next_try_at_nsec  = 0,
    };

    if (!endpoint) {
        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: no endpoint configured",
              nm_wireguard_peer_get_public_key(peer_data->peer));
    } else if (!nm_sock_addr_endpoint_get_host(endpoint)) {
        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: invalid endpoint \"%s\"",
              nm_wireguard_peer_get_public_key(peer_data->peer),
              nm_sock_addr_endpoint_get_endpoint(endpoint));
    } else if (sockaddr_fixed) {
        _LOGT(LOGD_DEVICE,
              "wireguard-peer[%s]: fixed endpoint \"%s\" (%s)",
              nm_wireguard_peer_get_public_key(peer_data->peer),
              nm_sock_addr_endpoint_get_endpoint(endpoint),
              nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr,
                                           sockaddr_sbuf,
                                           sizeof(sockaddr_sbuf)));
    } else
        _peers_resolve_start(self, peer_data);

    return changed;
}

static void
_peers_remove_all(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    PeerData                 *peer_data;

    while ((peer_data = c_list_first_entry(&priv->lst_peers_head, PeerData, lst_peers)))
        _peers_remove(self, peer_data);
}

static void
_peers_update_all(NMDeviceWireGuard *self, NMSettingWireGuard *s_wg, gboolean *out_peers_removed)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    PeerData                 *peer_data_safe;
    PeerData                 *peer_data;
    guint                     i, n;
    gboolean                  peers_removed = FALSE;

    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers)
        peer_data->dirty_update_all = TRUE;

    n = nm_setting_wireguard_get_peers_len(s_wg);
    for (i = 0; i < n; i++) {
        NMWireGuardPeer *peer  = nm_setting_wireguard_get_peer(s_wg, i);
        gboolean         added = FALSE;

        peer_data = _peers_find(priv, peer);
        if (!peer_data) {
            peer_data = _peers_add(self, peer);
            added     = TRUE;
        }
        _peers_update(self, peer_data, peer, added);
        peer_data->dirty_update_all = FALSE;
    }

    c_list_for_each_entry_safe (peer_data, peer_data_safe, &priv->lst_peers_head, lst_peers) {
        if (peer_data->dirty_update_all) {
            _peers_remove(self, peer_data);
            peers_removed = TRUE;
        }
    }

    NM_SET_OUT(out_peers_removed, peers_removed);
}

static void
_peers_get_platform_list(NMDeviceWireGuardPrivate            *priv,
                         LinkConfigMode                       config_mode,
                         NMPWireGuardPeer                   **out_peers,
                         NMPlatformWireGuardChangePeerFlags **out_peer_flags,
                         guint                               *out_len,
                         GArray                             **out_allowed_ips_data)
{
    gs_free NMPWireGuardPeer                   *plpeers      = NULL;
    gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL;
    gs_unref_array GArray                      *allowed_ips  = NULL;
    PeerData                                   *peer_data;
    guint                                       i_good;
    guint                                       n_aip;
    guint                                       i_aip;
    guint                                       len;
    guint                                       i;

    nm_assert(out_peers && !*out_peers);
    nm_assert(out_peer_flags && !*out_peer_flags);
    nm_assert(out_len && *out_len == 0);
    nm_assert(out_allowed_ips_data && !*out_allowed_ips_data);

    len = g_hash_table_size(priv->peers);

    nm_assert(len == c_list_length(&priv->lst_peers_head));

    if (len == 0)
        return;

    plpeers      = g_new0(NMPWireGuardPeer, len);
    plpeer_flags = g_new0(NMPlatformWireGuardChangePeerFlags, len);

    i_good = 0;
    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
        NMPlatformWireGuardChangePeerFlags *plf = &plpeer_flags[i_good];
        NMPWireGuardPeer                   *plp = &plpeers[i_good];
        NMSettingSecretFlags                psk_secret_flags;

        if (!nm_utils_base64secret_decode(nm_wireguard_peer_get_public_key(peer_data->peer),
                                          sizeof(plp->public_key),
                                          plp->public_key))
            continue;

        *plf = NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_NONE;

        plp->persistent_keepalive_interval =
            nm_wireguard_peer_get_persistent_keepalive(peer_data->peer);
        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY))
            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_KEEPALIVE_INTERVAL;

        /* if the peer has an endpoint but it is not yet resolved (not ready),
         * we still configure it and leave the endpoint unspecified. Later,
         * when we can resolve the endpoint, we will update. */
        plp->endpoint = peer_data->ep_resolv.sockaddr;
        if (plp->endpoint.sa.sa_family == AF_UNSPEC) {
            /* we don't actually ever clear endpoints, if we don't have better information. */
        } else
            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ENDPOINT;

        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) {
            psk_secret_flags = nm_wireguard_peer_get_preshared_key_flags(peer_data->peer);
            if (!NM_FLAGS_HAS(psk_secret_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
                if (!nm_utils_base64secret_decode(
                        nm_wireguard_peer_get_preshared_key(peer_data->peer),
                        sizeof(plp->preshared_key),
                        plp->preshared_key)
                    && config_mode == LINK_CONFIG_MODE_FULL)
                    goto skip;
            }
            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_PRESHARED_KEY;
        }

        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)
            && ((n_aip = nm_wireguard_peer_get_allowed_ips_len(peer_data->peer)) > 0)) {
            if (!allowed_ips)
                allowed_ips = g_array_new(FALSE, FALSE, sizeof(NMPWireGuardAllowedIP));

            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ALLOWEDIPS
                    | NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_REPLACE_ALLOWEDIPS;

            plp->_construct_idx_start = allowed_ips->len;
            for (i_aip = 0; i_aip < n_aip; i_aip++) {
                const char *aip;
                NMIPAddr    addrbin = {};
                int         addr_family;
                gboolean    valid;
                int         prefix;

                aip = nm_wireguard_peer_get_allowed_ip(peer_data->peer, i_aip, &valid);
                if (!valid
                    || !nm_inet_parse_with_prefix_bin(AF_UNSPEC,
                                                      aip,
                                                      &addr_family,
                                                      &addrbin,
                                                      &prefix)) {
                    /* the address is really not expected to be invalid, because then
                     * the connection would not verify. Anyway, silently skip it. */
                    continue;
                }

                if (prefix == -1)
                    prefix = addr_family == AF_INET ? 32 : 128;

                g_array_append_val(allowed_ips,
                                   ((NMPWireGuardAllowedIP) {
                                       .family = addr_family,
                                       .mask   = prefix,
                                       .addr   = addrbin,
                                   }));
            }
            plp->_construct_idx_end = allowed_ips->len;
        }

        i_good++;
        continue;

skip:
        memset(plp, 0, sizeof(*plp));
    }

    if (i_good == 0)
        return;

    for (i = 0; i < i_good; i++) {
        NMPWireGuardPeer *plp = &plpeers[i];
        guint             l;

        if (plp->_construct_idx_end == 0) {
            nm_assert(plp->_construct_idx_start == 0);
            plp->allowed_ips     = NULL;
            plp->allowed_ips_len = 0;
        } else {
            nm_assert(plp->_construct_idx_start < plp->_construct_idx_end);
            l = plp->_construct_idx_end - plp->_construct_idx_start;
            plp->allowed_ips =
                &nm_g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start);
            plp->allowed_ips_len = l;
        }
    }
    *out_peers            = g_steal_pointer(&plpeers);
    *out_peer_flags       = g_steal_pointer(&plpeer_flags);
    *out_len              = i_good;
    *out_allowed_ips_data = g_steal_pointer(&allowed_ips);
}

static void
_configure_firewall(NMDeviceWireGuard *self, NMConnection *connection, int addr_family, gboolean up)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    const char               *ip_iface;
    NMSettingIPConfig        *ip_config;

    ip_iface = nm_device_get_ip_iface(NM_DEVICE(self));

    nm_assert(ip_iface);

    switch (addr_family) {
    case AF_INET:
        if (!priv->auto_default_route_enabled_4)
            return;

        ip_config = nm_connection_get_setting_ip4_config(connection);
        break;
    case AF_INET6:
        if (!priv->auto_default_route_enabled_6)
            return;

        ip_config = nm_connection_get_setting_ip6_config(connection);
        break;
    default:
        nm_assert_not_reached();
    }

    nm_assert(ip_config);
    nm_assert(priv->auto_default_route_fwmark);

    nm_firewall_config_set_wg_rule(ip_iface, ip_config, priv->auto_default_route_fwmark, up);
}

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

static void
update_properties(NMDevice *device)
{
    NMDeviceWireGuard            *self;
    NMDeviceWireGuardPrivate     *priv;
    const NMPlatformLink         *plink;
    const NMPlatformLnkWireGuard *props = NULL;
    int                           ifindex;

    g_return_if_fail(NM_IS_DEVICE_WIREGUARD(device));
    self = NM_DEVICE_WIREGUARD(device);
    priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    ifindex = nm_device_get_ifindex(device);
    props   = nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device), ifindex, &plink);
    if (!props) {
        _LOGW(LOGD_PLATFORM, "could not get wireguard properties");
        return;
    }

    g_object_freeze_notify(G_OBJECT(device));

#define CHECK_PROPERTY_CHANGED(field, prop)         \
    G_STMT_START                                    \
    {                                               \
        if (priv->lnk_curr.field != props->field) { \
            priv->lnk_curr.field = props->field;    \
            _notify(self, prop);                    \
        }                                           \
    }                                               \
    G_STMT_END

#define CHECK_PROPERTY_CHANGED_ARRAY(field, prop)                                              \
    G_STMT_START                                                                               \
    {                                                                                          \
        if (memcmp(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field)) != 0) { \
            memcpy(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field));        \
            _notify(self, prop);                                                               \
        }                                                                                      \
    }                                                                                          \
    G_STMT_END

    CHECK_PROPERTY_CHANGED_ARRAY(public_key, PROP_PUBLIC_KEY);
    CHECK_PROPERTY_CHANGED(listen_port, PROP_LISTEN_PORT);
    CHECK_PROPERTY_CHANGED(fwmark, PROP_FWMARK);

    g_object_thaw_notify(G_OBJECT(device));
}

static void
link_changed(NMDevice *device, const NMPlatformLink *pllink)
{
    NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->link_changed(device, pllink);
    update_properties(device);
}

static NMDeviceCapabilities
get_generic_capabilities(NMDevice *dev)
{
    return NM_DEVICE_CAP_IS_SOFTWARE;
}

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

static gboolean
create_and_realize(NMDevice              *device,
                   NMConnection          *connection,
                   NMDevice              *parent,
                   const NMPlatformLink **out_plink,
                   GError               **error)
{
    const char *iface = nm_device_get_iface(device);
    int         r;

    g_return_val_if_fail(iface, FALSE);

    r = nm_platform_link_wireguard_add(nm_device_get_platform(device), iface, out_plink);
    if (r < 0) {
        g_set_error(error,
                    NM_DEVICE_ERROR,
                    NM_DEVICE_ERROR_CREATION_FAILED,
                    "Failed to create WireGuard interface '%s' for '%s': %s",
                    iface,
                    nm_connection_get_id(connection),
                    nm_strerror(r));
        return FALSE;
    }

    return TRUE;
}

static void
deactivate(NMDevice *device)
{
    NMDeviceWireGuard *self       = NM_DEVICE_WIREGUARD(device);
    NMConnection      *connection = nm_device_get_applied_connection(NM_DEVICE(self));

    if (connection) {
        _configure_firewall(self, connection, AF_INET, FALSE);
        _configure_firewall(self, connection, AF_INET6, FALSE);
    }
}

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

static void
_secrets_cancel(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    if (priv->secrets_call_id)
        nm_act_request_cancel_secrets(NULL, priv->secrets_call_id);
    nm_assert(!priv->secrets_call_id);
}

static void
_secrets_cb(NMActRequest                 *req,
            NMActRequestGetSecretsCallId *call_id,
            NMSettingsConnection         *connection,
            GError                       *error,
            gpointer                      user_data)
{
    NMDeviceWireGuard        *self   = NM_DEVICE_WIREGUARD(user_data);
    NMDevice                 *device = NM_DEVICE(self);
    NMDeviceWireGuardPrivate *priv;

    g_return_if_fail(NM_IS_DEVICE_WIREGUARD(self));
    g_return_if_fail(NM_IS_ACT_REQUEST(req));

    priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    g_return_if_fail(priv->secrets_call_id == call_id);

    priv->secrets_call_id = NULL;

    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    g_return_if_fail(req == nm_device_get_act_request(device));
    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
    g_return_if_fail(nm_act_request_get_settings_connection(req) == connection);

    if (error) {
        _LOGW(LOGD_ETHER, "%s", error->message);
        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
        return;
    }

    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
}

static void
_secrets_get_secrets(NMDeviceWireGuard           *self,
                     const char                  *setting_name,
                     NMSecretAgentGetSecretsFlags flags,
                     const char *const           *hints)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    NMActRequest             *req;

    _secrets_cancel(self);

    req = nm_device_get_act_request(NM_DEVICE(self));
    g_return_if_fail(NM_IS_ACT_REQUEST(req));

    priv->secrets_call_id =
        nm_act_request_get_secrets(req, TRUE, setting_name, flags, hints, _secrets_cb, self);
    g_return_if_fail(priv->secrets_call_id);
}

static NMActStageReturn
_secrets_handle_auth_or_fail(NMDeviceWireGuard *self, NMActRequest *req, gboolean new_secrets)
{
    NMConnection                *applied_connection;
    const char                  *setting_name;
    gs_unref_ptrarray GPtrArray *hints = NULL;

    if (!nm_device_auth_retries_try_next(NM_DEVICE(self)))
        return NM_ACT_STAGE_RETURN_FAILURE;

    nm_device_state_changed(NM_DEVICE(self),
                            NM_DEVICE_STATE_NEED_AUTH,
                            NM_DEVICE_STATE_REASON_NONE);

    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));

    applied_connection = nm_act_request_get_applied_connection(req);
    setting_name       = nm_connection_need_secrets(applied_connection, &hints);
    if (!setting_name) {
        _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets.");
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    if (hints)
        g_ptr_array_add(hints, NULL);

    _secrets_get_secrets(self,
                         setting_name,
                         NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
                             | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0),
                         (hints ? (const char *const *) hints->pdata : NULL));
    return NM_ACT_STAGE_RETURN_POSTPONE;
}

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

static void
_dns_config_changed(NMDnsManager *dns_manager, NMDeviceWireGuard *self)
{
    /* when the DNS configuration changes, we re-resolve the peer addresses.
     *
     * Possibly, we should also do that when the default-route changes, but it's
     * hard to figure out when that happens. */
    _peers_resolve_reresolve_all(self);
}

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

static NMActStageReturn
link_config(NMDeviceWireGuard   *self,
            const char          *reason,
            LinkConfigMode       config_mode,
            NMDeviceStateReason *out_failure_reason)
{
    NMDeviceWireGuardPrivate                   *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    nm_auto_bzero_secret_ptr NMSecretPtr        wg_lnk_clear_private_key = NM_SECRET_PTR_INIT();
    NMSettingWireGuard                         *s_wg;
    NMConnection                               *connection;
    NMActStageReturn                            ret;
    gs_unref_array GArray                      *allowed_ips_data = NULL;
    NMPlatformLnkWireGuard                      wg_lnk;
    gs_free NMPWireGuardPeer                   *plpeers      = NULL;
    gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL;
    guint                                       plpeers_len  = 0;
    const char                                 *setting_name;
    gboolean                                    peers_removed;
    NMPlatformWireGuardChangeFlags              wg_change_flags;
    int                                         ifindex;
    int                                         r;

    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE);

    connection = nm_device_get_applied_connection(NM_DEVICE(self));
    s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD));
    g_return_val_if_fail(s_wg, NM_ACT_STAGE_RETURN_FAILURE);

    priv->link_config_last_at = nm_utils_get_monotonic_timestamp_nsec();

    _LOGT(LOGD_DEVICE,
          "wireguard link config (%s, %s)...",
          reason,
          _link_config_mode_to_string(config_mode));

    _auto_default_route_init(self);

    if (!priv->dns_manager) {
        priv->dns_manager = g_object_ref(nm_dns_manager_get());
        g_signal_connect(priv->dns_manager,
                         NM_DNS_MANAGER_CONFIG_CHANGED,
                         G_CALLBACK(_dns_config_changed),
                         self);
    }

    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)
        && (setting_name = nm_connection_need_secrets(connection, NULL))) {
        NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self));

        _LOGD(LOGD_DEVICE,
              "Activation: connection '%s' has security, but secrets are required.",
              nm_connection_get_id(connection));

        ret = _secrets_handle_auth_or_fail(self, req, FALSE);
        if (ret != NM_ACT_STAGE_RETURN_SUCCESS) {
            if (ret != NM_ACT_STAGE_RETURN_POSTPONE) {
                nm_assert(ret == NM_ACT_STAGE_RETURN_FAILURE);
                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
            }
            return ret;
        }
    }

    ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
    if (ifindex <= 0) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    _peers_update_all(self, s_wg, &peers_removed);

    wg_lnk = (NMPlatformLnkWireGuard) {};

    wg_change_flags = NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE;

    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)
        || (NM_IN_SET(config_mode, LINK_CONFIG_MODE_REAPPLY) && peers_removed))
        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS;

    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) {
        wg_lnk.listen_port = nm_setting_wireguard_get_listen_port(s_wg);
        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_LISTEN_PORT;

        wg_lnk.fwmark = priv->auto_default_route_fwmark;
        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_FWMARK;

        if (nm_utils_base64secret_decode(nm_setting_wireguard_get_private_key(s_wg),
                                         sizeof(wg_lnk.private_key),
                                         wg_lnk.private_key)) {
            wg_lnk_clear_private_key = NM_SECRET_PTR_ARRAY(wg_lnk.private_key);
            wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY;
        } else {
            if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)) {
                _LOGD(LOGD_DEVICE, "the provided private-key is invalid");
                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
                return NM_ACT_STAGE_RETURN_FAILURE;
            }
        }
    }

    _peers_get_platform_list(priv,
                             config_mode,
                             &plpeers,
                             &plpeer_flags,
                             &plpeers_len,
                             &allowed_ips_data);

    r = nm_platform_link_wireguard_change(nm_device_get_platform(NM_DEVICE(self)),
                                          ifindex,
                                          &wg_lnk,
                                          plpeers,
                                          plpeer_flags,
                                          plpeers_len,
                                          wg_change_flags);

    nm_explicit_bzero(plpeers, sizeof(plpeers[0]) * plpeers_len);

    if (r < 0) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    return NM_ACT_STAGE_RETURN_SUCCESS;
}

static void
link_config_delayed(NMDeviceWireGuard *self, const char *reason)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    gint64                    now;

    priv->link_config_delayed_id = 0;

    if (priv->link_config_last_at != 0) {
        now = nm_utils_get_monotonic_timestamp_nsec();
        if (now < priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC) {
            /* we ratelimit calls to link_config(), because we call this whenever a resolver
             * completes. */
            _LOGT(LOGD_DEVICE, "wireguard link config (%s) (postponed)", reason);
            priv->link_config_delayed_id =
                g_timeout_add(NM_MAX((priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC - now)
                                         / NM_UTILS_NSEC_PER_MSEC,
                                     (gint64) 1),
                              link_config_delayed_ratelimit_cb,
                              self);
            return;
        }
    }

    link_config(self, reason, LINK_CONFIG_MODE_ENDPOINTS, NULL);
}

static gboolean
link_config_delayed_ratelimit_cb(gpointer user_data)
{
    link_config_delayed(user_data, "after-ratelimiting");
    return G_SOURCE_REMOVE;
}

static gboolean
link_config_delayed_resolver_cb(gpointer user_data)
{
    link_config_delayed(user_data, "resolver-update");
    return G_SOURCE_REMOVE;
}

static NMActStageReturn
act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
{
    NMDeviceWireGuard        *self = NM_DEVICE_WIREGUARD(device);
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    NMDeviceManagedType       managed_type;
    NMDeviceStateReason       failure_reason;
    NMActStageReturn          ret;

    managed_type = nm_device_managed_type_get(device);

    if (managed_type == NM_DEVICE_MANAGED_TYPE_EXTERNAL) {
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE);
        return NM_ACT_STAGE_RETURN_SUCCESS;
    }

    ret = link_config(NM_DEVICE_WIREGUARD(device),
                      "configure",
                      (managed_type == NM_DEVICE_MANAGED_TYPE_ASSUME) ? LINK_CONFIG_MODE_ASSUME
                                                                      : LINK_CONFIG_MODE_FULL,
                      &failure_reason);

    if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
        if (managed_type == NM_DEVICE_MANAGED_TYPE_ASSUME) {
            /* this never fails. */
            return NM_ACT_STAGE_RETURN_SUCCESS;
        }

        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, failure_reason);
        NM_SET_OUT(out_failure_reason, failure_reason);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    nm_assert(NM_IN_SET(ret, NM_ACT_STAGE_RETURN_SUCCESS, NM_ACT_STAGE_RETURN_POSTPONE));

    if (ret == NM_ACT_STAGE_RETURN_SUCCESS && _peers_resolving_cnt(priv) > 0u) {
        _LOGT(LOGD_DEVICE,
              "activation delayed to resolve DNS names of peers: resolving and waiting...");
        return NM_ACT_STAGE_RETURN_POSTPONE;
    }

    return ret;
}

static gboolean
skip_peer_route(const NMIPAddr    *peer_addr,
                guint              peer_addr_prefix,
                int                addr_family,
                NMSettingIPConfig *s_ip)
{
    guint num_addresses;
    guint i;

    /*
     * If the allowed-ip subnet is already reachable on the interface via the
     * prefix route of a static IP address, skip adding the peer route.
     * We don't want to override the prefix route with a new one because the
     * prefix route also specifies the correct source IP address.
     *
     * wg-quick does something similar here:
     * https://git.zx2c4.com/wireguard-tools/tree/src/wg-quick/linux.bash?h=v1.0.20250521#n177
     * The condition in wg-quick is a bit different because it checks that no
     * duplicate route exists on the interface. We can't do exactly the same
     * because here we don't have visibility on all the platform routes.
     */

    if (!s_ip)
        return FALSE;

    num_addresses = nm_setting_ip_config_get_num_addresses(s_ip);
    for (i = 0; i < num_addresses; i++) {
        NMIPAddr     setting_addr;
        NMIPAddr     peer_addr_tmp;
        guint        setting_prefix;
        NMIPAddress *a;

        peer_addr_tmp = *peer_addr;

        a = nm_setting_ip_config_get_address(s_ip, i);
        nm_ip_address_get_address_binary(a, &setting_addr);
        setting_prefix = nm_ip_address_get_prefix(a);

        if (setting_prefix > peer_addr_prefix)
            continue;

        nm_ip_addr_clear_host_address(addr_family, &setting_addr, NULL, setting_prefix);
        nm_ip_addr_clear_host_address(addr_family, &peer_addr_tmp, NULL, setting_prefix);

        if (nm_ip_addr_equal(addr_family, &peer_addr_tmp, &setting_addr))
            return TRUE;
    }

    return FALSE;
}

static const NML3ConfigData *
_get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
{
    NMDeviceWireGuardPrivate               *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
    NMConnection                           *connection;
    NMSettingWireGuard                     *s_wg;
    guint                                   n_peers;
    guint                                   i;
    int                                     ip_ifindex;
    guint32                                 route_metric;
    guint32                                 route_table_coerced;
    gboolean                                auto_default_route_enabled;

    _auto_default_route_init(self);

    connection = nm_device_get_applied_connection(NM_DEVICE(self));

    s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD));

    /* Differences to `wg-quick`.
     *
     * `wg-quick` supports the "Table" setting with 3 modes:
     *
     * a1) "off": this is what we do with "peer-routes" disabled.
     *
     * a2) an explicit routing table. This is our behavior with "peer-routes" on. In this case
     *   we honor the "ipv4.route-table" and "ipv6.route-table" settings. One difference is that
     *   `wg-quick` would resolve table names from /etc/iproute2/rt_tables. Our connection profiles
     *   only contain table numbers, so that conversion from name to table must have happened
     *   before already.
     *
     * a3) "auto" (the default). In this case, `wg-quick` would only add the route to the
     *   main table, if the AllowedIP range is not yet reachable on the link. With "peer-routes"
     *   enabled, we don't check for that and always add the routes to the main-table
     *   (with 'ipv4.route-table' and 'ipv6.route-table' set to zero or RT_TABLE_MAIN (254)).
     *
     *   Also, in "auto" mode, `wg-quick` would add special handling for /0 routes and pick
     *   an empty table to configure policy routing to avoid routing loops. This handling
     *   of routing-loops via policy routing is not yet done, and requires a separate solution
     *   from constructing the peer-routes here.
     */
    if (!nm_setting_wireguard_get_peer_routes(s_wg))
        return NULL;

    ip_ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));

    if (ip_ifindex <= 0)
        return NULL;

    route_metric = nm_device_get_route_metric(NM_DEVICE(self), addr_family);

    route_table_coerced =
        nm_platform_route_table_coerce(nm_device_get_route_table(NM_DEVICE(self), addr_family));

    auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4
                                                          : priv->auto_default_route_enabled_6;

    n_peers = nm_setting_wireguard_get_peers_len(s_wg);
    for (i = 0; i < n_peers; i++) {
        NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i);
        guint            n_aips;
        guint            j;

        n_aips = nm_wireguard_peer_get_allowed_ips_len(peer);
        for (j = 0; j < n_aips; j++) {
            NMSettingIPConfig *s_ip;
            NMPlatformIPXRoute rt;
            NMIPAddr           addrbin;
            const char        *aip;
            gboolean           valid;
            int                prefix;
            guint32            rtable_coerced;

            aip  = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
            s_ip = nm_connection_get_setting_ip_config(connection, addr_family);

            if (!valid || !nm_inet_parse_with_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
                continue;

            if (prefix < 0)
                prefix = (addr_family == AF_INET) ? 32 : 128;

            if (prefix == 0) {
                if (nm_setting_ip_config_get_never_default(s_ip))
                    continue;
            }

            if (!l3cd) {
                l3cd = nm_device_create_l3_config_data(NM_DEVICE(self), NM_IP_CONFIG_SOURCE_USER);
                nm_l3_config_data_set_flags(l3cd,
                                            NM_L3_CONFIG_DAT_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES);
            }

            nm_ip_addr_clear_host_address(addr_family, &addrbin, NULL, prefix);

            if (skip_peer_route(&addrbin, prefix, addr_family, s_ip))
                continue;

            rtable_coerced = route_table_coerced;

            if (prefix == 0 && auto_default_route_enabled) {
                /* In auto-default-route mode, we place the default route in a table that
                 * has the same number as the fwmark. wg-quick does that too. If you don't
                 * like that, configure the rules and the default-route explicitly in the
                 * connection profile. */
                rtable_coerced = nm_platform_route_table_coerce(priv->auto_default_route_fwmark);
            }

            if (addr_family == AF_INET) {
                rt.r4 = (NMPlatformIP4Route) {
                    .network       = addrbin.addr4,
                    .plen          = prefix,
                    .ifindex       = ip_ifindex,
                    .rt_source     = NM_IP_CONFIG_SOURCE_USER,
                    .table_coerced = rtable_coerced,
                    .metric        = route_metric,
                };
            } else {
                rt.r6 = (NMPlatformIP6Route) {
                    .network       = addrbin.addr6,
                    .plen          = prefix,
                    .ifindex       = ip_ifindex,
                    .rt_source     = NM_IP_CONFIG_SOURCE_USER,
                    .table_coerced = rtable_coerced,
                    .metric        = route_metric,
                };
            }

            nm_l3_config_data_add_route(l3cd, addr_family, NULL, &rt.rx);
        }
    }

    if (!l3cd)
        return NULL;

    return nm_l3_config_data_seal(g_steal_pointer(&l3cd));
}

static void
act_stage3_ip_config(NMDevice *device, int addr_family)
{
    nm_auto_unref_l3cd const NML3ConfigData *l3cd = NULL;

    l3cd = _get_dev2_ip_config(NM_DEVICE_WIREGUARD(device), addr_family);
    _configure_firewall(NM_DEVICE_WIREGUARD(device),
                        nm_device_get_applied_connection(device),
                        addr_family,
                        TRUE);
    nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, l3cd);
}

static guint32
get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
{
    /* When "MTU" for `wg-quick up` is unset, it calls `ip route get` for
     * each configured endpoint, to determine the suitable MTU how to reach
     * each endpoint.
     * For `wg-quick` this works very well, because whenever the script runs it
     * determines the best setting at that point in time. It's simply not concerned
     * with what happens later (and it's not around anyway).
     *
     * NetworkManager sticks around, so the right MTU would need to be re-determined
     * whenever anything relevant changes. Which basically means, to re-evaluate whenever
     * something related to addresses or routing changes (which happens all the time).
     *
     * The correct MTU indeed depends on the MTU setting of other interfaces (or routes).
     * But it's still odd, that activating/deactivating a seemingly unrelated interface
     * would trigger an MTU change. It's odd to explain/document and odd to implemented
     * -- despite this being the reality.
     *
     * For now, only support configuring an explicit MTU, or leave the setting untouched.
     * The same limitation also applies to other "ip-tunnel" types, where we could use
     * similar smarts for autodetecting the MTU.
     */
    return nm_device_get_configured_mtu_from_connection(device,
                                                        NM_TYPE_SETTING_WIREGUARD,
                                                        out_source);
}

static void
_device_cleanup(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    _peers_remove_all(self);

    _secrets_cancel(self);

    priv->auto_default_route_initialized          = FALSE;
    priv->auto_default_route_priority_initialized = FALSE;
}

static void
device_state_changed(NMDevice           *device,
                     NMDeviceState       new_state,
                     NMDeviceState       old_state,
                     NMDeviceStateReason reason)
{
    if (new_state <= NM_DEVICE_STATE_ACTIVATED)
        return;

    _device_cleanup(NM_DEVICE_WIREGUARD(device));
}

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

static gboolean
can_reapply_change(NMDevice   *device,
                   const char *setting_name,
                   NMSetting  *s_old,
                   NMSetting  *s_new,
                   GHashTable *diffs,
                   GError    **error)
{
    if (nm_streq(setting_name, NM_SETTING_WIREGUARD_SETTING_NAME)) {
        /* Most, but not all WireGuard settings can be reapplied. Whitelist.
         *
         * MTU cannot be reapplied. */
        return nm_device_hash_check_invalid_keys(diffs,
                                                 NM_SETTING_WIREGUARD_SETTING_NAME,
                                                 error,
                                                 NM_SETTING_WIREGUARD_FWMARK,
                                                 NM_SETTING_WIREGUARD_IP4_AUTO_DEFAULT_ROUTE,
                                                 NM_SETTING_WIREGUARD_IP6_AUTO_DEFAULT_ROUTE,
                                                 NM_SETTING_WIREGUARD_LISTEN_PORT,
                                                 NM_SETTING_WIREGUARD_PEERS,
                                                 NM_SETTING_WIREGUARD_PEER_ROUTES,
                                                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
                                                 NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS);
    }

    return NM_DEVICE_CLASS(nm_device_wireguard_parent_class)
        ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
}

static void
reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
{
    NMDeviceWireGuard        *self  = NM_DEVICE_WIREGUARD(device);
    NMDeviceWireGuardPrivate *priv  = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
    NMDeviceState             state = nm_device_get_state(device);

    NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->reapply_connection(device, con_old, con_new);

    if (state >= NM_DEVICE_STATE_CONFIG) {
        priv->auto_default_route_refresh = TRUE;

        _configure_firewall(self, con_old, AF_INET, FALSE);
        _configure_firewall(self, con_old, AF_INET6, FALSE);

        link_config(NM_DEVICE_WIREGUARD(device), "reapply", LINK_CONFIG_MODE_REAPPLY, NULL);
    }

    if (state >= NM_DEVICE_STATE_IP_CONFIG) {
        nm_auto_unref_l3cd const NML3ConfigData *l3cd_4 = NULL;
        nm_auto_unref_l3cd const NML3ConfigData *l3cd_6 = NULL;

        l3cd_4 = _get_dev2_ip_config(self, AF_INET);
        l3cd_6 = _get_dev2_ip_config(self, AF_INET6);

        nm_device_devip_set_state(device, AF_INET, NM_DEVICE_IP_STATE_READY, l3cd_4);
        nm_device_devip_set_state(device, AF_INET6, NM_DEVICE_IP_STATE_READY, l3cd_6);
    }
}

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

static void
update_connection(NMDevice *device, NMConnection *connection)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(device);
    NMSettingWireGuard *s_wg = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_WIREGUARD);
    const NMPObject    *obj_wg;
    const NMPObjectLnkWireGuard *olnk_wg;
    guint                        i;

    g_object_set(s_wg,
                 NM_SETTING_WIREGUARD_FWMARK,
                 (guint) priv->lnk_curr.fwmark,
                 NM_SETTING_WIREGUARD_LISTEN_PORT,
                 (guint) priv->lnk_curr.listen_port,
                 NULL);

    obj_wg = NMP_OBJECT_UP_CAST(nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device),
                                                                   nm_device_get_ip_ifindex(device),
                                                                   NULL));
    if (!obj_wg)
        return;

    olnk_wg = &obj_wg->_lnk_wireguard;

    for (i = 0; i < olnk_wg->peers_len; i++) {
        nm_auto_unref_wgpeer NMWireGuardPeer *peer  = NULL;
        const NMPWireGuardPeer               *ppeer = &olnk_wg->peers[i];

        peer = nm_wireguard_peer_new();

        _nm_wireguard_peer_set_public_key_bin(peer, ppeer->public_key);

        nm_setting_wireguard_append_peer(s_wg, peer);
    }
}

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

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMDeviceWireGuard        *self = NM_DEVICE_WIREGUARD(object);
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_PUBLIC_KEY:
        g_value_take_variant(
            value,
            nm_g_variant_new_ay(priv->lnk_curr.public_key, sizeof(priv->lnk_curr.public_key)));
        break;
    case PROP_LISTEN_PORT:
        g_value_set_uint(value, priv->lnk_curr.listen_port);
        break;
    case PROP_FWMARK:
        g_value_set_uint(value, priv->lnk_curr.fwmark);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_device_wireguard_init(NMDeviceWireGuard *self)
{
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    c_list_init(&priv->lst_peers_head);
    priv->peers = g_hash_table_new(_peer_data_hash, _peer_data_equal);
}

static void
dispose(GObject *object)
{
    NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD(object);

    _device_cleanup(self);

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

static void
finalize(GObject *object)
{
    NMDeviceWireGuard        *self = NM_DEVICE_WIREGUARD(object);
    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);

    nm_explicit_bzero(priv->lnk_curr.private_key, sizeof(priv->lnk_curr.private_key));

    if (priv->dns_manager) {
        g_signal_handlers_disconnect_by_func(priv->dns_manager, _dns_config_changed, self);
        g_object_unref(priv->dns_manager);
    }

    g_hash_table_destroy(priv->peers);

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

static const NMDBusInterfaceInfoExtended interface_info_device_wireguard = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_DEVICE_WIREGUARD,
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("PublicKey",
                                                           "ay",
                                                           NM_DEVICE_WIREGUARD_PUBLIC_KEY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ListenPort",
                                                           "q",
                                                           NM_DEVICE_WIREGUARD_LISTEN_PORT),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FwMark",
                                                           "u",
                                                           NM_DEVICE_WIREGUARD_FWMARK), ), ),
};

static void
nm_device_wireguard_class_init(NMDeviceWireGuardClass *klass)
{
    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
    NMDeviceClass     *device_class      = NM_DEVICE_CLASS(klass);

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

    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wireguard);

    device_class->connection_type_supported        = NM_SETTING_WIREGUARD_SETTING_NAME;
    device_class->connection_type_check_compatible = NM_SETTING_WIREGUARD_SETTING_NAME;
    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD);

    device_class->state_changed                                 = device_state_changed;
    device_class->create_and_realize                            = create_and_realize;
    device_class->deactivate                                    = deactivate;
    device_class->act_stage2_config                             = act_stage2_config;
    device_class->act_stage2_config_also_for_external_or_assume = TRUE;
    device_class->act_stage3_ip_config                          = act_stage3_ip_config;
    device_class->get_generic_capabilities                      = get_generic_capabilities;
    device_class->link_changed                                  = link_changed;
    device_class->update_connection                             = update_connection;
    device_class->can_reapply_change                            = can_reapply_change;
    device_class->reapply_connection                            = reapply_connection;
    device_class->get_configured_mtu                            = get_configured_mtu;
    device_class->get_extra_rules                               = get_extra_rules;
    device_class->coerce_route_table                            = coerce_route_table;

    obj_properties[PROP_PUBLIC_KEY] =
        g_param_spec_variant(NM_DEVICE_WIREGUARD_PUBLIC_KEY,
                             "",
                             "",
                             G_VARIANT_TYPE("ay"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_LISTEN_PORT] = g_param_spec_uint(NM_DEVICE_WIREGUARD_LISTEN_PORT,
                                                         "",
                                                         "",
                                                         0,
                                                         G_MAXUINT16,
                                                         0,
                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_FWMARK] = g_param_spec_uint(NM_DEVICE_WIREGUARD_FWMARK,
                                                    "",
                                                    "",
                                                    0,
                                                    G_MAXUINT32,
                                                    0,
                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}

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

#define NM_TYPE_WIREGUARD_DEVICE_FACTORY (nm_wireguard_device_factory_get_type())
#define NM_WIREGUARD_DEVICE_FACTORY(obj)                              \
    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                            \
                                    NM_TYPE_WIREGUARD_DEVICE_FACTORY, \
                                    NMWireGuardDeviceFactory))

static NMDevice *
create_device(NMDeviceFactory      *factory,
              const char           *iface,
              const NMPlatformLink *plink,
              NMConnection         *connection,
              gboolean             *out_ignore)
{
    return g_object_new(NM_TYPE_DEVICE_WIREGUARD,
                        NM_DEVICE_IFACE,
                        iface,
                        NM_DEVICE_TYPE_DESC,
                        "WireGuard",
                        NM_DEVICE_DEVICE_TYPE,
                        NM_DEVICE_TYPE_WIREGUARD,
                        NM_DEVICE_LINK_TYPE,
                        NM_LINK_TYPE_WIREGUARD,
                        NULL);
}

NM_DEVICE_FACTORY_DEFINE_INTERNAL(
    WIREGUARD,
    WireGuard,
    wireguard,
    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD)
        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIREGUARD_SETTING_NAME),
    factory_class->create_device = create_device;)
