/* TUN 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 tun
 *  @{
 *  tun network device (interface) convenience functions
 *
 * \file tundev.c
 *
 * Example lifecycle use of the API:
 *
 *	struct osmo_sockaddr_str osa_str = {};
 *	struct osmo_sockaddr osa = {};
 *
 *	// Allocate object:
 *	struct osmo_tundev *tundev = osmo_tundev_alloc(parent_talloc_ctx, name);
 *	OSMO_ASSERT(tundev);
 *
 *	// Configure object (before opening):
 *	osmo_tundev_set_data_ind_cb(tun, tun_data_ind_cb);
 *	rc = osmo_tundev_set_dev_name(tun, "mytunnel0");
 *	rc = osmo_tundev_set_netns_name(tun, "some_netns_name_or_null");
 *
 *	// Open the tundev object:
 *	rc = osmo_tundev_open(tundev);
 *	// The tunnel device is now created and an associatd netdev object
 *	// is available to operate the device:
 *	struct osmo_netdev *netdev = osmo_tundev_get_netdev(tundev);
 *	OSMO_ASSERT(netdev);
 *
 *	// 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);
 *
 *	// Close the tunnel (asssociated netdev object becomes unavailable)
 *	rc = osmo_tundev_close(tundev);
 *	// Free the object:
 *	osmo_tundev_free(tundev);
 */

#if (!EMBEDDED)

#include <stdio.h>
#include <stdlib.h>
#include <stdio.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>

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

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

#define TUN_DEV_PATH "/dev/net/tun"
#define TUN_PACKET_MAX 8196

#define LOGTUN(tun, lvl, fmt, args ...) \
	LOGP(DLGLOBAL, lvl, "TUN(%s,if=%s/%u,ns=%s): " fmt, \
	    (tun)->name, (tun)->dev_name ? : "", \
	     (tun)->ifindex, (tun)->netns_name ? : "", ## args)

struct osmo_tundev {
	/* Name used to identify the osmo_tundev */
	char *name;

	/* netdev managing the tun interface: */
	struct osmo_netdev *netdev;

	/* ifindiex of the currently opened tunnel interface */
	unsigned int ifindex;

	/* Network interface name to use when setting up the tun device.
	 * NULL = let the system pick one. */
	char *dev_name;
	/* Whether dev_name is set by user or dynamically allocated by system */
	bool dev_name_dynamic;

	/* Write queue used since tun fd is set non-blocking */
	struct osmo_wqueue wqueue;

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

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

	/* Called by tundev each time a new packet is received on the tun interface. Can be NULL. */
	osmo_tundev_data_ind_cb_t data_ind_cb;

	/* Whether the tundev is in opened state (managing the tun interface) */
	bool opened;
};

/* A new pkt arrived from the tun device, dispatch it to the API user */
static int tundev_decaps(struct osmo_tundev *tundev)
{
	struct msgb *msg;
	int rc;

	msg = msgb_alloc(TUN_PACKET_MAX, "tundev_rx");

	if ((rc = read(tundev->wqueue.bfd.fd, msgb_data(msg), TUN_PACKET_MAX)) <= 0) {
		LOGTUN(tundev, LOGL_ERROR, "read() failed: %s (%d)\n", strerror(errno), errno);
		msgb_free(msg);
		return -1;
	}
	msgb_put(msg, rc);

	if (tundev->data_ind_cb)
		return tundev->data_ind_cb(tundev, msg);

	msgb_free(msg);
	return 0;
}

/* callback for tun device osmocom select loop integration */
static int tundev_read_cb(struct osmo_fd *fd)
{
	struct osmo_tundev *tundev = fd->data;
	return tundev_decaps(tundev);
}

/* callback for tun device osmocom select loop integration */
static int tundev_write_cb(struct osmo_fd *fd, struct msgb *msg)
{
	struct osmo_tundev *tundev = fd->data;
	size_t pkt_len = msgb_length(msg);

	int rc;
	rc = write(tundev->wqueue.bfd.fd, msgb_data(msg), pkt_len);
	if (rc < 0)
		LOGTUN(tundev, LOGL_ERROR, "write() failed: %s (%d)\n", strerror(errno), errno);
	else if (rc < pkt_len)
		LOGTUN(tundev, LOGL_ERROR, "short write() %d < %zu\n", rc, pkt_len);
	return rc;
}

static int tundev_ifupdown_ind_cb(struct osmo_netdev *netdev, bool ifupdown)
{
	struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
	LOGTUN(tundev, LOGL_NOTICE, "Physical link state changed: %s\n",
		  ifupdown ? "UP" : "DOWN");

	/* free any backlog, both on IFUP and IFDOWN. Keep the LMI, as it makes
	 * sense to get one out of the door ASAP. */
	osmo_wqueue_clear(&tundev->wqueue);
	return 0;
}

static int tundev_dev_name_chg_cb(struct osmo_netdev *netdev, const char *new_dev_name)
{
	struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
	LOGTUN(tundev, LOGL_NOTICE, "netdev changed name: %s -> %s\n",
		  osmo_netdev_get_dev_name(netdev), new_dev_name);

	if (tundev->dev_name_dynamic) {
		osmo_talloc_replace_string(tundev, &tundev->dev_name, new_dev_name);
	} else {
		/* TODO: in here we probably want to force the iface name back
		 * to tundev->dev_name one we have a osmo_netdev_set_ifname() API */
		osmo_talloc_replace_string(tundev, &tundev->dev_name, new_dev_name);
	}

	return 0;
}

static int tundev_mtu_chg_cb(struct osmo_netdev *netdev, uint32_t new_mtu)
{
	struct osmo_tundev *tundev = osmo_netdev_get_priv_data(netdev);
	LOGTUN(tundev, LOGL_NOTICE, "netdev changed MTU: %u\n", new_mtu);

	return 0;
}

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

	tundev = talloc_zero(ctx, struct osmo_tundev);
	if (!tundev)
		return NULL;

	tundev->netdev = osmo_netdev_alloc(tundev, name);
	if (!tundev->netdev) {
		talloc_free(tundev);
		return NULL;
	}
	osmo_netdev_set_priv_data(tundev->netdev, tundev);
	osmo_netdev_set_ifupdown_ind_cb(tundev->netdev, tundev_ifupdown_ind_cb);
	osmo_netdev_set_dev_name_chg_cb(tundev->netdev, tundev_dev_name_chg_cb);
	osmo_netdev_set_mtu_chg_cb(tundev->netdev, tundev_mtu_chg_cb);

	tundev->name = talloc_strdup(tundev, name);
	osmo_wqueue_init(&tundev->wqueue, 1000);
	osmo_fd_setup(&tundev->wqueue.bfd, -1, OSMO_FD_READ, osmo_wqueue_bfd_cb, tundev, 0);
	tundev->wqueue.read_cb = tundev_read_cb;
	tundev->wqueue.write_cb = tundev_write_cb;

	return tundev;
}

/*! Free an allocated tundev object.
 *  \param[in] tundev The tundev object to free
 */
void osmo_tundev_free(struct osmo_tundev *tundev)
{
	if (!tundev)
		return;
	osmo_tundev_close(tundev);
	osmo_netdev_free(tundev->netdev);
	talloc_free(tundev);
}

/*! Open and configure fd of the tunnel device.
 *  \param[in] tundev The tundev object whose tunnel interface to open
 *  \param[in] flags internal linux flags to pass when creating the device (not used yet)
 *  \returns tun fd on success; negative on error
 */
static int tundev_open_fd(struct osmo_tundev *tundev, int flags)
{
	struct ifreq ifr;
	int fd, rc;

	fd = open(TUN_DEV_PATH, O_RDWR);
	if (fd < 0) {
		LOGTUN(tundev, LOGL_ERROR, "Cannot open " TUN_DEV_PATH ": %s\n", strerror(errno));
		return fd;
	}

	memset(&ifr, 0, sizeof(ifr));
	ifr.ifr_flags = IFF_TUN | IFF_NO_PI | flags;
	if (tundev->dev_name) {
		/* if a TUN interface name was specified, put it in the structure; otherwise,
		   the kernel will try to allocate the "next" device of the specified type */
		osmo_strlcpy(ifr.ifr_name, tundev->dev_name, IFNAMSIZ);
	}

	/* try to create the device */
	rc = ioctl(fd, TUNSETIFF, (void *) &ifr);
	if (rc < 0)
		goto close_ret;

	/* Read name back from device */
	if (!tundev->dev_name) {
		ifr.ifr_name[IFNAMSIZ - 1] = '\0';
		tundev->dev_name = talloc_strdup(tundev, ifr.ifr_name);
		tundev->dev_name_dynamic = true;
	}

	/* Store interface index:
	 * (Note: there's a potential race condition here between creating the
	 * iface with a given name above and attempting to retrieve its ifindex based
	 * on that name. Someone (ie udev) could have the iface renamed in
	 * between here. It's a pity that TUNSETIFF doesn't copy back to us the
	 * newly allocated ifinidex as it does with ifname)
	 */
	tundev->ifindex = if_nametoindex(tundev->dev_name);
	if (tundev->ifindex == 0) {
		LOGTUN(tundev, LOGL_ERROR, "Unable to find ifinidex for dev %s\n",
		       tundev->dev_name);
		rc = -ENODEV;
		goto close_ret;
	}

	LOGTUN(tundev, LOGL_INFO, "TUN device created\n");

	/* set non-blocking: */
	rc = fcntl(fd, F_GETFL);
	if (rc < 0) {
		LOGTUN(tundev, LOGL_ERROR, "fcntl(F_GETFL) failed: %s (%d)\n",
		       strerror(errno), errno);
		goto close_ret;
	}
	rc = fcntl(fd, F_SETFL, rc | O_NONBLOCK);
	if (rc < 0) {
		LOGTUN(tundev, LOGL_ERROR, "fcntl(F_SETFL, O_NONBLOCK) failed: %s (%d)\n",
		       strerror(errno), errno);
		goto close_ret;
	}
	return fd;

close_ret:
	close(fd);
	return rc;
}

/*! Open the tunnel device owned by the tundev object.
 *  \param[in] tundev The tundev object to open
 *  \returns 0 on success; negative on error
 */
int osmo_tundev_open(struct osmo_tundev *tundev)
{
	struct osmo_netns_switch_state switch_state;
	int rc;
	int netns_fd = -1;

	if (tundev->opened)
		return -EALREADY;

	/* temporarily switch to specified namespace to create tun device */
	if (tundev->netns_name) {
		LOGTUN(tundev, LOGL_INFO, "Open tun: Switch to netns '%s'\n",
		       tundev->netns_name);
		netns_fd = osmo_netns_open_fd(tundev->netns_name);
		if (netns_fd < 0) {
			LOGP(DLGLOBAL, LOGL_ERROR, "Open tun: Cannot switch to netns '%s': %s (%d)\n",
			     tundev->netns_name, strerror(errno), errno);
			return netns_fd;
		}
		rc = osmo_netns_switch_enter(netns_fd, &switch_state);
		if (rc < 0) {
			LOGTUN(tundev, LOGL_ERROR, "Open tun: Cannot switch to netns '%s': %s (%d)\n",
			       tundev->netns_name, strerror(errno), errno);
			goto err_close_netns_fd;
		}
	}

	tundev->wqueue.bfd.fd = tundev_open_fd(tundev, 0);
	if (tundev->wqueue.bfd.fd < 0) {
		LOGTUN(tundev, LOGL_ERROR, "Cannot open TUN device: %s\n", strerror(errno));
		rc = -ENODEV;
		goto err_restore_ns;
	}

	/* switch back to default namespace */
	if (tundev->netns_name) {
		rc = osmo_netns_switch_exit(&switch_state);
		if (rc < 0) {
			LOGTUN(tundev, LOGL_ERROR, "Open tun: Cannot switch back from netns '%s': %s\n",
				tundev->netns_name, strerror(errno));
			goto err_close_tun;
		}
		LOGTUN(tundev, LOGL_INFO, "Open tun: Back from netns '%s'\n",
		       tundev->netns_name);
	}

	rc = osmo_netdev_set_netns_name(tundev->netdev, tundev->netns_name);
	if (rc < 0)
		goto err_close_tun;
	rc = osmo_netdev_set_ifindex(tundev->netdev, tundev->ifindex);
	if (rc < 0)
		goto err_close_tun;

	rc = osmo_netdev_register(tundev->netdev);
	if (rc < 0)
		goto err_close_tun;

	rc = osmo_fd_register(&tundev->wqueue.bfd);
	if (rc < 0)
		goto err_unregister_netdev;

	tundev->opened = true;
	return 0;

err_unregister_netdev:
	osmo_netdev_unregister(tundev->netdev);
err_close_tun:
	close(tundev->wqueue.bfd.fd);
	tundev->wqueue.bfd.fd = -1;
err_restore_ns:
	if (tundev->netns_name)
		osmo_netns_switch_exit(&switch_state);
err_close_netns_fd:
	if (netns_fd >= 0)
		close(netns_fd);
	return rc;
}

/*! Close the tunnel device owned by the tundev object.
 *  \param[in] tundev The tundev object to close
 *  \returns 0 on success; negative on error
 */
int osmo_tundev_close(struct osmo_tundev *tundev)
{
	if (!tundev->opened)
		return -EALREADY;

	osmo_wqueue_clear(&tundev->wqueue);
	if (tundev->wqueue.bfd.fd != -1) {
		osmo_fd_unregister(&tundev->wqueue.bfd);
		close(tundev->wqueue.bfd.fd);
		tundev->wqueue.bfd.fd = -1;
	}

	osmo_netdev_unregister(tundev->netdev);
	if (tundev->dev_name_dynamic) {
		TALLOC_FREE(tundev->dev_name);
		tundev->dev_name_dynamic = false;
	}
	tundev->opened = false;
	return 0;
}

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

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

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

/*! Set data_ind_cb callback, called when a new packet is received on the tun interface.
 *  \param[in] tundev The tundev object where the field is set
 *  \param[in] data_ind_cb the user provided function to be called when a new packet is received
 */
void osmo_tundev_set_data_ind_cb(struct osmo_tundev *tundev, osmo_tundev_data_ind_cb_t data_ind_cb)
{
	tundev->data_ind_cb = data_ind_cb;
}

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

/*! Get FD associated to the tundev object.
 *  \param[in] tundev The tundev object from where to retrieve the field
 *  \returns The fd of the currently open tundev, negative otherwise
 */
int osmo_tundev_get_fd(const struct osmo_tundev *tundev)
{
	return tundev->wqueue.bfd.fd;
}

/*! Set name used to name the tunnel interface created by the tundev object.
 *  \param[in] tundev The tundev object where the field is set
 *  \param[in] dev_name The tunnel interface name to use
 *  \returns 0 on success; negative on error
 *
 *  This is used during osmo_tundev_open() time, and hence shouldn't be changed
 *  when the tundev object is in "opened" state.
 *  If left as NULL (default), the system will pick a suitable name during
 *  osmo_tundev_open(), and the field will be updated to the system-selected
 *  name, which can be retrieved later with osmo_tundev_get_dev_name().
 */
int osmo_tundev_set_dev_name(struct osmo_tundev *tundev, const char *dev_name)
{
	if (tundev->opened)
		return -EALREADY;
	osmo_talloc_replace_string(tundev, &tundev->dev_name, dev_name);
	tundev->dev_name_dynamic = false;
	return 0;
}

/*! Get name used to name the tunnel interface created by the tundev object
 *  \param[in] tundev The tundev object from where to retrieve the field
 *  \returns The current value of the configured tunnel interface name to use
 */
const char *osmo_tundev_get_dev_name(const struct osmo_tundev *tundev)
{
	return tundev->dev_name;
}

/*! Set name of the network namespace to use when opening the tunnel interface
 *  \param[in] tundev The tundev object where the field is set
 *  \param[in] netns_name The network namespace to use during tunnel interface creation
 *  \returns 0 on success; negative on error
 *
 *  This is used during osmo_tundev_open() time, and hence shouldn't be changed
 *  when the tundev object is in "opened" state.
 *  If left as NULL (default), the system will stay in the current network namespace.
 */
int osmo_tundev_set_netns_name(struct osmo_tundev *tundev, const char *netns_name)
{
	if (tundev->opened)
		return -EALREADY;
	osmo_talloc_replace_string(tundev, &tundev->netns_name, netns_name);
	return 0;
}

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

/*! Get netdev managing the tunnel interface of tundev
 *  \param[in] tundev The tundev object from where to retrieve the field
 *  \returns The netdev objet managing the tun interface
 */
struct osmo_netdev *osmo_tundev_get_netdev(struct osmo_tundev *tundev)
{
	return tundev->netdev;
}

/*! Submit a packet to the tunnel device managed by the tundev object
 *  \param[in] tundev The tundev object owning the tunnel device where to inject the packet
 *  \param[in] msg The msgb containg the packet to transfer
 *  \returns 0 on success; negative on error
 *
 * This function takes the ownership of msg in all cases.
 */
int osmo_tundev_send(struct osmo_tundev *tundev, struct msgb *msg)
{
	int rc;

	OSMO_ASSERT(tundev);
	rc = osmo_wqueue_enqueue(&tundev->wqueue, msg);
	if (rc < 0) {
		LOGTUN(tundev, LOGL_ERROR, "Failed to enqueue the packet\n");
		msgb_free(msg);
		return rc;
	}
	return rc;
}


#endif /* (!EMBEDDED) */

/*! @} */