
/* network device (interface) functions.
 * (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * Author: Pau Espin Pedrol <pespin@sysmocom.de>
 *
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include "config.h"

/*! \addtogroup netdev
 *  @{
 *  network device (interface) convenience functions
 *
 * \file netdev.c
 *
 * Example lifecycle use of the API:
 *
 *	struct osmo_sockaddr_str osa_str = {};
 *	struct osmo_sockaddr osa = {};
 *
 *	// Allocate object:
 *	struct osmo_netdev *netdev = osmo_netdev_alloc(parent_talloc_ctx, name);
 *	OSMO_ASSERT(netdev);
 *
 *	// Configure object (before registration):
 *	rc = osmo_netdev_set_netns_name(netdev, "some_netns_name_or_null");
 *	rc = osmo_netdev_set_ifindex(netdev, if_nametoindex("eth0"));
 *
 *	// Register object:
 *	rc = osmo_netdev_register(netdev);
 *	// The network interface is now being monitored and the network interface
 *	// can be operated (see below)
 *
 *	// Add a local IPv4 address:
 *	rc = osmo_sockaddr_str_from_str2(&osa_str, "192.168.200.1");
 *	rc = osmo_sockaddr_str_to_sockaddr(&osa_str, &osa.u.sas);
 *	rc = osmo_netdev_add_addr(netdev, &osa, 24);
 *
 *	// Bring network interface up:
 *	rc = osmo_netdev_ifupdown(netdev, true);
 *
 *	// Add default route (0.0.0.0/0):
 *	rc = osmo_sockaddr_str_from_str2(&osa_str, "0.0.0.0");
 *	rc = osmo_sockaddr_str_to_sockaddr(&osa_str, &osa.u.sas);
 *	rc = osmo_netdev_add_route(netdev, &osa, 0, NULL);
 *
 *	// Unregister (can be freed directly too):
 *	rc = osmo_netdev_unregister(netdev);
 *	// Free the object:
 *	osmo_netdev_free(netdev);
 */

#if (!EMBEDDED)

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ifaddrs.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <net/if.h>
#include <net/route.h>

#if ENABLE_LIBMNL

#if defined(__linux__)
#include <linux/if_link.h>
#include <linux/rtnetlink.h>
#else
#error "Unknown platform!"
#endif

#endif /* ENABLE_LIBMNL */

#include <osmocom/core/utils.h>
#include <osmocom/core/select.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/netns.h>
#include <osmocom/core/netdev.h>

#if ENABLE_LIBMNL
#include <osmocom/core/mnl.h>
#endif

#define IFINDEX_UNUSED 0

#define LOGNETDEV(netdev, lvl, fmt, args ...) \
	LOGP(DLGLOBAL, lvl, "NETDEV(%s,if=%s/%u,ns=%s): " fmt, \
	     (netdev)->name, osmo_netdev_get_dev_name(netdev) ? : "", \
	     (netdev)->ifindex, (netdev)->netns_name ? : "", ## args)

static struct llist_head g_netdev_netns_ctx_list = LLIST_HEAD_INIT(g_netdev_netns_ctx_list);
static struct llist_head g_netdev_list = LLIST_HEAD_INIT(g_netdev_list);

/* One per netns, shared by all osmo_netdev in a given netns: */
struct netdev_netns_ctx {
	struct llist_head entry; /* entry in g_netdev_netns_ctx_list */
	unsigned int refcount; /* Number of osmo_netdev currently registered on this netns */
	const char *netns_name; /* default netns has empty string "" (never NULL!) */
	int netns_fd; /* FD to the netns with name "netns_name" above */
#if ENABLE_LIBMNL
	struct osmo_mnl *omnl;
#endif
};

static struct netdev_netns_ctx *netdev_netns_ctx_alloc(void *ctx, const char *netns_name)
{
	struct netdev_netns_ctx *netns_ctx;
	OSMO_ASSERT(netns_name);

	netns_ctx = talloc_zero(ctx, struct netdev_netns_ctx);
	if (!netns_ctx)
		return NULL;

	netns_ctx->netns_name = talloc_strdup(netns_ctx, netns_name);
	netns_ctx->netns_fd = -1;

	llist_add_tail(&netns_ctx->entry, &g_netdev_netns_ctx_list);
	return netns_ctx;

}

static void netdev_netns_ctx_free(struct netdev_netns_ctx *netns_ctx)
{
	if (!netns_ctx)
		return;

	llist_del(&netns_ctx->entry);

#if ENABLE_LIBMNL
	if (netns_ctx->omnl) {
		osmo_mnl_destroy(netns_ctx->omnl);
		netns_ctx->omnl = NULL;
	}
#endif

	if (netns_ctx->netns_fd != -1) {
		close(netns_ctx->netns_fd);
		netns_ctx->netns_fd = -1;
	}
	talloc_free(netns_ctx);
}

#if ENABLE_LIBMNL
static int netdev_netns_ctx_mnl_cb(const struct nlmsghdr *nlh, void *data);
#endif

static int netdev_netns_ctx_init(struct netdev_netns_ctx *netns_ctx)
{
	struct osmo_netns_switch_state switch_state;
	int rc;

	if (netns_ctx->netns_name[0] != '\0') {
		LOGP(DLGLOBAL, LOGL_INFO, "Prepare netns: Switch to netns '%s'\n", netns_ctx->netns_name);
		netns_ctx->netns_fd = osmo_netns_open_fd(netns_ctx->netns_name);
		if (netns_ctx->netns_fd < 0) {
			LOGP(DLGLOBAL, LOGL_ERROR, "Prepare netns: Cannot switch to netns '%s': %s (%d)\n",
			     netns_ctx->netns_name, strerror(errno), errno);
			return netns_ctx->netns_fd;
		}

		/* temporarily switch to specified namespace to create netlink socket */
		rc = osmo_netns_switch_enter(netns_ctx->netns_fd, &switch_state);
		if (rc < 0) {
			LOGP(DLGLOBAL, LOGL_ERROR, "Prepare netns: Cannot switch to netns '%s': %s (%d)\n",
			     netns_ctx->netns_name, strerror(errno), errno);
			/* netns_ctx->netns_fd will be freed by future call to netdev_netns_ctx_free() */
			return rc;
		}
	}

#if ENABLE_LIBMNL
	netns_ctx->omnl = osmo_mnl_init(NULL, NETLINK_ROUTE, RTMGRP_LINK, netdev_netns_ctx_mnl_cb, netns_ctx);
	rc = (netns_ctx->omnl ? 0 : -EFAULT);
#else
	rc = 0;
#endif

	/* switch back to default namespace */
	if (netns_ctx->netns_name[0] != '\0') {
		int rc2 = osmo_netns_switch_exit(&switch_state);
		if (rc2 < 0) {
			LOGP(DLGLOBAL, LOGL_ERROR, "Prepare netns: Cannot switch back from netns '%s': %s\n",
			     netns_ctx->netns_name, strerror(errno));
			return rc2;
		}
		LOGP(DLGLOBAL, LOGL_INFO, "Prepare netns: Back from netns '%s'\n",
		     netns_ctx->netns_name);
	}
	return rc;
}

static struct netdev_netns_ctx *netdev_netns_ctx_find_by_netns_name(const char *netns_name)
{
	struct netdev_netns_ctx *netns_ctx;

	llist_for_each_entry(netns_ctx, &g_netdev_netns_ctx_list, entry) {
		if (strcmp(netns_ctx->netns_name, netns_name))
			continue;
		return netns_ctx;
	}

	return NULL;
}

static struct netdev_netns_ctx *netdev_netns_ctx_get(const char *netns_name)
{
	struct netdev_netns_ctx *netns_ctx;
	int rc;

	OSMO_ASSERT(netns_name);
	netns_ctx = netdev_netns_ctx_find_by_netns_name(netns_name);
	if (!netns_ctx) {
		netns_ctx = netdev_netns_ctx_alloc(NULL, netns_name);
		if (!netns_ctx)
			return NULL;
		rc = netdev_netns_ctx_init(netns_ctx);
		if (rc < 0) {
			netdev_netns_ctx_free(netns_ctx);
			return NULL;
		}
	}
	netns_ctx->refcount++;
	return netns_ctx;
}

static void netdev_netns_ctx_put(struct netdev_netns_ctx *netns_ctx)
{
	OSMO_ASSERT(netns_ctx);
	netns_ctx->refcount--;

	if (netns_ctx->refcount == 0)
		netdev_netns_ctx_free(netns_ctx);
}

struct osmo_netdev {
	/* entry in g_netdev_list */
	struct llist_head entry;

	/* Pointer to struct shared (refcounted) by all osmo_netdev in the same netns: */
	struct netdev_netns_ctx *netns_ctx;

	/* Name used to identify the osmo_netdev */
	char *name;

	/* ifindex of the network interface (address space is per netns) */
	unsigned int ifindex;

	/* Network interface name. Can change over lifetime of the interface. */
	char *dev_name;

	/* netns name where the netdev interface is created (NULL = default netns) */
	char *netns_name;

	/* API user private data */
	void *priv_data;

	/* Whether the netdev is in operation (managing the netdev interface) */
	bool registered;

	/* Called by netdev each time a new up/down state change is detected. Can be NULL. */
	osmo_netdev_ifupdown_ind_cb_t ifupdown_ind_cb;

	/* Called by netdev each time the registered network interface is renamed by the system. Can be NULL. */
	osmo_netdev_dev_name_chg_cb_t dev_name_chg_cb;

	/* Called by netdev each time the configured MTU changes in registered network interface. Can be NULL. */
	osmo_netdev_mtu_chg_cb_t mtu_chg_cb;

	/* Whether the netdev interface is UP */
	bool if_up;
	/* Whether we know the interface updown state (aka if if_up holds information)*/
	bool if_up_known;

	/* The netdev interface MTU size */
	uint32_t if_mtu;
	/* Whether we know the interface MTU size (aka if if_mtu holds information)*/
	bool if_mtu_known;
};

#define NETDEV_NETNS_ENTER(netdev, switch_state, str_prefix) \
	do { \
		if ((netdev)->netns_name) { \
			LOGNETDEV(netdev, LOGL_DEBUG, str_prefix ": Switch to netns '%s'\n", \
			       (netdev)->netns_name); \
			int rc2 = osmo_netns_switch_enter((netdev)->netns_ctx->netns_fd, switch_state); \
			if (rc2 < 0) { \
				LOGNETDEV(netdev, LOGL_ERROR, str_prefix ": Cannot switch to netns '%s': %s (%d)\n", \
				       (netdev)->netns_name, strerror(errno), errno); \
				return -EACCES; \
			} \
		} \
	} while (0)

#define NETDEV_NETNS_EXIT(netdev, switch_state, str_prefix) \
	do { \
		if ((netdev)->netns_name) { \
			int rc2 = osmo_netns_switch_exit(switch_state); \
			if (rc2 < 0) { \
				LOGNETDEV(netdev, LOGL_ERROR, str_prefix ": Cannot switch back from netns '%s': %s\n", \
				       (netdev)->netns_name, strerror(errno)); \
				return rc2; \
			} \
			LOGNETDEV(netdev, LOGL_DEBUG, str_prefix ": Back from netns '%s'\n", \
			       (netdev)->netns_name); \
		} \
	} while (0)

#if ENABLE_LIBMNL
/* validate the netlink attributes */
static int netdev_mnl_data_attr_cb(const struct nlattr *attr, void *data)
{
	const struct nlattr **tb = data;
	int type = mnl_attr_get_type(attr);

	if (mnl_attr_type_valid(attr, IFLA_MAX) < 0)
		return MNL_CB_OK;

	switch (type) {
	case IFLA_ADDRESS:
		if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0)
			return MNL_CB_ERROR;
		break;
	case IFLA_MTU:
		if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0)
			return MNL_CB_ERROR;
		break;
	case IFLA_IFNAME:
		if (mnl_attr_validate(attr, MNL_TYPE_STRING) < 0)
			return MNL_CB_ERROR;
		break;
	}
	tb[type] = attr;
	return MNL_CB_OK;
}

static void netdev_mnl_check_mtu_change(struct osmo_netdev *netdev, uint32_t mtu)
{
	if (netdev->if_mtu_known && netdev->if_mtu == mtu)
		return;

	LOGNETDEV(netdev, LOGL_NOTICE, "MTU changed: %u\n", mtu);
	if (netdev->mtu_chg_cb)
		netdev->mtu_chg_cb(netdev, mtu);

	netdev->if_mtu_known = true;
	netdev->if_mtu = mtu;
}

static void netdev_mnl_check_link_state_change(struct osmo_netdev *netdev, bool if_up)
{
	if (netdev->if_up_known && netdev->if_up == if_up)
		return;

	LOGNETDEV(netdev, LOGL_NOTICE, "Physical link state changed: %s\n",
		  if_up ? "UP" : "DOWN");
	if (netdev->ifupdown_ind_cb)
		netdev->ifupdown_ind_cb(netdev, if_up);

	netdev->if_up_known = true;
	netdev->if_up = if_up;
}

static int netdev_mnl_cb(struct osmo_netdev *netdev, struct ifinfomsg *ifm, struct nlattr **tb)
{
	char ifnamebuf[IF_NAMESIZE];
	const char *ifname = NULL;
	bool if_running;

	if (tb[IFLA_IFNAME]) {
		ifname = mnl_attr_get_str(tb[IFLA_IFNAME]);
		LOGNETDEV(netdev, LOGL_DEBUG, "%s(): ifname=%s\n", __func__, ifname);
	} else {
		/* Try harder to obtain the ifname. This code path should in
		 * general not be triggered since usually IFLA_IFNAME is there */
		struct osmo_netns_switch_state switch_state;
		NETDEV_NETNS_ENTER(netdev, &switch_state, "if_indextoname");
		ifname = if_indextoname(ifm->ifi_index, ifnamebuf);
		NETDEV_NETNS_EXIT(netdev, &switch_state, "if_indextoname");
	}
	if (ifname) {
		/* Update dev_name if it changed: */
		if (strcmp(netdev->dev_name, ifname) != 0) {
			if (netdev->dev_name_chg_cb)
				netdev->dev_name_chg_cb(netdev, ifname);
			osmo_talloc_replace_string(netdev, &netdev->dev_name, ifname);
		}
	}

	if (tb[IFLA_MTU]) {
		uint32_t mtu = mnl_attr_get_u32(tb[IFLA_MTU]);
		LOGNETDEV(netdev, LOGL_DEBUG, "%s(): mtu=%u\n", __func__,  mtu);
		netdev_mnl_check_mtu_change(netdev, mtu);
	}
	if (tb[IFLA_ADDRESS]) {
		uint8_t *hwaddr = mnl_attr_get_payload(tb[IFLA_ADDRESS]);
		uint16_t hwaddr_len = mnl_attr_get_payload_len(tb[IFLA_ADDRESS]);
		LOGNETDEV(netdev, LOGL_DEBUG, "%s(): hwaddress=%s\n",
		       __func__, osmo_hexdump(hwaddr, hwaddr_len));
	}

	if_running = !!(ifm->ifi_flags & IFF_RUNNING);
	LOGNETDEV(netdev, LOGL_DEBUG, "%s(): up=%u running=%u\n",
	       __func__, !!(ifm->ifi_flags & IFF_UP), if_running);
	netdev_mnl_check_link_state_change(netdev, if_running);

	return MNL_CB_OK;
}

static int netdev_netns_ctx_mnl_cb(const struct nlmsghdr *nlh, void *data)
{
	struct osmo_mnl *omnl = data;
	struct netdev_netns_ctx *netns_ctx = (struct netdev_netns_ctx *)omnl->priv;
	struct nlattr *tb[IFLA_MAX+1] = {};
	struct ifinfomsg *ifm = mnl_nlmsg_get_payload(nlh);
	struct osmo_netdev *netdev;

	OSMO_ASSERT(omnl);
	OSMO_ASSERT(ifm);

	mnl_attr_parse(nlh, sizeof(*ifm), netdev_mnl_data_attr_cb, tb);

	LOGP(DLGLOBAL, LOGL_DEBUG,
	    "%s(): index=%d type=%d flags=0x%x family=%d\n", __func__,
	    ifm->ifi_index, ifm->ifi_type, ifm->ifi_flags, ifm->ifi_family);

	if (ifm->ifi_index == IFINDEX_UNUSED)
		return MNL_CB_ERROR;

	/* Find the netdev (if any) using key <netns,ifindex>.
	 * Different users of the API may have its own osmo_netdev object
	 * tracking potentially same netif, hence we need to iterate the whole list
	 * and dispatch to all matches:
	 */
	bool found_any = false;
	llist_for_each_entry(netdev, &g_netdev_list, entry) {
		if (!netdev->registered)
			continue;
		if (netdev->ifindex != ifm->ifi_index)
			continue;
		if (strcmp(netdev->netns_ctx->netns_name, netns_ctx->netns_name))
			continue;
		found_any = true;
		netdev_mnl_cb(netdev, ifm, &tb[0]);
	}

	if (!found_any) {
		LOGP(DLGLOBAL, LOGL_DEBUG, "%s(): device with ifindex %u on netns %s not registered\n", __func__,
		     ifm->ifi_index, netns_ctx->netns_name);
	}
	return MNL_CB_OK;
}

/* Trigger an initial dump of the iface to get link information */
static int netdev_mnl_request_initial_dump(struct osmo_mnl *omnl, unsigned int if_index)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct ifinfomsg *ifm;

	nlh->nlmsg_type	= RTM_GETLINK;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifi_family = AF_UNSPEC;
	ifm->ifi_index = if_index;

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}

static int netdev_mnl_set_ifupdown(struct osmo_mnl *omnl, unsigned int if_index,
				   const char *dev_name, bool up)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct ifinfomsg *ifm;
	unsigned int change = 0;
	unsigned int flags = 0;

	if (up) {
		change |= IFF_UP;
		flags |= IFF_UP;
	} else {
		change |= IFF_UP;
		flags &= ~IFF_UP;
	}

	nlh->nlmsg_type	= RTM_NEWLINK;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifi_family = AF_UNSPEC;
	ifm->ifi_change = change;
	ifm->ifi_flags = flags;
	ifm->ifi_index = if_index;

	if (dev_name)
		mnl_attr_put_str(nlh, IFLA_IFNAME, dev_name);

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}

static int netdev_mnl_set_mtu(struct osmo_mnl *omnl, unsigned int if_index,
			      const char *dev_name, uint32_t mtu)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct ifinfomsg *ifm;

	nlh->nlmsg_type	= RTM_NEWLINK;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifi_family = AF_UNSPEC;
	ifm->ifi_change = 0;
	ifm->ifi_flags = 0;
	ifm->ifi_index = if_index;

	mnl_attr_put_u32(nlh, IFLA_MTU, mtu);

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}

static int netdev_mnl_add_addr(struct osmo_mnl *omnl, unsigned int if_index, const struct osmo_sockaddr *osa, uint8_t prefix)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct ifaddrmsg *ifm;

	nlh->nlmsg_type	= RTM_NEWADDR;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifa_family = osa->u.sa.sa_family;
	ifm->ifa_prefixlen = prefix;
	ifm->ifa_flags = IFA_F_PERMANENT;
	ifm->ifa_scope = RT_SCOPE_UNIVERSE;
	ifm->ifa_index = if_index;

	/*
	 * The exact meaning of IFA_LOCAL and IFA_ADDRESS depend
	 * on the address family being used and the device type.
	 * For broadcast devices (like the interfaces we use),
	 * for IPv4 we specify both and they are used interchangeably.
	 * For IPv6, only IFA_ADDRESS needs to be set.
	 */
	switch (osa->u.sa.sa_family) {
	case AF_INET:
		mnl_attr_put_u32(nlh, IFA_LOCAL, osa->u.sin.sin_addr.s_addr);
		mnl_attr_put_u32(nlh, IFA_ADDRESS, osa->u.sin.sin_addr.s_addr);
		break;
	case AF_INET6:
		mnl_attr_put(nlh, IFA_ADDRESS, sizeof(struct in6_addr), &osa->u.sin6.sin6_addr);
		break;
	default:
		return -EINVAL;
	}

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}

static int netdev_mnl_del_addr(struct osmo_mnl *omnl, unsigned int if_index, const struct osmo_sockaddr *osa, uint8_t prefix)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct ifaddrmsg *ifm;

	nlh->nlmsg_type	= RTM_DELADDR;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifa_family = osa->u.sa.sa_family;
	ifm->ifa_prefixlen = prefix;
	ifm->ifa_flags = IFA_F_PERMANENT;
	ifm->ifa_scope = RT_SCOPE_UNIVERSE;
	ifm->ifa_index = if_index;

	/*
	 * The exact meaning of IFA_LOCAL and IFA_ADDRESS depend
	 * on the address family being used and the device type.
	 * For broadcast devices (like the interfaces we use),
	 * for IPv4 we specify both and they are used interchangeably.
	 * For IPv6, only IFA_ADDRESS needs to be set.
	 */
	switch (osa->u.sa.sa_family) {
	case AF_INET:
		mnl_attr_put_u32(nlh, IFA_LOCAL, osa->u.sin.sin_addr.s_addr);
		mnl_attr_put_u32(nlh, IFA_ADDRESS, osa->u.sin.sin_addr.s_addr);
		break;
	case AF_INET6:
		mnl_attr_put(nlh, IFA_ADDRESS, sizeof(struct in6_addr), &osa->u.sin6.sin6_addr);
		break;
	default:
		return -EINVAL;
	}

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}

static int netdev_mnl_add_route(struct osmo_mnl *omnl,
				unsigned int if_index,
				const struct osmo_sockaddr *dst_osa,
				uint8_t dst_prefix,
				const struct osmo_sockaddr *gw_osa)
{
	char buf[MNL_SOCKET_BUFFER_SIZE];
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	struct rtmsg *rtm;

	nlh->nlmsg_type	= RTM_NEWROUTE;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK;
	nlh->nlmsg_seq = (uint32_t)time(NULL);

	rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(*rtm));
	rtm->rtm_family = dst_osa->u.sa.sa_family;
	rtm->rtm_dst_len = dst_prefix;
	rtm->rtm_src_len = 0;
	rtm->rtm_tos = 0;
	rtm->rtm_protocol = RTPROT_STATIC;
	rtm->rtm_table = RT_TABLE_MAIN;
	rtm->rtm_type = RTN_UNICAST;
	rtm->rtm_scope = gw_osa ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK;
	rtm->rtm_flags = 0;

	switch (dst_osa->u.sa.sa_family) {
	case AF_INET:
		mnl_attr_put_u32(nlh, RTA_DST, dst_osa->u.sin.sin_addr.s_addr);
		break;
	case AF_INET6:
		mnl_attr_put(nlh, RTA_DST, sizeof(struct in6_addr), &dst_osa->u.sin6.sin6_addr);
		break;
	default:
		return -EINVAL;
	}

	mnl_attr_put_u32(nlh, RTA_OIF, if_index);

	if (gw_osa) {
		switch (gw_osa->u.sa.sa_family) {
		case AF_INET:
			mnl_attr_put_u32(nlh, RTA_GATEWAY, gw_osa->u.sin.sin_addr.s_addr);
			break;
		case AF_INET6:
			mnl_attr_put(nlh, RTA_GATEWAY, sizeof(struct in6_addr), &gw_osa->u.sin6.sin6_addr);
			break;
		default:
			return -EINVAL;
		}
	}

	if (mnl_socket_sendto(omnl->mnls, nlh, nlh->nlmsg_len) < 0) {
		LOGP(DLGLOBAL, LOGL_ERROR, "mnl_socket_sendto\n");
		return -EIO;
	}

	return 0;
}
#endif /* if ENABLE_LIBMNL */

/*! Allocate a new netdev object.
 *  \param[in] ctx talloc context to use as a parent when allocating the netdev object
 *  \param[in] name A name providen to identify the netdev object
 *  \returns newly allocated netdev object on success; NULL on error
 */
struct osmo_netdev *osmo_netdev_alloc(void *ctx, const char *name)
{
	struct osmo_netdev *netdev;

	netdev = talloc_zero(ctx, struct osmo_netdev);
	if (!netdev)
		return NULL;

	netdev->name = talloc_strdup(netdev, name);

	llist_add_tail(&netdev->entry, &g_netdev_list);
	return netdev;
}

/*! Free an allocated netdev object.
 *  \param[in] netdev The netdev object to free
 */
void osmo_netdev_free(struct osmo_netdev *netdev)
{
	if (!netdev)
		return;
	if (osmo_netdev_is_registered(netdev))
		osmo_netdev_unregister(netdev);
	llist_del(&netdev->entry);
	talloc_free(netdev);
}

/*! Start managing the network device referenced by the netdev object.
 *  \param[in] netdev The netdev object to open
 *  \returns 0 on success; negative on error
 */
int osmo_netdev_register(struct osmo_netdev *netdev)
{
	char ifnamebuf[IF_NAMESIZE];
	struct osmo_netns_switch_state switch_state;
	int rc = 0;

	if (netdev->registered)
		return -EALREADY;

	netdev->netns_ctx = netdev_netns_ctx_get(netdev->netns_name ? : "");
	if (!netdev->netns_ctx)
		return -EFAULT;

	NETDEV_NETNS_ENTER(netdev, &switch_state, "register");

	if (!if_indextoname(netdev->ifindex, ifnamebuf)) {
		rc = -ENODEV;
		goto err_put_exit;
	}
	osmo_talloc_replace_string(netdev, &netdev->dev_name, ifnamebuf);

#if ENABLE_LIBMNL
	rc = netdev_mnl_request_initial_dump(netdev->netns_ctx->omnl, netdev->ifindex);
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "register");

	netdev->registered = true;
	return rc;

err_put_exit:
	NETDEV_NETNS_EXIT(netdev, &switch_state, "register");
	netdev_netns_ctx_put(netdev->netns_ctx);
	return rc;
}

/*! Unregister the netdev object (stop managing /moniutoring the interface)
 *  \param[in] netdev The netdev object to close
 *  \returns 0 on success; negative on error
 */
int osmo_netdev_unregister(struct osmo_netdev *netdev)
{
	if (!netdev->registered)
		return -EALREADY;

	netdev->if_up_known = false;
	netdev->if_mtu_known = false;

	netdev_netns_ctx_put(netdev->netns_ctx);
	netdev->registered = false;
	return 0;
}

/*! Retrieve whether the netdev object is in "registered" state.
 *  \param[in] netdev The netdev object to check
 *  \returns true if in state "registered"; false otherwise
 */
bool osmo_netdev_is_registered(struct osmo_netdev *netdev)
{
	return netdev->registered;
}

/*! Set private user data pointer on the netdev object.
 *  \param[in] netdev The netdev object where the field is set
 */
void osmo_netdev_set_priv_data(struct osmo_netdev *netdev, void *priv_data)
{
	netdev->priv_data = priv_data;
}

/*! Get private user data pointer from the netdev object.
 *  \param[in] netdev The netdev object from where to retrieve the field
 *  \returns The current value of the priv_data field.
 */
void *osmo_netdev_get_priv_data(struct osmo_netdev *netdev)
{
	return netdev->priv_data;
}

/*! Set ifupdown_ind_cb callback, called when the link status (UP/DOWN) changes.
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] ifupdown_ind_cb the user provided function to be called when the link status (UP/DOWN) changes
 */
void osmo_netdev_set_ifupdown_ind_cb(struct osmo_netdev *netdev, osmo_netdev_ifupdown_ind_cb_t ifupdown_ind_cb)
{
	netdev->ifupdown_ind_cb = ifupdown_ind_cb;
}

/*! Set dev_name_chg_cb callback, called when a change in the network name is detected
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] dev_name_chg_cb the user provided function to be called when a the interface is renamed
 */
void osmo_netdev_set_dev_name_chg_cb(struct osmo_netdev *netdev, osmo_netdev_dev_name_chg_cb_t dev_name_chg_cb)
{
	netdev->dev_name_chg_cb = dev_name_chg_cb;
}

/*! Set mtu_chg_cb callback, called when a change in the network name is detected
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] mtu_chg_cb the user provided function to be called when the configured MTU at the interface changes
 */
void osmo_netdev_set_mtu_chg_cb(struct osmo_netdev *netdev, osmo_netdev_mtu_chg_cb_t mtu_chg_cb)
{
	netdev->mtu_chg_cb = mtu_chg_cb;
}

/*! Get name used to identify the netdev object.
 *  \param[in] netdev The netdev object from where to retrieve the field
 *  \returns The current value of the name used to identify the netdev object
 */
const char *osmo_netdev_get_name(const struct osmo_netdev *netdev)
{
	return netdev->name;
}

/*! Set (specify) interface index identifying the network interface to manage
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] ifindex The interface index identifying the interface
 *  \returns 0 on success; negative on error
 *
 *  The ifindex, together with the netns_name (see
 *  osmo_netdev_netns_name_set()), form together the key identifiers of a
 *  network interface to manage.
 *  This field is used during osmo_netdev_register() time, and hence must be set
 *  before calling that API, and cannot be changed when the netdev object is in
 *  "registered" state.
 */
int osmo_netdev_set_ifindex(struct osmo_netdev *netdev, unsigned int ifindex)
{
	if (netdev->registered)
		return -EALREADY;
	netdev->ifindex = ifindex;
	return 0;
}

/*! Get interface index identifying the interface managed by netdev
 *  \param[in] netdev The netdev object from where to retrieve the field
 *  \returns The current value of the configured netdev interface ifindex to use (0 = unset)
 */
unsigned int osmo_netdev_get_ifindex(const struct osmo_netdev *netdev)
{
	return netdev->ifindex;
}

/*! Set (specify) name of the network namespace where the network interface to manage is located
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] netns_name The network namespace where the network interface is located
 *  \returns 0 on success; negative on error
 *
 *  The netns_name, together with the ifindex (see
 *  osmo_netdev_ifindex_set()), form together the key identifiers of a
 *  network interface to manage.
 *  This field is used during osmo_netdev_register() time, and hence must be set
 *  before calling that API, and cannot be changed when the netdev object is in
 *  "registered" state.
 *  If left as NULL (default), the management will be done in the current network namespace.
 */
int osmo_netdev_set_netns_name(struct osmo_netdev *netdev, const char *netns_name)
{
	if (netdev->registered)
		return -EALREADY;
	osmo_talloc_replace_string(netdev, &netdev->netns_name, netns_name);
	return 0;
}

/*! Get name of network namespace used when opening the netdev interface
 *  \param[in] netdev The netdev object from where to retrieve the field
 *  \returns The current value of the configured network namespace
 */
const char *osmo_netdev_get_netns_name(const struct osmo_netdev *netdev)
{
	return netdev->netns_name;
}

/*! Get name used to name the network interface created by the netdev object
 *  \param[in] netdev The netdev object from where to retrieve the field
 *  \returns The interface name (or NULL if unknown)
 *
 * This information is retrieved internally once the netdev object enters the
 * "registered" state. Hence, when not registered NULL  can be returned.
 */
const char *osmo_netdev_get_dev_name(const struct osmo_netdev *netdev)
{
	return netdev->dev_name;
}

/*! Set the MTU of the network interface
 *  \param[in] netdev The netdev object where the field is set
 *  \param[in] mtu The MTU to be set on the network interface
 *  \returns 0 on success; negative on error
 *
 */
int osmo_netdev_set_mtu(struct osmo_netdev *netdev, uint32_t mtu)
{
	struct osmo_netns_switch_state switch_state;
	int rc;

	if (!netdev->registered)
		return -ENODEV;

	LOGNETDEV(netdev, LOGL_NOTICE, "Setting dev %s MTU %" PRIu32 "\n",
		  netdev->dev_name, mtu);

	NETDEV_NETNS_ENTER(netdev, &switch_state, "set_mtu");

#if ENABLE_LIBMNL
	rc = netdev_mnl_set_mtu(netdev->netns_ctx->omnl, netdev->ifindex,
				netdev->dev_name, mtu);
#else
	LOGNETDEV(netdev, LOGL_ERROR, "%s: NOT SUPPORTED. Build libosmocore with --enable-libmnl.\n", __func__);
	rc = -ENOTSUP;
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "set_mtu");

	return rc;
}

/*! Bring netdev interface UP or DOWN.
 *  \param[in] netdev The netdev object managing the netdev interface
 *  \param[in] ifupdown true to set the interface UP, false to set it DOWN
 *  \returns 0 on succes; negative on error.
 */
int osmo_netdev_ifupdown(struct osmo_netdev *netdev, bool ifupdown)
{
	struct osmo_netns_switch_state switch_state;
	int rc;

	if (!netdev->registered)
		return -ENODEV;

	LOGNETDEV(netdev, LOGL_NOTICE, "Bringing dev %s %s\n",
		  netdev->dev_name, ifupdown ? "UP" : "DOWN");

	NETDEV_NETNS_ENTER(netdev, &switch_state, "ifupdown");

#if ENABLE_LIBMNL
	rc = netdev_mnl_set_ifupdown(netdev->netns_ctx->omnl, netdev->ifindex,
				     netdev->dev_name, ifupdown);
#else
	LOGNETDEV(netdev, LOGL_ERROR, "%s: NOT SUPPORTED. Build libosmocore with --enable-libmnl.\n", __func__);
	rc = -ENOTSUP;
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "ifupdown");

	return rc;
}

/*! Add IP address to netdev interface
 *  \param[in] netdev The netdev object managing the netdev interface
 *  \param[in] addr The local address to set on the interface
 *  \param[in] prefixlen The network prefix of addr
 *  \returns 0 on succes; negative on error.
 */
int osmo_netdev_add_addr(struct osmo_netdev *netdev, const struct osmo_sockaddr *addr, uint8_t prefixlen)
{
	struct osmo_netns_switch_state switch_state;
	char buf[INET6_ADDRSTRLEN];
	int rc;

	if (!netdev->registered)
		return -ENODEV;

	LOGNETDEV(netdev, LOGL_NOTICE, "Adding address %s/%u to dev %s\n",
	       osmo_sockaddr_ntop(&addr->u.sa, buf), prefixlen, netdev->dev_name);

	NETDEV_NETNS_ENTER(netdev, &switch_state, "Add address");

#if ENABLE_LIBMNL
	rc = netdev_mnl_add_addr(netdev->netns_ctx->omnl, netdev->ifindex, addr, prefixlen);
#else
	LOGNETDEV(netdev, LOGL_ERROR, "%s: NOT SUPPORTED. Build libosmocore with --enable-libmnl.\n", __func__);
	rc = -ENOTSUP;
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "Add address");

	return rc;
}

/*! Remove IP address from netdev interface
 *  \param[in] netdev The netdev object managing the netdev interface
 *  \param[in] addr The local address to remove from the interface
 *  \param[in] prefixlen The network prefix of addr
 *  \returns 0 on succes; negative on error.
 */
int osmo_netdev_del_addr(struct osmo_netdev *netdev, const struct osmo_sockaddr *addr, uint8_t prefixlen)
{
	struct osmo_netns_switch_state switch_state;
	char buf[INET6_ADDRSTRLEN];
	int rc;

	if (!netdev->registered)
		return -ENODEV;

	LOGNETDEV(netdev, LOGL_NOTICE, "Deleting address %s/%u from dev %s\n",
		  osmo_sockaddr_ntop(&addr->u.sa, buf), prefixlen, netdev->dev_name);

	NETDEV_NETNS_ENTER(netdev, &switch_state, "Delete address");

#if ENABLE_LIBMNL
	rc = netdev_mnl_del_addr(netdev->netns_ctx->omnl, netdev->ifindex, addr, prefixlen);
#else
	LOGNETDEV(netdev, LOGL_ERROR, "%s: NOT SUPPORTED. Build libosmocore with --enable-libmnl.\n", __func__);
	rc = -ENOTSUP;
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "Delete address");

	return rc;
}

/*! Add IP route to netdev interface
 *  \param[in] netdev The netdev object managing the netdev interface
 *  \param[in] dst_addr The destination address of the route
 *  \param[in] dst_prefixlen The network prefix of dst_addr
 *  \param[in] gw_addr The gateway address. Optional, can be NULL.
 *  \returns 0 on succes; negative on error.
 */
int osmo_netdev_add_route(struct osmo_netdev *netdev, const struct osmo_sockaddr *dst_addr, uint8_t dst_prefixlen, const struct osmo_sockaddr *gw_addr)
{
	struct osmo_netns_switch_state switch_state;
	char buf_dst[INET6_ADDRSTRLEN];
	char buf_gw[INET6_ADDRSTRLEN];
	int rc;

	if (!netdev->registered)
		return -ENODEV;

	LOGNETDEV(netdev, LOGL_NOTICE, "Adding route %s/%u%s%s dev %s\n",
	       osmo_sockaddr_ntop(&dst_addr->u.sa, buf_dst), dst_prefixlen,
	       gw_addr ? " via " : "",
	       gw_addr ? osmo_sockaddr_ntop(&gw_addr->u.sa, buf_gw) : "",
	       netdev->dev_name);

	NETDEV_NETNS_ENTER(netdev, &switch_state, "Add route");

#if ENABLE_LIBMNL
	rc = netdev_mnl_add_route(netdev->netns_ctx->omnl, netdev->ifindex, dst_addr, dst_prefixlen, gw_addr);
#else
	LOGNETDEV(netdev, LOGL_ERROR, "%s: NOT SUPPORTED. Build libosmocore with --enable-libmnl.\n", __func__);
	rc = -ENOTSUP;
#endif

	NETDEV_NETNS_EXIT(netdev, &switch_state, "Add route");

	return rc;
}

#endif /* (!EMBEDDED) */

/*! @} */
