/* SPDX-License-Identifier: LGPL-2.1-or-later */

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

#include "nmp-rules-manager.h"

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

#include "nm-std-aux/c-list-util.h"
#include "nmp-object.h"

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

struct _NMPRulesManager {
    NMPlatform *platform;
    GHashTable *by_obj;
    GHashTable *by_user_tag;
    GHashTable *by_data;
    guint       ref_count;
};

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

static void _rules_init(NMPRulesManager *self);

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

#define _NMLOG_DOMAIN      LOGD_PLATFORM
#define _NMLOG_PREFIX_NAME "rules-manager"

#define _NMLOG(level, ...)                                                 \
    G_STMT_START                                                           \
    {                                                                      \
        const NMLogLevel __level = (level);                                \
                                                                           \
        if (nm_logging_enabled(__level, _NMLOG_DOMAIN)) {                  \
            _nm_log(__level,                                               \
                    _NMLOG_DOMAIN,                                         \
                    0,                                                     \
                    NULL,                                                  \
                    NULL,                                                  \
                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),             \
                    _NMLOG_PREFIX_NAME _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
        }                                                                  \
    }                                                                      \
    G_STMT_END

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

static gboolean
NMP_IS_RULES_MANAGER(gpointer self)
{
    return self && ((NMPRulesManager *) self)->ref_count > 0
           && NM_IS_PLATFORM(((NMPRulesManager *) self)->platform);
}

#define _USER_TAG_LOG(user_tag) nm_hash_obfuscate_ptr(1240261787u, (user_tag))

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

typedef struct {
    const NMPObject *obj;
    gconstpointer    user_tag;
    CList            obj_lst;
    CList            user_tag_lst;

    /* track_priority_val zero is special: those are weakly tracked rules.
     * That means: NetworkManager will restore them only if it removed them earlier.
     * But it will not remove or add them otherwise.
     *
     * Otherwise, the track_priority_val goes together with track_priority_present.
     * In case of one rule being tracked multiple times (with different priorities),
     * the one with higher priority wins. See _rules_obj_get_best_data().
     * Then, the winning present state either enforces that the rule is present
     * or absent.
     *
     * If a rules is not tracked at all, it is ignored by NetworkManager. Assuming
     * that it was added externally by the user. But unlike weakly tracked rules,
     * NM will *not* restore such rules if NetworkManager themself removed them. */
    guint32 track_priority_val;
    bool    track_priority_present : 1;

    bool dirty : 1;
} RulesData;

typedef enum {
    CONFIG_STATE_NONE          = 0,
    CONFIG_STATE_ADDED_BY_US   = 1,
    CONFIG_STATE_REMOVED_BY_US = 2,

    /* ConfigState encodes whether the rule was touched by us at all (CONFIG_STATE_NONE).
     *
     * Maybe we would only need to track whether we touched the rule at all. But we
     * track it more in detail what we did: did we add it (CONFIG_STATE_ADDED_BY_US)
     * or did we remove it (CONFIG_STATE_REMOVED_BY_US)?
     * Finally, we need CONFIG_STATE_OWNED_BY_US, which means that we didn't actively
     * add/remove it, but whenever we are about to undo the add/remove, we need to do it.
     * In that sense, CONFIG_STATE_OWNED_BY_US is really just a flag that we unconditionally
     * force the state next time when necessary. */
    CONFIG_STATE_OWNED_BY_US = 3,
} ConfigState;

typedef struct {
    const NMPObject *obj;
    CList            obj_lst_head;

    /* indicates whether we configured/removed the rule (during sync()). We need that, so
     * if the rule gets untracked, that we know to remove/restore it.
     *
     * This makes NMPRulesManager stateful (beyond the configuration that indicates
     * which rules are tracked).
     * After a restart, NetworkManager would no longer remember which rules were added
     * by us.
     *
     * That is partially fixed by NetworkManager taking over the rules that it
     * actively configures (see %NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG). */
    ConfigState config_state;
} RulesObjData;

typedef struct {
    gconstpointer user_tag;
    CList         user_tag_lst_head;
} RulesUserTagData;

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

static void _rules_data_untrack(NMPRulesManager *self,
                                RulesData *      rules_data,
                                gboolean         remove_user_tag_data,
                                gboolean         make_owned_by_us);

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

static void
_rules_data_assert(const RulesData *rules_data, gboolean linked)
{
    nm_assert(rules_data);
    nm_assert(NMP_OBJECT_GET_TYPE(rules_data->obj) == NMP_OBJECT_TYPE_ROUTING_RULE);
    nm_assert(nmp_object_is_visible(rules_data->obj));
    nm_assert(rules_data->user_tag);
    nm_assert(!linked || !c_list_is_empty(&rules_data->obj_lst));
    nm_assert(!linked || !c_list_is_empty(&rules_data->user_tag_lst));
}

static guint
_rules_data_hash(gconstpointer data)
{
    const RulesData *rules_data = data;
    NMHashState      h;

    _rules_data_assert(rules_data, FALSE);

    nm_hash_init(&h, 269297543u);
    nm_platform_routing_rule_hash_update(NMP_OBJECT_CAST_ROUTING_RULE(rules_data->obj),
                                         NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID,
                                         &h);
    nm_hash_update_val(&h, rules_data->user_tag);
    return nm_hash_complete(&h);
}

static gboolean
_rules_data_equal(gconstpointer data_a, gconstpointer data_b)
{
    const RulesData *rules_data_a = data_a;
    const RulesData *rules_data_b = data_b;

    _rules_data_assert(rules_data_a, FALSE);
    _rules_data_assert(rules_data_b, FALSE);

    return rules_data_a->user_tag == rules_data_b->user_tag
           && (nm_platform_routing_rule_cmp(NMP_OBJECT_CAST_ROUTING_RULE(rules_data_a->obj),
                                            NMP_OBJECT_CAST_ROUTING_RULE(rules_data_b->obj),
                                            NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID)
               == 0);
}

static void
_rules_data_destroy(gpointer data)
{
    RulesData *rules_data = data;

    _rules_data_assert(rules_data, FALSE);

    c_list_unlink_stale(&rules_data->obj_lst);
    c_list_unlink_stale(&rules_data->user_tag_lst);
    nmp_object_unref(rules_data->obj);
    g_slice_free(RulesData, rules_data);
}

static const RulesData *
_rules_obj_get_best_data(RulesObjData *obj_data)
{
    RulesData *      rules_data;
    const RulesData *rd_best = NULL;

    c_list_for_each_entry (rules_data, &obj_data->obj_lst_head, obj_lst) {
        _rules_data_assert(rules_data, TRUE);

        if (rd_best) {
            if (rd_best->track_priority_val > rules_data->track_priority_val)
                continue;
            if (rd_best->track_priority_val == rules_data->track_priority_val) {
                if (rd_best->track_priority_present || !rules_data->track_priority_present) {
                    /* if the priorities are identical, then "present" wins over
                     * "!present" (absent). */
                    continue;
                }
            }
        }

        rd_best = rules_data;
    }

    return rd_best;
}

static guint
_rules_obj_hash(gconstpointer data)
{
    const RulesObjData *obj_data = data;
    NMHashState         h;

    nm_hash_init(&h, 432817559u);
    nm_platform_routing_rule_hash_update(NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj),
                                         NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID,
                                         &h);
    return nm_hash_complete(&h);
}

static gboolean
_rules_obj_equal(gconstpointer data_a, gconstpointer data_b)
{
    const RulesObjData *obj_data_a = data_a;
    const RulesObjData *obj_data_b = data_b;

    return (nm_platform_routing_rule_cmp(NMP_OBJECT_CAST_ROUTING_RULE(obj_data_a->obj),
                                         NMP_OBJECT_CAST_ROUTING_RULE(obj_data_b->obj),
                                         NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID)
            == 0);
}

static void
_rules_obj_destroy(gpointer data)
{
    RulesObjData *obj_data = data;

    c_list_unlink_stale(&obj_data->obj_lst_head);
    nmp_object_unref(obj_data->obj);
    g_slice_free(RulesObjData, obj_data);
}

static guint
_rules_user_tag_hash(gconstpointer data)
{
    const RulesUserTagData *user_tag_data = data;

    return nm_hash_val(644693447u, user_tag_data->user_tag);
}

static gboolean
_rules_user_tag_equal(gconstpointer data_a, gconstpointer data_b)
{
    const RulesUserTagData *user_tag_data_a = data_a;
    const RulesUserTagData *user_tag_data_b = data_b;

    return user_tag_data_a->user_tag == user_tag_data_b->user_tag;
}

static void
_rules_user_tag_destroy(gpointer data)
{
    RulesUserTagData *user_tag_data = data;

    c_list_unlink_stale(&user_tag_data->user_tag_lst_head);
    g_slice_free(RulesUserTagData, user_tag_data);
}

static RulesData *
_rules_data_lookup(GHashTable *by_data, const NMPObject *obj, gconstpointer user_tag)
{
    RulesData rules_data_needle = {
        .obj      = obj,
        .user_tag = user_tag,
    };

    return g_hash_table_lookup(by_data, &rules_data_needle);
}

/**
 * nmp_rules_manager_track:
 * @self: the #NMPRulesManager instance
 * @routing_rule: the #NMPlatformRoutingRule to track or untrack
 * @track_priority: the priority for tracking the rule. Note that
 *   negative values indicate a forced absence of the rule. Priorities
 *   are compared with their absolute values (with higher absolute
 *   value being more important). For example, if you track the same
 *   rule twice, once with priority -5 and +10, then the rule is
 *   present (because the positive number is more important).
 *   The special value 0 indicates weakly-tracked rules.
 * @user_tag: the tag associated with tracking this rule. The same tag
 *   must be used to untrack the rule later.
 * @user_tag_untrack: if not %NULL, at the same time untrack this user-tag
 *   for the same rule. Note that this is different from a plain nmp_rules_manager_untrack(),
 *   because it enforces ownership of the now tracked rule. On the other hand,
 *   a plain nmp_rules_manager_untrack() merely forgets about the tracking.
 *   The purpose here is to set this to %NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG.
 */
void
nmp_rules_manager_track(NMPRulesManager *            self,
                        const NMPlatformRoutingRule *routing_rule,
                        gint32                       track_priority,
                        gconstpointer                user_tag,
                        gconstpointer                user_tag_untrack)
{
    NMPObject         obj_stack;
    const NMPObject * p_obj_stack;
    RulesData *       rules_data;
    RulesObjData *    obj_data;
    RulesUserTagData *user_tag_data;
    gboolean          changed = FALSE;
    guint32           track_priority_val;
    gboolean          track_priority_present;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));
    g_return_if_fail(routing_rule);
    g_return_if_fail(user_tag);
    nm_assert(track_priority != G_MININT32);

    _rules_init(self);

    p_obj_stack = nmp_object_stackinit(&obj_stack, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule);

    nm_assert(nmp_object_is_visible(p_obj_stack));

    if (track_priority >= 0) {
        track_priority_val     = track_priority;
        track_priority_present = TRUE;
    } else {
        track_priority_val     = -track_priority;
        track_priority_present = FALSE;
    }

    rules_data = _rules_data_lookup(self->by_data, p_obj_stack, user_tag);

    if (!rules_data) {
        rules_data  = g_slice_new(RulesData);
        *rules_data = (RulesData){
            .obj      = nm_dedup_multi_index_obj_intern(nm_platform_get_multi_idx(self->platform),
                                                   p_obj_stack),
            .user_tag = user_tag,
            .track_priority_val     = track_priority_val,
            .track_priority_present = track_priority_present,
            .dirty                  = FALSE,
        };
        g_hash_table_add(self->by_data, rules_data);

        obj_data = g_hash_table_lookup(self->by_obj, &rules_data->obj);
        if (!obj_data) {
            obj_data  = g_slice_new(RulesObjData);
            *obj_data = (RulesObjData){
                .obj          = nmp_object_ref(rules_data->obj),
                .obj_lst_head = C_LIST_INIT(obj_data->obj_lst_head),
                .config_state = CONFIG_STATE_NONE,
            };
            g_hash_table_add(self->by_obj, obj_data);
        }
        c_list_link_tail(&obj_data->obj_lst_head, &rules_data->obj_lst);

        user_tag_data = g_hash_table_lookup(self->by_user_tag, &rules_data->user_tag);
        if (!user_tag_data) {
            user_tag_data  = g_slice_new(RulesUserTagData);
            *user_tag_data = (RulesUserTagData){
                .user_tag          = user_tag,
                .user_tag_lst_head = C_LIST_INIT(user_tag_data->user_tag_lst_head),
            };
            g_hash_table_add(self->by_user_tag, user_tag_data);
        }
        c_list_link_tail(&user_tag_data->user_tag_lst_head, &rules_data->user_tag_lst);
        changed = TRUE;
    } else {
        rules_data->dirty = FALSE;
        if (rules_data->track_priority_val != track_priority_val
            || rules_data->track_priority_present != track_priority_present) {
            rules_data->track_priority_val     = track_priority_val;
            rules_data->track_priority_present = track_priority_present;
            changed                            = TRUE;
        }
    }

    if (user_tag_untrack) {
        if (user_tag != user_tag_untrack) {
            RulesData *rules_data_untrack;

            rules_data_untrack = _rules_data_lookup(self->by_data, p_obj_stack, user_tag_untrack);
            if (rules_data_untrack)
                _rules_data_untrack(self, rules_data_untrack, FALSE, TRUE);
        } else
            nm_assert_not_reached();
    }

    _rules_data_assert(rules_data, TRUE);

    if (changed) {
        _LOGD("routing-rule: track [" NM_HASH_OBFUSCATE_PTR_FMT ",%s%u] \"%s\")",
              _USER_TAG_LOG(rules_data->user_tag),
              (rules_data->track_priority_val == 0
                   ? ""
                   : (rules_data->track_priority_present ? "+" : "-")),
              (guint) rules_data->track_priority_val,
              nmp_object_to_string(rules_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
    }
}

static void
_rules_data_untrack(NMPRulesManager *self,
                    RulesData *      rules_data,
                    gboolean         remove_user_tag_data,
                    gboolean         make_owned_by_us)
{
    RulesObjData *obj_data;

    nm_assert(NMP_IS_RULES_MANAGER(self));
    _rules_data_assert(rules_data, TRUE);
    nm_assert(self->by_data);
    nm_assert(g_hash_table_lookup(self->by_data, rules_data) == rules_data);

    _LOGD("routing-rule: untrack [" NM_HASH_OBFUSCATE_PTR_FMT "] \"%s\"",
          _USER_TAG_LOG(rules_data->user_tag),
          nmp_object_to_string(rules_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));

#if NM_MORE_ASSERTS
    {
        RulesUserTagData *user_tag_data;

        user_tag_data = g_hash_table_lookup(self->by_user_tag, &rules_data->user_tag);
        nm_assert(user_tag_data);
        nm_assert(c_list_contains(&user_tag_data->user_tag_lst_head, &rules_data->user_tag_lst));
    }
#endif

    nm_assert(!c_list_is_empty(&rules_data->user_tag_lst));

    obj_data = g_hash_table_lookup(self->by_obj, &rules_data->obj);
    nm_assert(obj_data);
    nm_assert(c_list_contains(&obj_data->obj_lst_head, &rules_data->obj_lst));
    nm_assert(obj_data == g_hash_table_lookup(self->by_obj, &rules_data->obj));

    if (make_owned_by_us) {
        if (obj_data->config_state == CONFIG_STATE_NONE) {
            /* we need to mark this entry that it requires a touch on the next
             * sync. */
            obj_data->config_state = CONFIG_STATE_OWNED_BY_US;
        }
    } else if (remove_user_tag_data && c_list_length_is(&rules_data->user_tag_lst, 1))
        g_hash_table_remove(self->by_user_tag, &rules_data->user_tag);

    /* if obj_data is marked to be "added_by_us" or "removed_by_us", we need to keep this entry
     * around for the next sync -- so that we can undo what we did earlier. */
    if (obj_data->config_state == CONFIG_STATE_NONE && c_list_length_is(&rules_data->obj_lst, 1))
        g_hash_table_remove(self->by_obj, &rules_data->obj);

    g_hash_table_remove(self->by_data, rules_data);
}

void
nmp_rules_manager_untrack(NMPRulesManager *            self,
                          const NMPlatformRoutingRule *routing_rule,
                          gconstpointer                user_tag)
{
    NMPObject        obj_stack;
    const NMPObject *p_obj_stack;
    RulesData *      rules_data;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));
    g_return_if_fail(routing_rule);
    g_return_if_fail(user_tag);

    _rules_init(self);

    p_obj_stack = nmp_object_stackinit(&obj_stack, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule);

    nm_assert(nmp_object_is_visible(p_obj_stack));

    rules_data = _rules_data_lookup(self->by_data, p_obj_stack, user_tag);
    if (rules_data)
        _rules_data_untrack(self, rules_data, TRUE, FALSE);
}

void
nmp_rules_manager_set_dirty(NMPRulesManager *self, gconstpointer user_tag)
{
    RulesData *       rules_data;
    RulesUserTagData *user_tag_data;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));
    g_return_if_fail(user_tag);

    if (!self->by_data)
        return;

    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
    if (!user_tag_data)
        return;

    c_list_for_each_entry (rules_data, &user_tag_data->user_tag_lst_head, user_tag_lst)
        rules_data->dirty = TRUE;
}

void
nmp_rules_manager_untrack_all(NMPRulesManager *self,
                              gconstpointer    user_tag,
                              gboolean         all /* or only dirty */)
{
    RulesData *       rules_data;
    RulesData *       rules_data_safe;
    RulesUserTagData *user_tag_data;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));
    g_return_if_fail(user_tag);

    if (!self->by_data)
        return;

    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
    if (!user_tag_data)
        return;

    c_list_for_each_entry_safe (rules_data,
                                rules_data_safe,
                                &user_tag_data->user_tag_lst_head,
                                user_tag_lst) {
        if (all || rules_data->dirty)
            _rules_data_untrack(self, rules_data, FALSE, FALSE);
    }
    if (c_list_is_empty(&user_tag_data->user_tag_lst_head))
        g_hash_table_remove(self->by_user_tag, user_tag_data);
}

void
nmp_rules_manager_sync(NMPRulesManager *self, gboolean keep_deleted_rules)
{
    const NMDedupMultiHeadEntry *pl_head_entry;
    NMDedupMultiIter             pl_iter;
    const NMPObject *            plobj;
    gs_unref_ptrarray GPtrArray *rules_to_delete = NULL;
    RulesObjData *               obj_data;
    GHashTableIter               h_iter;
    guint                        i;
    const RulesData *            rd_best;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));

    if (!self->by_data)
        return;

    _LOGD("sync%s", keep_deleted_rules ? " (don't remove any rules)" : "");

    pl_head_entry = nm_platform_lookup_obj_type(self->platform, NMP_OBJECT_TYPE_ROUTING_RULE);
    if (pl_head_entry) {
        nmp_cache_iter_for_each (&pl_iter, pl_head_entry, &plobj) {
            obj_data = g_hash_table_lookup(self->by_obj, &plobj);

            if (!obj_data) {
                /* this rule is not tracked. It was externally added, hence we
                 * ignore it. */
                continue;
            }

            rd_best = _rules_obj_get_best_data(obj_data);
            if (rd_best) {
                if (rd_best->track_priority_present) {
                    if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
                        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
                    continue;
                }
                if (rd_best->track_priority_val == 0) {
                    if (!NM_IN_SET(obj_data->config_state,
                                   CONFIG_STATE_ADDED_BY_US,
                                   CONFIG_STATE_OWNED_BY_US)) {
                        obj_data->config_state = CONFIG_STATE_NONE;
                        continue;
                    }
                    obj_data->config_state = CONFIG_STATE_NONE;
                }
            }

            if (keep_deleted_rules) {
                _LOGD("forget/leak rule added by us: %s",
                      nmp_object_to_string(plobj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
                continue;
            }

            if (!rules_to_delete)
                rules_to_delete = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);

            g_ptr_array_add(rules_to_delete, (gpointer) nmp_object_ref(plobj));

            obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
        }
    }

    if (rules_to_delete) {
        for (i = 0; i < rules_to_delete->len; i++)
            nm_platform_object_delete(self->platform, rules_to_delete->pdata[i]);
    }

    g_hash_table_iter_init(&h_iter, self->by_obj);
    while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj_data, NULL)) {
        rd_best = _rules_obj_get_best_data(obj_data);

        if (!rd_best) {
            g_hash_table_iter_remove(&h_iter);
            continue;
        }

        if (!rd_best->track_priority_present) {
            if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
                obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
            continue;
        }
        if (rd_best->track_priority_val == 0) {
            if (!NM_IN_SET(obj_data->config_state,
                           CONFIG_STATE_REMOVED_BY_US,
                           CONFIG_STATE_OWNED_BY_US)) {
                obj_data->config_state = CONFIG_STATE_NONE;
                continue;
            }
            obj_data->config_state = CONFIG_STATE_NONE;
        }

        plobj =
            nm_platform_lookup_obj(self->platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj_data->obj);
        if (plobj)
            continue;

        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
        nm_platform_routing_rule_add(self->platform,
                                     NMP_NLM_FLAG_ADD,
                                     NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj));
    }
}

void
nmp_rules_manager_track_from_platform(NMPRulesManager *self,
                                      NMPlatform *     platform,
                                      int              addr_family,
                                      gint32           tracking_priority,
                                      gconstpointer    user_tag)
{
    NMPLookup                    lookup;
    const NMDedupMultiHeadEntry *head_entry;
    NMDedupMultiIter             iter;
    const NMPObject *            o;

    g_return_if_fail(NMP_IS_RULES_MANAGER(self));

    if (!platform)
        platform = self->platform;
    else
        g_return_if_fail(NM_IS_PLATFORM(platform));

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

    nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
    head_entry = nm_platform_lookup(platform, &lookup);
    nmp_cache_iter_for_each (&iter, head_entry, &o) {
        const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE(o);

        if (addr_family != AF_UNSPEC && rr->addr_family != addr_family)
            continue;

        nmp_rules_manager_track(self, rr, tracking_priority, user_tag, NULL);
    }
}

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

void
nmp_rules_manager_track_default(NMPRulesManager *self,
                                int              addr_family,
                                gint32           track_priority,
                                gconstpointer    user_tag)
{
    g_return_if_fail(NMP_IS_RULES_MANAGER(self));

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

    /* track the default rules. See also `man ip-rule`. */

    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)) {
        nmp_rules_manager_track(self,
                                &((NMPlatformRoutingRule){
                                    .addr_family = AF_INET,
                                    .priority    = 0,
                                    .table       = RT_TABLE_LOCAL,
                                    .action      = FR_ACT_TO_TBL,
                                    .protocol    = RTPROT_KERNEL,
                                }),
                                track_priority,
                                user_tag,
                                NULL);
        nmp_rules_manager_track(self,
                                &((NMPlatformRoutingRule){
                                    .addr_family = AF_INET,
                                    .priority    = 32766,
                                    .table       = RT_TABLE_MAIN,
                                    .action      = FR_ACT_TO_TBL,
                                    .protocol    = RTPROT_KERNEL,
                                }),
                                track_priority,
                                user_tag,
                                NULL);
        nmp_rules_manager_track(self,
                                &((NMPlatformRoutingRule){
                                    .addr_family = AF_INET,
                                    .priority    = 32767,
                                    .table       = RT_TABLE_DEFAULT,
                                    .action      = FR_ACT_TO_TBL,
                                    .protocol    = RTPROT_KERNEL,
                                }),
                                track_priority,
                                user_tag,
                                NULL);
    }
    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) {
        nmp_rules_manager_track(self,
                                &((NMPlatformRoutingRule){
                                    .addr_family = AF_INET6,
                                    .priority    = 0,
                                    .table       = RT_TABLE_LOCAL,
                                    .action      = FR_ACT_TO_TBL,
                                    .protocol    = RTPROT_KERNEL,
                                }),
                                track_priority,
                                user_tag,
                                NULL);
        nmp_rules_manager_track(self,
                                &((NMPlatformRoutingRule){
                                    .addr_family = AF_INET6,
                                    .priority    = 32766,
                                    .table       = RT_TABLE_MAIN,
                                    .action      = FR_ACT_TO_TBL,
                                    .protocol    = RTPROT_KERNEL,
                                }),
                                track_priority,
                                user_tag,
                                NULL);
    }
}

static void
_rules_init(NMPRulesManager *self)
{
    if (self->by_data)
        return;

    self->by_data =
        g_hash_table_new_full(_rules_data_hash, _rules_data_equal, NULL, _rules_data_destroy);
    self->by_obj =
        g_hash_table_new_full(_rules_obj_hash, _rules_obj_equal, NULL, _rules_obj_destroy);
    self->by_user_tag = g_hash_table_new_full(_rules_user_tag_hash,
                                              _rules_user_tag_equal,
                                              NULL,
                                              _rules_user_tag_destroy);
}

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

NMPRulesManager *
nmp_rules_manager_new(NMPlatform *platform)
{
    NMPRulesManager *self;

    g_return_val_if_fail(NM_IS_PLATFORM(platform), NULL);

    self  = g_slice_new(NMPRulesManager);
    *self = (NMPRulesManager){
        .ref_count = 1,
        .platform  = g_object_ref(platform),
    };
    return self;
}

void
nmp_rules_manager_ref(NMPRulesManager *self)
{
    g_return_if_fail(NMP_IS_RULES_MANAGER(self));

    self->ref_count++;
}

void
nmp_rules_manager_unref(NMPRulesManager *self)
{
    g_return_if_fail(NMP_IS_RULES_MANAGER(self));

    if (--self->ref_count > 0)
        return;

    if (self->by_data) {
        g_hash_table_destroy(self->by_user_tag);
        g_hash_table_destroy(self->by_obj);
        g_hash_table_destroy(self->by_data);
    }
    g_object_unref(self->platform);
    g_slice_free(NMPRulesManager, self);
}
