#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <linux/vm_sockets.h>

#include "trace-cmd-private.h"

int __hidden trace_vsock_open(unsigned int cid, unsigned int port)
{
	struct sockaddr_vm addr = {
		.svm_family = AF_VSOCK,
		.svm_cid = cid,
		.svm_port = port,
	};
	int sd;

	sd = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (sd < 0)
		return -errno;

	if (connect(sd, (struct sockaddr *)&addr, sizeof(addr))) {
		close(sd);
		return -errno;
	}

	return sd;
}

int __hidden trace_vsock_make(unsigned int port)
{
	struct sockaddr_vm addr = {
		.svm_family = AF_VSOCK,
		.svm_cid = VMADDR_CID_ANY,
		.svm_port = port,
	};
	int sd;

	sd = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (sd < 0)
		return -errno;

	setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));

	if (bind(sd, (struct sockaddr *)&addr, sizeof(addr)))
		return -errno;

	if (listen(sd, SOMAXCONN))
		return -errno;

	return sd;
}

int __hidden trace_vsock_make_any(void)
{
	return trace_vsock_make(VMADDR_PORT_ANY);
}

int __hidden trace_vsock_get_port(int sd, unsigned int *port)
{
	struct sockaddr_vm addr;
	socklen_t addr_len = sizeof(addr);

	if (getsockname(sd, (struct sockaddr *)&addr, &addr_len))
		return -errno;

	if (addr.svm_family != AF_VSOCK)
		return -EINVAL;

	if (port)
		*port = addr.svm_port;

	return 0;
}

int get_vsocket_params(int fd, unsigned int *lcid, unsigned int *rcid)
{
	struct sockaddr_vm addr;
	socklen_t addr_len = sizeof(addr);

	if (lcid) {
		memset(&addr, 0, sizeof(addr));
		if (getsockname(fd, (struct sockaddr *)&addr, &addr_len))
			return -1;
		if (addr.svm_family != AF_VSOCK)
			return -1;
		*lcid = addr.svm_cid;
	}

	if (rcid) {
		memset(&addr, 0, sizeof(addr));
		addr_len = sizeof(addr);
		if (getpeername(fd, (struct sockaddr *)&addr, &addr_len))
			return -1;
		if (addr.svm_family != AF_VSOCK)
			return -1;
		*rcid = addr.svm_cid;
	}

	return 0;
}

int trace_vsock_print_connection(int fd)
{
	struct sockaddr_vm vm_addr;
	socklen_t addr_len;
	int cid, port;

	addr_len = sizeof(vm_addr);
	if (getpeername(fd, (struct sockaddr *)&vm_addr, &addr_len))
		return -1;
	if (vm_addr.svm_family != AF_VSOCK)
		return -1;
	cid = vm_addr.svm_cid;
	port = vm_addr.svm_port;
	if (tracecmd_get_debug())
		tracecmd_debug("Connected to @%u:%u fd:%d\n", cid, port, fd);
	else
		tracecmd_plog("Connected to @%u:%u\n", cid, port);
	return 0;
}

static int try_splice_read_vsock(void)
{
	int ret, sd, brass[2];

	sd = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (sd < 0)
		return -errno;

	ret = pipe(brass);
	if (ret < 0)
		goto out_close_sd;

	/*
	 * On kernels that don't support splice reading from vsockets
	 * this will fail with EINVAL, or ENOTCONN otherwise.
	 * Technically, it should never succeed but if it does, claim splice
	 * reading is supported.
	 */
	ret = splice(sd, NULL, brass[1], NULL, 10, 0);
	if (ret < 0)
		ret = errno != EINVAL;
	else
		ret = 1;

	close(brass[0]);
	close(brass[1]);
out_close_sd:
	close(sd);
	return ret;
}

bool __hidden trace_vsock_can_splice_read(void)
{
	static bool initialized, res;

	if (initialized)
		return res;

	res = try_splice_read_vsock() > 0;
	initialized = true;
	return res;
}

#define GET_LOCAL_CID	0x7b9

int __hidden trace_vsock_local_cid(void)
{
	int cid;
	int fd;

	fd = open("/dev/vsock", O_RDONLY);
	if (fd < 0)
		return -errno;

	if (ioctl(fd, GET_LOCAL_CID, &cid))
		cid = -errno;

	close(fd);
	return cid;
}
