// SPDX-License-Identifier: LGPL-2.1+

#include "nm-default.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);
}
