// SPDX-License-Identifier: MIT
/*
 * Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/genetlink.h>
#include <linux/if_link.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/wireguard.h>
#include <netinet/in.h>
#include "containers.h"
#include "encoding.h"
#include "netlink.h"

#define IPC_SUPPORTS_KERNEL_INTERFACE

void put_magic_header_attr(uint8_t version, struct nlmsghdr *nlh, int attr_name, char *header_field)
{
	if (version >= 2) {
		mnl_attr_put_strz(nlh, attr_name, header_field);
	} else {
		uint32_t magic_header = strtoul(header_field, NULL, 10);
		mnl_attr_put_u32(nlh, attr_name, magic_header);
	}
}

#define GET_MAGIC_HEADER(attr, magic_header, attr_name)\
	if (!mnl_attr_validate(attr, MNL_TYPE_U32)) {\
		uint32_t numeric_value = mnl_attr_get_u32(attr);\
		magic_header = malloc(12);\
		if (!magic_header) {\
			perror("malloc");\
			return MNL_CB_ERROR;\
		}\
		snprintf(magic_header, 12, "%u", numeric_value);\
		device->flags |= attr_name;\
	} else if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {\
		magic_header = strdup(mnl_attr_get_str(attr));\
		if (!magic_header) {\
			perror("strdup");\
			return MNL_CB_ERROR;\
		}\
		device->flags |= attr_name;\
	}

#define SOCKET_BUFFER_SIZE (mnl_ideal_socket_buffer_size())

struct interface {
	const char *name;
	bool is_wireguard;
};

static int parse_linkinfo(const struct nlattr *attr, void *data)
{
	struct interface *interface = data;

	if (mnl_attr_get_type(attr) == IFLA_INFO_KIND && !strcmp(WG_GENL_NAME, mnl_attr_get_str(attr)))
		interface->is_wireguard = true;
	return MNL_CB_OK;
}

static int parse_infomsg(const struct nlattr *attr, void *data)
{
	struct interface *interface = data;

	if (mnl_attr_get_type(attr) == IFLA_LINKINFO)
		return mnl_attr_parse_nested(attr, parse_linkinfo, data);
	else if (mnl_attr_get_type(attr) == IFLA_IFNAME)
		interface->name = mnl_attr_get_str(attr);
	return MNL_CB_OK;
}

static int read_devices_cb(const struct nlmsghdr *nlh, void *data)
{
	struct string_list *list = data;
	struct interface interface = { 0 };
	int ret;

	ret = mnl_attr_parse(nlh, sizeof(struct ifinfomsg), parse_infomsg, &interface);
	if (ret != MNL_CB_OK)
		return ret;
	if (interface.name && interface.is_wireguard)
		ret = string_list_add(list, interface.name);
	if (ret < 0)
		return ret;
	if (nlh->nlmsg_type != NLMSG_DONE)
		return MNL_CB_OK + 1;
	return MNL_CB_OK;
}

static int kernel_get_wireguard_interfaces(struct string_list *list)
{
	struct mnl_socket *nl = NULL;
	char *rtnl_buffer = NULL;
	size_t message_len;
	unsigned int portid, seq;
	ssize_t len;
	int ret = 0;
	struct nlmsghdr *nlh;
	struct ifinfomsg *ifm;

	ret = -ENOMEM;
	rtnl_buffer = calloc(SOCKET_BUFFER_SIZE, 1);
	if (!rtnl_buffer)
		goto cleanup;

	nl = mnl_socket_open(NETLINK_ROUTE);
	if (!nl) {
		ret = -errno;
		goto cleanup;
	}

	if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
		ret = -errno;
		goto cleanup;
	}

	seq = time(NULL);
	portid = mnl_socket_get_portid(nl);
	nlh = mnl_nlmsg_put_header(rtnl_buffer);
	nlh->nlmsg_type = RTM_GETLINK;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP;
	nlh->nlmsg_seq = seq;
	ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
	ifm->ifi_family = AF_UNSPEC;
	message_len = nlh->nlmsg_len;

	if (mnl_socket_sendto(nl, rtnl_buffer, message_len) < 0) {
		ret = -errno;
		goto cleanup;
	}

another:
	if ((len = mnl_socket_recvfrom(nl, rtnl_buffer, SOCKET_BUFFER_SIZE)) < 0) {
		ret = -errno;
		goto cleanup;
	}
	if ((len = mnl_cb_run(rtnl_buffer, len, seq, portid, read_devices_cb, list)) < 0) {
		/* Netlink returns NLM_F_DUMP_INTR if the set of all tunnels changed
		 * during the dump. That's unfortunate, but is pretty common on busy
		 * systems that are adding and removing tunnels all the time. Rather
		 * than retrying, potentially indefinitely, we just work with the
		 * partial results. */
		if (errno != EINTR) {
			ret = -errno;
			goto cleanup;
		}
	}
	if (len == MNL_CB_OK + 1)
		goto another;
	ret = 0;

cleanup:
	free(rtnl_buffer);
	if (nl)
		mnl_socket_close(nl);
	return ret;
}

static int kernel_set_device(struct wgdevice *dev)
{
	int ret = 0;
	struct wgpeer *peer = NULL;
	struct wgallowedip *allowedip = NULL;
	struct nlattr *peers_nest, *peer_nest, *allowedips_nest, *allowedip_nest;
	struct nlmsghdr *nlh;
	struct mnlg_socket *nlg;

	nlg = mnlg_socket_open(WG_GENL_NAME);
	if (!nlg)
		return -errno;

again:
	nlh = mnlg_msg_prepare(nlg, WG_CMD_SET_DEVICE, NLM_F_REQUEST | NLM_F_ACK);
	mnl_attr_put_strz(nlh, WGDEVICE_A_IFNAME, dev->name);

	if (!peer) {
		uint32_t flags = 0;

		if (dev->flags & WGDEVICE_HAS_PRIVATE_KEY)
			mnl_attr_put(nlh, WGDEVICE_A_PRIVATE_KEY, sizeof(dev->private_key), dev->private_key);
		if (dev->flags & WGDEVICE_HAS_LISTEN_PORT)
			mnl_attr_put_u16(nlh, WGDEVICE_A_LISTEN_PORT, dev->listen_port);
		if (dev->flags & WGDEVICE_HAS_JC)
			mnl_attr_put_u16(nlh, WGDEVICE_A_JC, dev->junk_packet_count);
		if (dev->flags & WGDEVICE_HAS_JMIN)
			mnl_attr_put_u16(nlh, WGDEVICE_A_JMIN, dev->junk_packet_min_size);
		if (dev->flags & WGDEVICE_HAS_JMAX)
			mnl_attr_put_u16(nlh, WGDEVICE_A_JMAX, dev->junk_packet_max_size);
		if (dev->flags & WGDEVICE_HAS_S1)
			mnl_attr_put_u16(nlh, WGDEVICE_A_S1, dev->init_packet_junk_size);
		if (dev->flags & WGDEVICE_HAS_S2)
			mnl_attr_put_u16(nlh, WGDEVICE_A_S2, dev->response_packet_junk_size);
		if (dev->flags & WGDEVICE_HAS_S3)
			mnl_attr_put_u16(nlh, WGDEVICE_A_S3, dev->cookie_reply_packet_junk_size);
		if (dev->flags & WGDEVICE_HAS_S4)
			mnl_attr_put_u16(nlh, WGDEVICE_A_S4, dev->transport_packet_junk_size);
		if (dev->flags & WGDEVICE_HAS_H1)
			put_magic_header_attr(nlg->version, nlh, WGDEVICE_A_H1, dev->init_packet_magic_header);
		if (dev->flags & WGDEVICE_HAS_H2)
			put_magic_header_attr(nlg->version, nlh, WGDEVICE_A_H2, dev->response_packet_magic_header);
		if (dev->flags & WGDEVICE_HAS_H3)
			put_magic_header_attr(nlg->version, nlh, WGDEVICE_A_H3, dev->underload_packet_magic_header);
		if (dev->flags & WGDEVICE_HAS_H4)
			put_magic_header_attr(nlg->version, nlh, WGDEVICE_A_H4, dev->transport_packet_magic_header);
		if (dev->flags & WGDEVICE_HAS_I1)
			mnl_attr_put_strz(nlh, WGDEVICE_A_I1, dev->i1);
		if (dev->flags & WGDEVICE_HAS_I2)
			mnl_attr_put_strz(nlh, WGDEVICE_A_I2, dev->i2);
		if (dev->flags & WGDEVICE_HAS_I3)
			mnl_attr_put_strz(nlh, WGDEVICE_A_I3, dev->i3);
		if (dev->flags & WGDEVICE_HAS_I4)
			mnl_attr_put_strz(nlh, WGDEVICE_A_I4, dev->i4);
		if (dev->flags & WGDEVICE_HAS_I5)
			mnl_attr_put_strz(nlh, WGDEVICE_A_I5, dev->i5);
		if (dev->flags & WGDEVICE_HAS_FWMARK)
			mnl_attr_put_u32(nlh, WGDEVICE_A_FWMARK, dev->fwmark);
		if (dev->flags & WGDEVICE_REPLACE_PEERS)
			flags |= WGDEVICE_F_REPLACE_PEERS;
		if (flags)
			mnl_attr_put_u32(nlh, WGDEVICE_A_FLAGS, flags);
	}
	if (!dev->first_peer)
		goto send;
	peers_nest = peer_nest = allowedips_nest = allowedip_nest = NULL;
	peers_nest = mnl_attr_nest_start(nlh, WGDEVICE_A_PEERS);
	for (peer = peer ? peer : dev->first_peer; peer; peer = peer->next_peer) {
		uint32_t flags = 0;

		peer_nest = mnl_attr_nest_start_check(nlh, SOCKET_BUFFER_SIZE, 0);
		if (!peer_nest)
			goto toobig_peers;
		if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_PUBLIC_KEY, sizeof(peer->public_key), peer->public_key))
			goto toobig_peers;
		if (peer->flags & WGPEER_REMOVE_ME)
			flags |= WGPEER_F_REMOVE_ME;
		if (!allowedip) {
			if (peer->flags & WGPEER_REPLACE_ALLOWEDIPS)
				flags |= WGPEER_F_REPLACE_ALLOWEDIPS;
			if (peer->flags & WGPEER_HAS_PRESHARED_KEY) {
				if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_PRESHARED_KEY, sizeof(peer->preshared_key), peer->preshared_key))
					goto toobig_peers;
			}
			if (peer->endpoint.addr.sa_family == AF_INET) {
				if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_ENDPOINT, sizeof(peer->endpoint.addr4), &peer->endpoint.addr4))
					goto toobig_peers;
			} else if (peer->endpoint.addr.sa_family == AF_INET6) {
				if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_ENDPOINT, sizeof(peer->endpoint.addr6), &peer->endpoint.addr6))
					goto toobig_peers;
			}
			if (peer->flags & WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL) {
				if (!mnl_attr_put_u16_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, peer->persistent_keepalive_interval))
					goto toobig_peers;
			}
		}
		if (peer->flags & WGPEER_HAS_AWG) {
			if (peer->awg)
				mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_AWG, 0, NULL);
			flags |= WGPEER_F_HAS_AWG;
		}
		if (flags) {
			if (!mnl_attr_put_u32_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_FLAGS, flags))
				goto toobig_peers;
		}
		if (peer->first_allowedip) {
			if (!allowedip)
				allowedip = peer->first_allowedip;
			allowedips_nest = mnl_attr_nest_start_check(nlh, SOCKET_BUFFER_SIZE, WGPEER_A_ALLOWEDIPS);
			if (!allowedips_nest)
				goto toobig_allowedips;
			for (; allowedip; allowedip = allowedip->next_allowedip) {
				allowedip_nest = mnl_attr_nest_start_check(nlh, SOCKET_BUFFER_SIZE, 0);
				if (!allowedip_nest)
					goto toobig_allowedips;
				if (!mnl_attr_put_u16_check(nlh, SOCKET_BUFFER_SIZE, WGALLOWEDIP_A_FAMILY, allowedip->family))
					goto toobig_allowedips;
				if (allowedip->family == AF_INET) {
					if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGALLOWEDIP_A_IPADDR, sizeof(allowedip->ip4), &allowedip->ip4))
						goto toobig_allowedips;
				} else if (allowedip->family == AF_INET6) {
					if (!mnl_attr_put_check(nlh, SOCKET_BUFFER_SIZE, WGALLOWEDIP_A_IPADDR, sizeof(allowedip->ip6), &allowedip->ip6))
						goto toobig_allowedips;
				}
				if (!mnl_attr_put_u8_check(nlh, SOCKET_BUFFER_SIZE, WGALLOWEDIP_A_CIDR_MASK, allowedip->cidr))
					goto toobig_allowedips;
				mnl_attr_nest_end(nlh, allowedip_nest);
				allowedip_nest = NULL;
			}
			mnl_attr_nest_end(nlh, allowedips_nest);
			allowedips_nest = NULL;
		}

		mnl_attr_nest_end(nlh, peer_nest);
		peer_nest = NULL;
	}
	mnl_attr_nest_end(nlh, peers_nest);
	peers_nest = NULL;
	goto send;
toobig_allowedips:
	if (allowedip_nest)
		mnl_attr_nest_cancel(nlh, allowedip_nest);
	if (allowedips_nest)
		mnl_attr_nest_end(nlh, allowedips_nest);
	mnl_attr_nest_end(nlh, peer_nest);
	mnl_attr_nest_end(nlh, peers_nest);
	goto send;
toobig_peers:
	if (peer_nest)
		mnl_attr_nest_cancel(nlh, peer_nest);
	mnl_attr_nest_end(nlh, peers_nest);
	goto send;
send:
	if (mnlg_socket_send(nlg, nlh) < 0) {
		ret = -errno;
		goto out;
	}
	errno = 0;
	if (mnlg_socket_recv_run(nlg, NULL, NULL) < 0) {
		ret = errno ? -errno : -EINVAL;
		goto out;
	}
	if (peer)
		goto again;

out:
	mnlg_socket_close(nlg);
	errno = -ret;
	return ret;
}

static int parse_allowedip(const struct nlattr *attr, void *data)
{
	struct wgallowedip *allowedip = data;

	switch (mnl_attr_get_type(attr)) {
	case WGALLOWEDIP_A_UNSPEC:
		break;
	case WGALLOWEDIP_A_FAMILY:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16))
			allowedip->family = mnl_attr_get_u16(attr);
		break;
	case WGALLOWEDIP_A_IPADDR:
		if (mnl_attr_get_payload_len(attr) == sizeof(allowedip->ip4))
			memcpy(&allowedip->ip4, mnl_attr_get_payload(attr), sizeof(allowedip->ip4));
		else if (mnl_attr_get_payload_len(attr) == sizeof(allowedip->ip6))
			memcpy(&allowedip->ip6, mnl_attr_get_payload(attr), sizeof(allowedip->ip6));
		break;
	case WGALLOWEDIP_A_CIDR_MASK:
		if (!mnl_attr_validate(attr, MNL_TYPE_U8))
			allowedip->cidr = mnl_attr_get_u8(attr);
		break;
	}

	return MNL_CB_OK;
}

static int parse_allowedips(const struct nlattr *attr, void *data)
{
	struct wgpeer *peer = data;
	struct wgallowedip *new_allowedip = calloc(1, sizeof(*new_allowedip));
	int ret;

	if (!new_allowedip) {
		perror("calloc");
		return MNL_CB_ERROR;
	}
	if (!peer->first_allowedip)
		peer->first_allowedip = peer->last_allowedip = new_allowedip;
	else {
		peer->last_allowedip->next_allowedip = new_allowedip;
		peer->last_allowedip = new_allowedip;
	}
	ret = mnl_attr_parse_nested(attr, parse_allowedip, new_allowedip);
	if (!ret)
		return ret;
	if (!((new_allowedip->family == AF_INET && new_allowedip->cidr <= 32) || (new_allowedip->family == AF_INET6 && new_allowedip->cidr <= 128)))
		return MNL_CB_ERROR;
	return MNL_CB_OK;
}

static int parse_peer(const struct nlattr *attr, void *data)
{
	struct wgpeer *peer = data;

	switch (mnl_attr_get_type(attr)) {
	case WGPEER_A_UNSPEC:
		break;
	case WGPEER_A_PUBLIC_KEY:
		if (mnl_attr_get_payload_len(attr) == sizeof(peer->public_key)) {
			memcpy(peer->public_key, mnl_attr_get_payload(attr), sizeof(peer->public_key));
			peer->flags |= WGPEER_HAS_PUBLIC_KEY;
		}
		break;
	case WGPEER_A_PRESHARED_KEY:
		if (mnl_attr_get_payload_len(attr) == sizeof(peer->preshared_key)) {
			memcpy(peer->preshared_key, mnl_attr_get_payload(attr), sizeof(peer->preshared_key));
			if (!key_is_zero(peer->preshared_key))
				peer->flags |= WGPEER_HAS_PRESHARED_KEY;
		}
		break;
	case WGPEER_A_ENDPOINT: {
		struct sockaddr *addr;

		if (mnl_attr_get_payload_len(attr) < sizeof(*addr))
			break;
		addr = mnl_attr_get_payload(attr);
		if (addr->sa_family == AF_INET && mnl_attr_get_payload_len(attr) == sizeof(peer->endpoint.addr4))
			memcpy(&peer->endpoint.addr4, addr, sizeof(peer->endpoint.addr4));
		else if (addr->sa_family == AF_INET6 && mnl_attr_get_payload_len(attr) == sizeof(peer->endpoint.addr6))
			memcpy(&peer->endpoint.addr6, addr, sizeof(peer->endpoint.addr6));
		break;
	}
	case WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16))
			peer->persistent_keepalive_interval = mnl_attr_get_u16(attr);
		break;
	case WGPEER_A_LAST_HANDSHAKE_TIME:
		if (mnl_attr_get_payload_len(attr) == sizeof(peer->last_handshake_time))
			memcpy(&peer->last_handshake_time, mnl_attr_get_payload(attr), sizeof(peer->last_handshake_time));
		break;
	case WGPEER_A_RX_BYTES:
		if (!mnl_attr_validate(attr, MNL_TYPE_U64))
			peer->rx_bytes = mnl_attr_get_u64(attr);
		break;
	case WGPEER_A_TX_BYTES:
		if (!mnl_attr_validate(attr, MNL_TYPE_U64))
			peer->tx_bytes = mnl_attr_get_u64(attr);
		break;
	case WGPEER_A_FLAGS:
		if (!mnl_attr_validate(attr, MNL_TYPE_U32)) {
			uint32_t flags = mnl_attr_get_u32(attr);

			if (flags & WGPEER_F_HAS_AWG && !(peer->flags & WGPEER_HAS_AWG)) {
				peer->flags |= WGPEER_HAS_AWG;
				peer->awg = false;
			}
		}
		break;
	case WGPEER_A_AWG:
		if (!mnl_attr_validate(attr, MNL_TYPE_FLAG)) {
			peer->awg = true;

			if (!(peer->flags & WGPEER_HAS_AWG)) {
				peer->flags |= WGPEER_HAS_AWG;
			}
		}
		break;
	case WGPEER_A_ALLOWEDIPS:
		return mnl_attr_parse_nested(attr, parse_allowedips, peer);
	}

	return MNL_CB_OK;
}

static int parse_peers(const struct nlattr *attr, void *data)
{
	struct wgdevice *device = data;
	struct wgpeer *new_peer = calloc(1, sizeof(*new_peer));
	int ret;

	if (!new_peer) {
		perror("calloc");
		return MNL_CB_ERROR;
	}
	if (!device->first_peer)
		device->first_peer = device->last_peer = new_peer;
	else {
		device->last_peer->next_peer = new_peer;
		device->last_peer = new_peer;
	}
	ret = mnl_attr_parse_nested(attr, parse_peer, new_peer);
	if (!ret)
		return ret;
	if (!(new_peer->flags & WGPEER_HAS_PUBLIC_KEY))
		return MNL_CB_ERROR;
	return MNL_CB_OK;
}

static int parse_device(const struct nlattr *attr, void *data)
{
	struct wgdevice *device = data;

	switch (mnl_attr_get_type(attr)) {
	case WGDEVICE_A_UNSPEC:
		break;
	case WGDEVICE_A_IFINDEX:
		if (!mnl_attr_validate(attr, MNL_TYPE_U32))
			device->ifindex = mnl_attr_get_u32(attr);
		break;
	case WGDEVICE_A_IFNAME:
		if (!mnl_attr_validate(attr, MNL_TYPE_STRING)) {
			strncpy(device->name, mnl_attr_get_str(attr), sizeof(device->name) - 1);
			device->name[sizeof(device->name) - 1] = '\0';
		}
		break;
	case WGDEVICE_A_PRIVATE_KEY:
		if (mnl_attr_get_payload_len(attr) == sizeof(device->private_key)) {
			memcpy(device->private_key, mnl_attr_get_payload(attr), sizeof(device->private_key));
			device->flags |= WGDEVICE_HAS_PRIVATE_KEY;
		}
		break;
	case WGDEVICE_A_PUBLIC_KEY:
		if (mnl_attr_get_payload_len(attr) == sizeof(device->public_key)) {
			memcpy(device->public_key, mnl_attr_get_payload(attr), sizeof(device->public_key));
			device->flags |= WGDEVICE_HAS_PUBLIC_KEY;
		}
		break;
	case WGDEVICE_A_LISTEN_PORT:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16))
			device->listen_port = mnl_attr_get_u16(attr);
		break;
	case WGDEVICE_A_FWMARK:
		if (!mnl_attr_validate(attr, MNL_TYPE_U32))
			device->fwmark = mnl_attr_get_u32(attr);
		break;
	case WGDEVICE_A_PEERS:
		return mnl_attr_parse_nested(attr, parse_peers, device);
	case WGDEVICE_A_JC:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->junk_packet_count = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_JC;
		}
		break;
	case WGDEVICE_A_JMIN:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->junk_packet_min_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_JMIN;
		}
		break;
	case WGDEVICE_A_JMAX:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->junk_packet_max_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_JMAX;
		}
		break;
	case WGDEVICE_A_S1:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->init_packet_junk_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_S1;
		}
		break;
	case WGDEVICE_A_S2:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->response_packet_junk_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_S2;
		}
		break;
	case WGDEVICE_A_S3:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->cookie_reply_packet_junk_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_S3;
		}
		break;
	case WGDEVICE_A_S4:
		if (!mnl_attr_validate(attr, MNL_TYPE_U16)) {
			device->transport_packet_junk_size = mnl_attr_get_u16(attr);
			device->flags |= WGDEVICE_HAS_S4;
		}
		break;
	case WGDEVICE_A_H1:
		GET_MAGIC_HEADER(attr, device->init_packet_magic_header, WGDEVICE_HAS_H1);
		break;
	case WGDEVICE_A_H2:
		GET_MAGIC_HEADER(attr, device->response_packet_magic_header, WGDEVICE_HAS_H2);
		break;
	case WGDEVICE_A_H3:
		GET_MAGIC_HEADER(attr, device->underload_packet_magic_header, WGDEVICE_HAS_H3);
		break;
	case WGDEVICE_A_H4:
		GET_MAGIC_HEADER(attr, device->transport_packet_magic_header, WGDEVICE_HAS_H4);
		break;
	case WGDEVICE_A_I1:
		if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {
			device->i1 = strdup(mnl_attr_get_str(attr));
			if (!device->i1) {
				perror("strdup");
				return MNL_CB_ERROR;
			}

			device->flags |= WGDEVICE_HAS_I1;
		}
		break;
	case WGDEVICE_A_I2:
		if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {
			device->i2 = strdup(mnl_attr_get_str(attr));
			if (!device->i2) {
				perror("strdup");
				return MNL_CB_ERROR;
			}

			device->flags |= WGDEVICE_HAS_I2;
		}
		break;
	case WGDEVICE_A_I3:
		if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {
			device->i3 = strdup(mnl_attr_get_str(attr));
			if (!device->i3) {
				perror("strdup");
				return MNL_CB_ERROR;
			}

			device->flags |= WGDEVICE_HAS_I3;
		}
		break;
	case WGDEVICE_A_I4:
		if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {
			device->i4 = strdup(mnl_attr_get_str(attr));
			if (!device->i4) {
				perror("strdup");
				return MNL_CB_ERROR;
			}

			device->flags |= WGDEVICE_HAS_I4;
		}
		break;
	case WGDEVICE_A_I5:
		if (!mnl_attr_validate(attr, MNL_TYPE_NUL_STRING)) {
			device->i5 = strdup(mnl_attr_get_str(attr));
			if (!device->i5) {
				perror("strdup");
				return MNL_CB_ERROR;
			}

			device->flags |= WGDEVICE_HAS_I5;
		}
		break;
	}

	return MNL_CB_OK;
}

static int read_device_cb(const struct nlmsghdr *nlh, void *data)
{
	return mnl_attr_parse(nlh, sizeof(struct genlmsghdr), parse_device, data);
}

static void coalesce_peers(struct wgdevice *device)
{
	struct wgpeer *old_next_peer, *peer = device->first_peer;

	while (peer && peer->next_peer) {
		if (memcmp(peer->public_key, peer->next_peer->public_key, sizeof(peer->public_key))) {
			peer = peer->next_peer;
			continue;
		}
		if (!peer->first_allowedip) {
			peer->first_allowedip = peer->next_peer->first_allowedip;
			peer->last_allowedip = peer->next_peer->last_allowedip;
		} else {
			peer->last_allowedip->next_allowedip = peer->next_peer->first_allowedip;
			peer->last_allowedip = peer->next_peer->last_allowedip;
		}
		old_next_peer = peer->next_peer;
		peer->next_peer = old_next_peer->next_peer;
		free(old_next_peer);
	}
}

static int kernel_get_device(struct wgdevice **device, const char *iface)
{
	int ret;
	struct nlmsghdr *nlh;
	struct mnlg_socket *nlg;

	/* libmnl doesn't check the buffer size, so enforce that before using. */
	if (strlen(iface) >= IFNAMSIZ) {
		errno = ENAMETOOLONG;
		return -ENAMETOOLONG;
	}

try_again:
	ret = 0;
	*device = calloc(1, sizeof(**device));
	if (!*device)
		return -errno;

	nlg = mnlg_socket_open(WG_GENL_NAME);
	if (!nlg) {
		free_wgdevice(*device);
		*device = NULL;
		return -errno;
	}

	nlh = mnlg_msg_prepare(nlg, WG_CMD_GET_DEVICE, NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP);
	mnl_attr_put_strz(nlh, WGDEVICE_A_IFNAME, iface);
	if (mnlg_socket_send(nlg, nlh) < 0) {
		ret = -errno;
		goto out;
	}
	errno = 0;
	if (mnlg_socket_recv_run(nlg, read_device_cb, *device) < 0) {
		ret = errno ? -errno : -EINVAL;
		goto out;
	}
	coalesce_peers(*device);

out:
	if (nlg)
		mnlg_socket_close(nlg);
	if (ret) {
		free_wgdevice(*device);
		if (ret == -EINTR)
			goto try_again;
		*device = NULL;
	}
	errno = -ret;
	return ret;
}
