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

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

#include "nm-ip-config.h"

#include <linux/rtnetlink.h>

#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
#include "nm-l3cfg.h"
#include "NetworkManagerUtils.h"

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

#define NOTIFY_PLATFORM_RATELIMIT_MSEC 333

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

GType nm_ip4_config_get_type(void);
GType nm_ip6_config_get_type(void);

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

#define NM_IP_CONFIG_ADDRESS_DATA "address-data"
#define NM_IP_CONFIG_DNS_OPTIONS  "dns-options"
#define NM_IP_CONFIG_DNS_PRIORITY "dns-priority"
#define NM_IP_CONFIG_DOMAINS      "domains"
#define NM_IP_CONFIG_GATEWAY      "gateway"
#define NM_IP_CONFIG_ROUTE_DATA   "route-data"
#define NM_IP_CONFIG_SEARCHES     "searches"

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

typedef struct _NMIPConfigPrivate NMIPConfigPrivate;

NM_GOBJECT_PROPERTIES_DEFINE_FULL(_ip,
                                  NMIPConfig,
                                  PROP_IP_L3CFG,
                                  PROP_IP_ADDRESS_DATA,
                                  PROP_IP_GATEWAY,
                                  PROP_IP_ROUTE_DATA,
                                  PROP_IP_DOMAINS,
                                  PROP_IP_SEARCHES,
                                  PROP_IP_DNS_PRIORITY,
                                  PROP_IP_DNS_OPTIONS, );

G_DEFINE_ABSTRACT_TYPE(NMIPConfig, nm_ip_config, NM_TYPE_DBUS_OBJECT)

#define NM_IP_CONFIG_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMIPConfig, NM_IS_IP_CONFIG)

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

static void _handle_platform_change(NMIPConfig *self, guint64 obj_type_flags, gboolean is_init);
static void _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd);

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

static void
_value_set_variant_as(GValue *value, const char *const *strv, guint len)
{
    if (len > 0) {
        nm_assert(strv && strv[0]);
        g_value_set_variant(value, g_variant_new_strv((const char *const *) strv, len));
    } else
        g_value_set_variant(value, nm_g_variant_singleton_as());
}

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

static void
_notify_platform_handle(NMIPConfig *self, gint64 now_msec)
{
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
    guint64            obj_type_flags;

    nm_clear_g_source_inst(&priv->notify_platform_timeout_source);

    priv->notify_platform_rlimited_until_msec = now_msec + NOTIFY_PLATFORM_RATELIMIT_MSEC;

    obj_type_flags = nm_steal_int(&priv->notify_platform_obj_type_flags);

    nm_assert(obj_type_flags != 0u);

    _handle_platform_change(self, obj_type_flags, FALSE);
}

static gboolean
_notify_platform_cb(gpointer user_data)
{
    _notify_platform_handle(user_data, nm_utils_get_monotonic_timestamp_msec());
    return G_SOURCE_CONTINUE;
}

static void
_notify_platform(NMIPConfig *self, guint64 obj_type_flags)
{
    const int          addr_family = nm_ip_config_get_addr_family(self);
    const int          IS_IPv4     = NM_IS_IPv4(addr_family);
    NMIPConfigPrivate *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
    gint64             now_msec;

    obj_type_flags &= (nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4))
                       | nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)));

    if (obj_type_flags == 0u)
        return;

    priv->notify_platform_obj_type_flags |= obj_type_flags;

    if (priv->notify_platform_timeout_source) {
        /* We are currently rate limited. Don't bother to check whether
         * (now_msec < priv->notify_platform_rlimited_until_msec), just always
         * delegate to the timeout handler. It is scheduled with a lower idle
         * priority, so we want that additional backoff. */
        return;
    }

    now_msec = nm_utils_get_monotonic_timestamp_msec();

    if (now_msec < priv->notify_platform_rlimited_until_msec) {
        priv->notify_platform_timeout_source = nm_g_source_attach(
            /* Schedule with a low G_PRIORITY_LOW. */
            nm_g_timeout_source_new(priv->notify_platform_rlimited_until_msec - now_msec,
                                    G_PRIORITY_LOW - 10,
                                    _notify_platform_cb,
                                    self,
                                    NULL),
            NULL);
        return;
    }

    _notify_platform_handle(self, now_msec);
}

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

static void
_l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMIPConfig *self)
{
    switch (notify_data->notify_type) {
    case NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT:
        if (notify_data->commit.l3cd_changed)
            _handle_l3cd_changed(self, notify_data->commit.l3cd_new);
        break;
    case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
        _notify_platform(self, notify_data->platform_change_on_idle.obj_type_flags);
        break;
    default:
        break;
    }
}

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

static void
get_property_ip(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMIPConfig        *self        = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
    const int          addr_family = nm_ip_config_get_addr_family(self);
    char             **to_free     = NULL;
    char               sbuf_addr[NM_INET_ADDRSTRLEN];
    const char *const *strv;
    guint              len;
    int                v_i;

    switch (prop_id) {
    case PROP_IP_ADDRESS_DATA:
        g_value_set_variant(value, priv->v_address_data);
        break;
    case PROP_IP_GATEWAY:
        if (priv->v_gateway.best_default_route) {
            const NMIPAddr *gateway;

            gateway = nm_platform_ip_route_get_gateway(
                addr_family,
                NMP_OBJECT_CAST_IP_ROUTE(priv->v_gateway.best_default_route));
            g_value_set_variant(
                value,
                g_variant_new_string(nm_inet_ntop(addr_family, gateway, sbuf_addr)));
        } else
            g_value_set_variant(value, nm_g_variant_singleton_s_empty());
        break;
    case PROP_IP_ROUTE_DATA:
        g_value_set_variant(value, priv->v_route_data);
        break;
    case PROP_IP_DOMAINS:
        strv = nm_l3_config_data_get_domains(priv->l3cd, addr_family, &len);
        _value_set_variant_as(value, strv, len);
        break;
    case PROP_IP_SEARCHES:
        strv = nm_l3_config_data_get_searches(priv->l3cd, addr_family, &len);
        if (strv) {
            strv = nm_utils_buf_utf8safe_escape_strv(
                strv,
                len,
                NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
                    | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII,
                &to_free);
        }

        _value_set_variant_as(value, strv, len);

        if (to_free) {
            g_strfreev(to_free);
        }
        break;
    case PROP_IP_DNS_PRIORITY:
        v_i = nm_l3_config_data_get_dns_priority_or_default(priv->l3cd, addr_family);
        g_value_set_variant(value,
                            (v_i == 0) ? nm_g_variant_singleton_i_0() : g_variant_new_int32(v_i));
        break;
    case PROP_IP_DNS_OPTIONS:
        strv = nm_l3_config_data_get_dns_options(priv->l3cd, addr_family, &len);
        _value_set_variant_as(value, strv, len);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NMIPConfig        *self = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
    gpointer           ptr;

    switch (prop_id) {
    case PROP_IP_L3CFG:
        /* construct-only */
        ptr = g_value_get_pointer(value);
        nm_assert(NM_IS_L3CFG(ptr));
        priv->l3cfg = g_object_ref(ptr);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_ip_config_init(NMIPConfig *self)
{}

NMIPConfig *
nm_ip_config_new(int addr_family, NML3Cfg *l3cfg)
{
    nm_assert_addr_family(addr_family);
    nm_assert(NM_L3CFG(l3cfg));

    return g_object_new(NM_IS_IPv4(addr_family) ? nm_ip4_config_get_type()
                                                : nm_ip6_config_get_type(),
                        NM_IP_CONFIG_L3CFG,
                        l3cfg,
                        NULL);
}

static void
constructed(GObject *object)
{
    NMIPConfig        *self = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);

    priv->l3cfg_notify_id =
        g_signal_connect(priv->l3cfg, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(_l3cfg_notify_cb), self);

    priv->l3cd = nm_l3_config_data_ref(nm_l3cfg_get_combined_l3cd(priv->l3cfg, TRUE));

    _handle_platform_change(self, ~((guint32) 0u), TRUE);

    G_OBJECT_CLASS(nm_ip_config_parent_class)->constructed(object);
}

void
nm_ip_config_take_and_unexport_on_idle(NMIPConfig *self_take)
{
    if (self_take)
        nm_dbus_object_unexport_on_idle(g_steal_pointer(&self_take));
}

static void
finalize(GObject *object)
{
    NMIPConfig        *self = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);

    nm_clear_g_source_inst(&priv->notify_platform_timeout_source);

    nm_clear_g_signal_handler(priv->l3cfg, &priv->l3cfg_notify_id);

    g_object_unref(priv->l3cfg);

    nm_g_variant_unref(priv->v_address_data);
    nm_g_variant_unref(priv->v_addresses);
    nm_g_variant_unref(priv->v_route_data);
    nm_g_variant_unref(priv->v_routes);

    nmp_object_unref(priv->v_gateway.best_default_route);

    nm_l3_config_data_unref(priv->l3cd);

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

static void
nm_ip_config_class_init(NMIPConfigClass *klass)
{
    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);

    object_class->get_property = get_property_ip;
    object_class->set_property = set_property;
    object_class->constructed  = constructed;
    object_class->finalize     = finalize;

    dbus_object_class->export_on_construction = TRUE;

    obj_properties_ip[PROP_IP_L3CFG] =
        g_param_spec_pointer(NM_IP_CONFIG_L3CFG,
                             "",
                             "",
                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties_ip[PROP_IP_ADDRESS_DATA] =
        g_param_spec_variant(NM_IP_CONFIG_ADDRESS_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_GATEWAY] =
        g_param_spec_variant(NM_IP_CONFIG_GATEWAY,
                             "",
                             "",
                             G_VARIANT_TYPE("s"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_ROUTE_DATA] =
        g_param_spec_variant(NM_IP_CONFIG_ROUTE_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_DOMAINS] =
        g_param_spec_variant(NM_IP_CONFIG_DOMAINS,
                             "",
                             "",
                             G_VARIANT_TYPE("as"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_SEARCHES] =
        g_param_spec_variant(NM_IP_CONFIG_SEARCHES,
                             "",
                             "",
                             G_VARIANT_TYPE("as"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_DNS_PRIORITY] =
        g_param_spec_variant(NM_IP_CONFIG_DNS_PRIORITY,
                             "",
                             "",
                             G_VARIANT_TYPE("i"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip[PROP_IP_DNS_OPTIONS] =
        g_param_spec_variant(NM_IP_CONFIG_DNS_OPTIONS,
                             "",
                             "",
                             G_VARIANT_TYPE("as"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST_ip, obj_properties_ip);
}

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

/* public */
#define NM_IP4_CONFIG_NAMESERVER_DATA  "nameserver-data"
#define NM_IP4_CONFIG_WINS_SERVER_DATA "wins-server-data"

/* deprecated */
#define NM_IP4_CONFIG_ADDRESSES    "addresses"
#define NM_IP4_CONFIG_NAMESERVERS  "nameservers"
#define NM_IP4_CONFIG_ROUTES       "routes"
#define NM_IP4_CONFIG_WINS_SERVERS "wins-servers"

typedef struct _NMIP4Config      NMIP4Config;
typedef struct _NMIP4ConfigClass NMIP4ConfigClass;

NM_GOBJECT_PROPERTIES_DEFINE_FULL(_ip4,
                                  NMIP4Config,
                                  PROP_IP4_ADDRESSES,
                                  PROP_IP4_NAMESERVERS,
                                  PROP_IP4_NAMESERVER_DATA,
                                  PROP_IP4_ROUTES,
                                  PROP_IP4_WINS_SERVERS,
                                  PROP_IP4_WINS_SERVER_DATA, );

struct _NMIP4Config {
    NMIPConfig parent;
};

struct _NMIP4ConfigClass {
    NMIPConfigClass parent;
};

G_DEFINE_TYPE(NMIP4Config, nm_ip4_config, NM_TYPE_IP_CONFIG)

static void
get_property_ip4(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMIPConfig        *self = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
    char               addr_str[NM_INET_ADDRSTRLEN];
    GVariantBuilder    builder;
    const in_addr_t   *addrs;
    const char *const *strarr;
    guint              len;
    guint              i;

    switch (prop_id) {
    case PROP_IP4_ADDRESSES:
        g_value_set_variant(value, priv->v_addresses);
        break;
    case PROP_IP4_ROUTES:
        g_value_set_variant(value, priv->v_routes);
        break;
    case PROP_IP4_NAMESERVERS:
    case PROP_IP4_NAMESERVER_DATA:
        strarr = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET, &len);
        if (len == 0) {
            g_value_set_variant(value,
                                (prop_id == PROP_IP4_NAMESERVERS)
                                    ? nm_g_variant_singleton_au()
                                    : nm_g_variant_singleton_aaLsvI());
        } else {
            if (prop_id == PROP_IP4_NAMESERVERS)
                g_variant_builder_init(&builder, G_VARIANT_TYPE("au"));
            else
                g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
            for (i = 0; i < len; i++) {
                NMIPAddr a;

                if (prop_id == PROP_IP4_NAMESERVERS) {
                    if (!nm_dns_uri_parse_plain(AF_INET, strarr[i], NULL, &a))
                        continue;
                    g_variant_builder_add(&builder, "u", a);
                } else {
                    GVariantBuilder nested_builder;
                    char            addrstr[NM_INET_ADDRSTRLEN];

                    g_variant_builder_init(&nested_builder, G_VARIANT_TYPE("a{sv}"));
                    if (nm_dns_uri_parse_plain(AF_INET, strarr[i], addrstr, NULL)) {
                        g_variant_builder_add(&nested_builder,
                                              "{sv}",
                                              "address",
                                              g_variant_new_string(addrstr));
                    }
                    g_variant_builder_add(&nested_builder,
                                          "{sv}",
                                          "uri",
                                          g_variant_new_string(strarr[i]));
                    g_variant_builder_add(&builder, "a{sv}", &nested_builder);
                }
            }

            g_value_take_variant(value, g_variant_builder_end(&builder));
        }
        break;
    case PROP_IP4_WINS_SERVERS:
        addrs = nm_l3_config_data_get_wins(priv->l3cd, &len);
        g_value_set_variant(value,
                            (len == 0) ? nm_g_variant_singleton_au()
                                       : nm_g_variant_new_au(addrs, len));
        break;
    case PROP_IP4_WINS_SERVER_DATA:
        addrs = nm_l3_config_data_get_wins(priv->l3cd, &len);
        if (len == 0)
            g_value_set_variant(value, nm_g_variant_singleton_as());
        else {
            g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
            for (i = 0; i < len; i++)
                g_variant_builder_add(&builder, "s", nm_inet4_ntop(addrs[i], addr_str));
            g_value_take_variant(value, g_variant_builder_end(&builder));
        }
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static const NMDBusInterfaceInfoExtended interface_info_ip4_config = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_IP4_CONFIG,
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "Addresses",
                "aau",
                NM_IP4_CONFIG_ADDRESSES,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                           "aa{sv}",
                                                           NM_IP_CONFIG_ADDRESS_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "Routes",
                "aau",
                NM_IP4_CONFIG_ROUTES,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RouteData",
                                                           "aa{sv}",
                                                           NM_IP_CONFIG_ROUTE_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("NameserverData",
                                                           "aa{sv}",
                                                           NM_IP4_CONFIG_NAMESERVER_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "Nameservers",
                "au",
                NM_IP4_CONFIG_NAMESERVERS,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Domains", "as", NM_IP_CONFIG_DOMAINS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Searches", "as", NM_IP_CONFIG_SEARCHES),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DnsOptions",
                                                           "as",
                                                           NM_IP_CONFIG_DNS_OPTIONS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DnsPriority",
                                                           "i",
                                                           NM_IP_CONFIG_DNS_PRIORITY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WinsServerData",
                                                           "as",
                                                           NM_IP4_CONFIG_WINS_SERVER_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "WinsServers",
                "au",
                NM_IP4_CONFIG_WINS_SERVERS,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
};

static void
nm_ip4_config_init(NMIP4Config *self)
{}

static void
nm_ip4_config_class_init(NMIP4ConfigClass *klass)
{
    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
    NMIPConfigClass   *ip_config_class   = NM_IP_CONFIG_CLASS(klass);

    ip_config_class->addr_family = AF_INET;

    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/IP4Config");
    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_ip4_config);

    object_class->get_property = get_property_ip4;

    obj_properties_ip4[PROP_IP4_ADDRESSES] =
        g_param_spec_variant(NM_IP4_CONFIG_ADDRESSES,
                             "",
                             "",
                             G_VARIANT_TYPE("aau"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip4[PROP_IP4_ROUTES] =
        g_param_spec_variant(NM_IP4_CONFIG_ROUTES,
                             "",
                             "",
                             G_VARIANT_TYPE("aau"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip4[PROP_IP4_NAMESERVER_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_NAMESERVER_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip4[PROP_IP4_NAMESERVERS] =
        g_param_spec_variant(NM_IP4_CONFIG_NAMESERVERS,
                             "",
                             "",
                             G_VARIANT_TYPE("au"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip4[PROP_IP4_WINS_SERVER_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_WINS_SERVER_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("as"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip4[PROP_IP4_WINS_SERVERS] =
        g_param_spec_variant(NM_IP4_CONFIG_WINS_SERVERS,
                             "",
                             "",
                             G_VARIANT_TYPE("au"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST_ip4, obj_properties_ip4);
}

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

/* public */
#define NM_IP6_CONFIG_NAMESERVERS "nameservers"

/* deprecated */
#define NM_IP6_CONFIG_ADDRESSES "addresses"
#define NM_IP6_CONFIG_ROUTES    "routes"

typedef struct _NMIP6Config      NMIP6Config;
typedef struct _NMIP6ConfigClass NMIP6ConfigClass;

NM_GOBJECT_PROPERTIES_DEFINE_FULL(_ip6,
                                  NMIP6Config,
                                  PROP_IP6_NAMESERVERS,
                                  PROP_IP6_ADDRESSES,
                                  PROP_IP6_ROUTES, );

struct _NMIP6Config {
    NMIPConfig parent;
};

struct _NMIP6ConfigClass {
    NMIPConfigClass parent;
};

G_DEFINE_TYPE(NMIP6Config, nm_ip6_config, NM_TYPE_IP_CONFIG)

static const NMDBusInterfaceInfoExtended interface_info_ip6_config = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_IP6_CONFIG,
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "Addresses",
                "a(ayuay)",
                NM_IP6_CONFIG_ADDRESSES,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                           "aa{sv}",
                                                           NM_IP_CONFIG_ADDRESS_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                "Routes",
                "a(ayuayu)",
                NM_IP6_CONFIG_ROUTES,
                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RouteData",
                                                           "aa{sv}",
                                                           NM_IP_CONFIG_ROUTE_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Nameservers",
                                                           "aay",
                                                           NM_IP6_CONFIG_NAMESERVERS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Domains", "as", NM_IP_CONFIG_DOMAINS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Searches", "as", NM_IP_CONFIG_SEARCHES),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DnsOptions",
                                                           "as",
                                                           NM_IP_CONFIG_DNS_OPTIONS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DnsPriority",
                                                           "i",
                                                           NM_IP_CONFIG_DNS_PRIORITY), ), ),
};

static void
get_property_ip6(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMIPConfig        *self = NM_IP_CONFIG(object);
    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
    GVariantBuilder    builder;
    guint              len;
    guint              i;
    const char *const *strarr;

    switch (prop_id) {
    case PROP_IP6_ADDRESSES:
        g_value_set_variant(value, priv->v_addresses);
        break;
    case PROP_IP6_ROUTES:
        g_value_set_variant(value, priv->v_routes);
        break;
    case PROP_IP6_NAMESERVERS:
        strarr = nm_l3_config_data_get_nameservers(priv->l3cd, AF_INET6, &len);
        if (len == 0)
            g_value_set_variant(value, nm_g_variant_singleton_aay());
        else {
            g_variant_builder_init(&builder, G_VARIANT_TYPE("aay"));
            for (i = 0; i < len; i++) {
                NMIPAddr a;

                /* TODO: expose the full URI as well */
                if (!nm_dns_uri_parse_plain(AF_INET6, strarr[i], NULL, &a))
                    continue;

                g_variant_builder_add(&builder, "@ay", nm_g_variant_new_ay_in6addr(&a.addr6));
            }
            g_value_take_variant(value, g_variant_builder_end(&builder));
        }
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
nm_ip6_config_init(NMIP6Config *self)
{}

static void
nm_ip6_config_class_init(NMIP6ConfigClass *klass)
{
    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
    NMIPConfigClass   *ip_config_class   = NM_IP_CONFIG_CLASS(klass);

    ip_config_class->addr_family = AF_INET6;

    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/IP6Config");
    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_ip6_config);

    object_class->get_property = get_property_ip6;

    obj_properties_ip6[PROP_IP6_ADDRESSES] =
        g_param_spec_variant(NM_IP6_CONFIG_ADDRESSES,
                             "",
                             "",
                             G_VARIANT_TYPE("a(ayuay)"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip6[PROP_IP6_ROUTES] =
        g_param_spec_variant(NM_IP6_CONFIG_ROUTES,
                             "",
                             "",
                             G_VARIANT_TYPE("a(ayuayu)"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties_ip6[PROP_IP6_NAMESERVERS] =
        g_param_spec_variant(NM_IP6_CONFIG_NAMESERVERS,
                             "",
                             "",
                             G_VARIANT_TYPE("aay"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST_ip6, obj_properties_ip6);
}

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

#define _notify_all(self, changed_params, n_changed_params)                     \
    G_STMT_START                                                                \
    {                                                                           \
        NMIPConfig *const        _self             = (self);                    \
        const guint              _n_changed_params = (n_changed_params);        \
        GParamSpec *const *const _changed_params   = (changed_params);          \
        guint                    _i;                                            \
                                                                                \
        if (_n_changed_params > 0) {                                            \
            nm_assert(_n_changed_params <= G_N_ELEMENTS(changed_params));       \
            if (_n_changed_params > 1)                                          \
                g_object_freeze_notify(G_OBJECT(_self));                        \
            for (_i = 0; _i < _n_changed_params; _i++)                          \
                g_object_notify_by_pspec(G_OBJECT(_self), _changed_params[_i]); \
            if (_n_changed_params > 1)                                          \
                g_object_thaw_notify(G_OBJECT(_self));                          \
        }                                                                       \
    }                                                                           \
    G_STMT_END

static void
_handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
{
    const int                                addr_family = nm_ip_config_get_addr_family(self);
    const int                                IS_IPv4     = NM_IS_IPv4(addr_family);
    NMIPConfigPrivate                       *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old    = NULL;
    GParamSpec                              *changed_params[8];
    guint                                    n_changed_params = 0;
    const char *const                       *strarr;
    const char *const                       *strarr_old;
    gconstpointer                            addrs;
    gconstpointer                            addrs_old;
    guint                                    len;
    guint                                    len_old;
    int                                      v_i;
    int                                      v_i_old;

    l3cd_old   = g_steal_pointer(&priv->l3cd);
    priv->l3cd = nm_l3_config_data_ref(l3cd);

    strarr_old = nm_l3_config_data_get_nameservers(l3cd_old, addr_family, &len_old);
    strarr     = nm_l3_config_data_get_nameservers(priv->l3cd, addr_family, &len);
    if (!nm_strv_equal_n(strarr_old, len_old, strarr, len)) {
        if (IS_IPv4) {
            changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_NAMESERVER_DATA];
            changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_NAMESERVERS];
        } else
            changed_params[n_changed_params++] = obj_properties_ip6[PROP_IP6_NAMESERVERS];
    }

    strarr_old = nm_l3_config_data_get_domains(l3cd_old, addr_family, &len_old);
    strarr     = nm_l3_config_data_get_domains(priv->l3cd, addr_family, &len);
    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
        changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DOMAINS];

    strarr_old = nm_l3_config_data_get_searches(l3cd_old, addr_family, &len_old);
    strarr     = nm_l3_config_data_get_searches(priv->l3cd, addr_family, &len);
    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
        changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_SEARCHES];

    v_i_old = nm_l3_config_data_get_dns_priority_or_default(l3cd_old, addr_family);
    v_i     = nm_l3_config_data_get_dns_priority_or_default(priv->l3cd, addr_family);
    if (v_i != v_i_old)
        changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DNS_PRIORITY];

    strarr_old = nm_l3_config_data_get_dns_options(l3cd_old, addr_family, &len_old);
    strarr     = nm_l3_config_data_get_dns_options(priv->l3cd, addr_family, &len);
    if (!nm_strv_equal_n(strarr, len, strarr_old, len_old))
        changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_DNS_OPTIONS];

    if (IS_IPv4) {
        addrs_old = nm_l3_config_data_get_wins(l3cd_old, &len_old);
        addrs     = nm_l3_config_data_get_wins(priv->l3cd, &len);
        if (!nm_memeq_n(addrs_old, len_old, addrs, len, sizeof(in_addr_t))) {
            changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_WINS_SERVER_DATA];
            changed_params[n_changed_params++] = obj_properties_ip4[PROP_IP4_WINS_SERVERS];
        }
    }

    _notify_all(self, changed_params, n_changed_params);
}

static void
_handle_platform_change(NMIPConfig *self, guint64 obj_type_flags, gboolean is_init)
{
    const int                    addr_family = nm_ip_config_get_addr_family(self);
    const int                    IS_IPv4     = NM_IS_IPv4(addr_family);
    NMIPConfigPrivate           *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
    GParamSpec                  *changed_params[5];
    guint                        n_changed_params           = 0;
    const NMDedupMultiHeadEntry *head_entry_routes          = NULL;
    gboolean                     best_default_route_changed = FALSE;

    if (NM_FLAGS_ANY(obj_type_flags,
                     (nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4))
                      | nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))))) {
        const NMPObject *best_default_route = NULL;

        head_entry_routes = nm_platform_lookup_object(nm_l3cfg_get_platform(priv->l3cfg),
                                                      NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4),
                                                      nm_l3cfg_get_ifindex(priv->l3cfg));
        if (head_entry_routes) {
            NMDedupMultiIter iter;
            const NMPObject *obj;

            nm_dedup_multi_iter_init(&iter, head_entry_routes);
            while (nm_platform_dedup_multi_iter_next_obj(&iter,
                                                         &obj,
                                                         NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))) {
                const NMPlatformIPXRoute *r = NMP_OBJECT_CAST_IPX_ROUTE(obj);

                /* Determine the gateway. That is the next hop of a route
                 *  - 0.0.0.0/0 or ::/0
                 *  - type=unicast
                 *  - table=main
                 */
                if (r->rx.plen != 0
                    || r->rx.type_coerced != nm_platform_route_type_coerce(RTN_UNICAST)
                    || r->rx.table_coerced != nm_platform_route_table_coerce(RT_TABLE_MAIN)
                    || !nm_ip_addr_is_null(addr_family, r->rx.network_ptr))
                    continue;

                if (!best_default_route
                    || NMP_OBJECT_CAST_IP_ROUTE(best_default_route)->metric > r->rx.metric)
                    best_default_route = obj;
            }
        }

        if (priv->v_gateway.best_default_route != best_default_route) {
            if (!nm_ip_addr_equal(
                    addr_family,
                    nm_platform_ip_route_get_gateway(
                        addr_family,
                        NMP_OBJECT_CAST_IP_ROUTE(priv->v_gateway.best_default_route)),
                    nm_platform_ip_route_get_gateway(addr_family,
                                                     NMP_OBJECT_CAST_IP_ROUTE(best_default_route))))
                best_default_route_changed = TRUE;

            nmp_object_ref_set(&priv->v_gateway.best_default_route, best_default_route);
        }
    }

    if (best_default_route_changed
        || NM_FLAGS_ANY(obj_type_flags,
                        nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4)))) {
        gs_unref_variant GVariant *x_address_data = NULL;
        gs_unref_variant GVariant *x_addresses    = NULL;

        nm_utils_ip_addresses_to_dbus(addr_family,
                                      nm_platform_lookup_object(nm_l3cfg_get_platform(priv->l3cfg),
                                                                NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4),
                                                                nm_l3cfg_get_ifindex(priv->l3cfg)),
                                      priv->v_gateway.best_default_route,
                                      &x_address_data,
                                      &x_addresses);

        if (!nm_g_variant_equal(priv->v_address_data, x_address_data)) {
            changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_ADDRESS_DATA];
            g_variant_ref_sink(x_address_data);
            NM_SWAP(&priv->v_address_data, &x_address_data);
        }
        if (!nm_g_variant_equal(priv->v_addresses, x_addresses)) {
            changed_params[n_changed_params++] = IS_IPv4 ? obj_properties_ip4[PROP_IP4_ADDRESSES]
                                                         : obj_properties_ip6[PROP_IP6_ADDRESSES];
            g_variant_ref_sink(x_addresses);
            NM_SWAP(&priv->v_addresses, &x_addresses);
        }
    }

    if (best_default_route_changed)
        changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_GATEWAY];

    if (NM_FLAGS_ANY(obj_type_flags, nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)))) {
        gs_unref_variant GVariant *x_route_data = NULL;
        gs_unref_variant GVariant *x_routes     = NULL;

        nm_utils_ip_routes_to_dbus(addr_family, head_entry_routes, &x_route_data, &x_routes);

        if (!nm_g_variant_equal(priv->v_route_data, x_route_data)) {
            changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_ROUTE_DATA];
            g_variant_ref_sink(x_route_data);
            NM_SWAP(&priv->v_route_data, &x_route_data);
        }
        if (!nm_g_variant_equal(priv->v_routes, x_routes)) {
            changed_params[n_changed_params++] =
                IS_IPv4 ? obj_properties_ip4[PROP_IP4_ROUTES] : obj_properties_ip6[PROP_IP6_ROUTES];
            g_variant_ref_sink(x_routes);
            NM_SWAP(&priv->v_routes, &x_routes);
        }
    }

    if (!is_init)
        _notify_all(self, changed_params, n_changed_params);
}
