/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <isc/proxy2.h>

enum isc_proxy2_states {
	ISC_PROXY2_STATE_WAITING_SIGNATURE,
	ISC_PROXY2_STATE_WAITING_HEADER,
	ISC_PROXY2_STATE_WAITING_PAYLOAD, /* Addresses and TLVs */
	ISC_PROXY2_STATE_END
};

static inline void
isc__proxy2_handler_init_direct(isc_proxy2_handler_t *restrict handler,
				const uint16_t max_size,
				const isc_region_t *restrict data,
				isc_proxy2_handler_cb_t cb, void *cbarg) {
	*handler = (isc_proxy2_handler_t){ .result = ISC_R_UNSET,
					   .max_size = max_size };
	isc_proxy2_handler_setcb(handler, cb, cbarg);

	if (data == NULL) {
		isc_buffer_init(&handler->hdrbuf, handler->buf,
				sizeof(handler->buf));
	} else {
		isc_buffer_init(&handler->hdrbuf, data->base, data->length);
		isc_buffer_add(&handler->hdrbuf, data->length);
	}
}

void
isc_proxy2_handler_init(isc_proxy2_handler_t *restrict handler, isc_mem_t *mctx,
			const uint16_t max_size, isc_proxy2_handler_cb_t cb,
			void *cbarg) {
	REQUIRE(handler != NULL);
	REQUIRE(mctx != NULL);
	REQUIRE(max_size == 0 || max_size >= ISC_PROXY2_HEADER_SIZE);
	REQUIRE(cb != NULL);

	isc__proxy2_handler_init_direct(handler, max_size, NULL, cb, cbarg);

	isc_mem_attach(mctx, &handler->mctx);
	isc_buffer_setmctx(&handler->hdrbuf, handler->mctx);
}

void
isc_proxy2_handler_uninit(isc_proxy2_handler_t *restrict handler) {
	REQUIRE(handler != NULL);

	/*
	 * Uninitialising the object from withing the callback does not
	 * make any sense.
	 */
	INSIST(handler->calling_cb == false);
	if (handler->mctx != NULL) {
		isc_buffer_clearmctx(&handler->hdrbuf);
		isc_mem_detach(&handler->mctx);
	}
	isc_buffer_invalidate(&handler->hdrbuf);
}

void
isc_proxy2_handler_clear(isc_proxy2_handler_t *restrict handler) {
	REQUIRE(handler != NULL);

	*handler = (isc_proxy2_handler_t){ .result = ISC_R_UNSET,
					   .mctx = handler->mctx,
					   .cb = handler->cb,
					   .cbarg = handler->cbarg,
					   .hdrbuf = handler->hdrbuf,
					   .max_size = handler->max_size };

	isc_buffer_clear(&handler->hdrbuf);
	isc_buffer_trycompact(&handler->hdrbuf);
}

isc_proxy2_handler_t *
isc_proxy2_handler_new(isc_mem_t *mctx, const uint16_t max_size,
		       isc_proxy2_handler_cb_t cb, void *cbarg) {
	isc_proxy2_handler_t *newhandler;

	REQUIRE(mctx != NULL);
	REQUIRE(cb != NULL);

	newhandler = isc_mem_get(mctx, sizeof(*newhandler));
	isc_proxy2_handler_init(newhandler, mctx, max_size, cb, cbarg);

	return newhandler;
}

void
isc_proxy2_handler_free(isc_proxy2_handler_t **restrict phandler) {
	isc_proxy2_handler_t *restrict handler = NULL;
	isc_mem_t *mctx = NULL;
	REQUIRE(phandler != NULL && *phandler != NULL);

	handler = *phandler;

	isc_mem_attach(handler->mctx, &mctx);
	isc_proxy2_handler_uninit(handler);
	isc_mem_putanddetach(&mctx, handler, sizeof(*handler));

	*phandler = NULL;
}

void
isc_proxy2_handler_setcb(isc_proxy2_handler_t *restrict handler,
			 isc_proxy2_handler_cb_t cb, void *cbarg) {
	REQUIRE(handler != NULL);
	REQUIRE(cb != NULL);
	handler->cb = cb;
	handler->cbarg = cbarg;
}

static inline int
proxy2_socktype_to_socktype(const isc_proxy2_socktype_t proxy_socktype) {
	int socktype = 0;

	switch (proxy_socktype) {
	case ISC_PROXY2_SOCK_UNSPEC:
		socktype = 0;
		break;
	case ISC_PROXY2_SOCK_STREAM:
		socktype = SOCK_STREAM;
		break;
	case ISC_PROXY2_SOCK_DGRAM:
		socktype = SOCK_DGRAM;
		break;
	default:
		ISC_UNREACHABLE();
	};

	return socktype;
}

static inline void
isc__proxy2_handler_callcb(isc_proxy2_handler_t *restrict handler,
			   const isc_result_t result,
			   const isc_proxy2_command_t cmd,
			   const isc_proxy2_socktype_t proxy_socktype,
			   const isc_sockaddr_t *src_addr,
			   const isc_sockaddr_t *dst_addr,
			   const isc_region_t *restrict tlv_data,
			   const isc_region_t *restrict extra_data) {
	int socktype = 0;

	handler->result = result;
	handler->calling_cb = true;

	if (result != ISC_R_SUCCESS) {
		handler->cb(result, cmd, -1, NULL, NULL, NULL, NULL,
			    handler->cbarg);
	} else {
		socktype = proxy2_socktype_to_socktype(proxy_socktype);
		handler->cb(result, cmd, socktype,
			    proxy_socktype == ISC_PROXY2_SOCK_UNSPEC ? NULL
								     : src_addr,
			    proxy_socktype == ISC_PROXY2_SOCK_UNSPEC ? NULL
								     : dst_addr,
			    tlv_data->length == 0 ? NULL : tlv_data,
			    extra_data->length == 0 ? NULL : extra_data,
			    handler->cbarg);
	}

	handler->calling_cb = false;
}

static inline void
isc__proxy2_handler_error(isc_proxy2_handler_t *restrict handler,
			  const isc_result_t result) {
	INSIST(result != ISC_R_SUCCESS);
	isc__proxy2_handler_callcb(handler, result, ISC_PROXY2_CMD_ILLEGAL,
				   ISC_PROXY2_SOCK_ILLEGAL, NULL, NULL, NULL,
				   NULL);
	if (result != ISC_R_NOMORE) {
		handler->state = ISC_PROXY2_STATE_END;
	}
}

static inline bool
isc__proxy2_handler_handle_signature(isc_proxy2_handler_t *restrict handler) {
	isc_region_t remaining = { 0, 0 };
	size_t len;

	isc_buffer_remainingregion(&handler->hdrbuf, &remaining);
	len = ISC_MIN(remaining.length, ISC_PROXY2_HEADER_SIGNATURE_SIZE);

	if (memcmp(ISC_PROXY2_HEADER_SIGNATURE, remaining.base, len) != 0) {
		isc__proxy2_handler_error(handler, ISC_R_UNEXPECTED);
		return false;
	} else if (len == ISC_PROXY2_HEADER_SIGNATURE_SIZE) {
		isc_buffer_forward(&handler->hdrbuf,
				   ISC_PROXY2_HEADER_SIGNATURE_SIZE);
		handler->expect_data = ISC_PROXY2_HEADER_SIZE -
				       ISC_PROXY2_HEADER_SIGNATURE_SIZE;
		handler->state++;
	} else {
		INSIST(len < ISC_PROXY2_HEADER_SIGNATURE_SIZE);
		isc__proxy2_handler_error(handler, ISC_R_NOMORE);
		return false;
	}
	return true;
}

static inline bool
isc__proxy2_handler_handle_header(isc_proxy2_handler_t *restrict handler) {
	/*
	 * The PROXYv2 header can be described as (signature 'sig' has been
	 * processed and verified already as a separate step):
	 *
	 *  struct proxy_hdr_v2 {
	 *     uint8_t sig[12];  // hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A
	 *     uint8_t ver_cmd;  // protocol version and command
	 *     uint8_t fam;      // protocol family and address
	 *     uint16_t len;     // number of following bytes part of the header
	 *  };
	 */
	uint8_t ver_cmd = 0;
	uint8_t cmd = 0;
	uint8_t fam = 0;
	uint16_t len = 0;
	int addrfamily = 0;
	int socktype = 0;
	size_t min_addr_payload_size = 0;

	ver_cmd = isc_buffer_getuint8(&handler->hdrbuf);

	/* extract version and check it */
	if ((ver_cmd & 0xF0U) >> 4 != 2) {
		/* only support for version 2 is implemented */
		isc__proxy2_handler_error(handler, ISC_R_NOTIMPLEMENTED);
		return false;
	}

	/* extract command */
	cmd = ver_cmd & 0xFU;

	fam = isc_buffer_getuint8(&handler->hdrbuf);
	len = isc_buffer_getuint16(&handler->hdrbuf);

	if (handler->max_size > 0 &&
	    (len + ISC_PROXY2_HEADER_SIZE) > handler->max_size)
	{
		goto error_range;
	}

	handler->expect_data = len;

	/* extract address family and socket type */
	addrfamily = (fam & 0xF0U) >> 4;
	socktype = fam & 0xFU;

	/* dispatch on the command value */
	switch (cmd) {
	case ISC_PROXY2_CMD_LOCAL:
		/* LOCAL implies "unspec" mode */
		handler->cmd = ISC_PROXY2_CMD_LOCAL;
		if (addrfamily != ISC_PROXY2_AF_UNSPEC ||
		    socktype != ISC_PROXY2_SOCK_UNSPEC)
		{
			goto error_unexpected;
		}
		handler->proxy_addr_family = ISC_PROXY2_AF_UNSPEC;
		handler->proxy_socktype = ISC_PROXY2_SOCK_UNSPEC;
		break;
	case ISC_PROXY2_CMD_PROXY:
		handler->cmd = ISC_PROXY2_CMD_PROXY;
		switch (addrfamily) {
		case ISC_PROXY2_AF_UNSPEC:
			if (socktype != ISC_PROXY2_SOCK_UNSPEC) {
				goto error_unexpected;
			}
			handler->proxy_addr_family = ISC_PROXY2_AF_UNSPEC;
			handler->proxy_socktype = ISC_PROXY2_SOCK_UNSPEC;
			break;
		case ISC_PROXY2_AF_INET:
		case ISC_PROXY2_AF_INET6:
		case ISC_PROXY2_AF_UNIX:
			handler->proxy_addr_family =
				(isc_proxy2_addrfamily_t)addrfamily;
			switch (socktype) {
			case ISC_PROXY2_SOCK_DGRAM:
			case ISC_PROXY2_SOCK_STREAM:
				handler->proxy_socktype =
					(isc_proxy2_socktype_t)socktype;
				break;
			default:
				goto error_unexpected;
			}
			break;
		default:
			goto error_unexpected;
		}
		break;
	default:
		goto error_unexpected;
	};

	/* verify if enough data will be available in the payload */
	switch (handler->proxy_addr_family) {
	case ISC_PROXY2_AF_INET:
		min_addr_payload_size = ISC_PROXY2_MIN_AF_INET_SIZE -
					ISC_PROXY2_HEADER_SIZE;
		break;
	case ISC_PROXY2_AF_INET6:
		min_addr_payload_size = ISC_PROXY2_MIN_AF_INET6_SIZE -
					ISC_PROXY2_HEADER_SIZE;
		break;
	case ISC_PROXY2_AF_UNIX:
		min_addr_payload_size = ISC_PROXY2_MIN_AF_UNIX_SIZE -
					ISC_PROXY2_HEADER_SIZE;
		break;
	default:
		break;
	}

	if (min_addr_payload_size > 0) {
		if (len < min_addr_payload_size) {
			goto error_range;
		}
		handler->tlv_data_size = len - min_addr_payload_size;
	}

	if (handler->tlv_data_size > 0 &&
	    handler->tlv_data_size < ISC_PROXY2_TLV_HEADER_SIZE)
	{
		goto error_range;
	}

	handler->header_size = ISC_PROXY2_HEADER_SIZE + len;

	handler->state++;

	return true;

error_unexpected:
	isc__proxy2_handler_error(handler, ISC_R_UNEXPECTED);
	return false;
error_range:
	isc__proxy2_handler_error(handler, ISC_R_RANGE);
	return false;
}

static inline isc_result_t
isc__proxy2_handler_get_addresses(isc_proxy2_handler_t *restrict handler,
				  isc_buffer_t *restrict hdrbuf,
				  isc_sockaddr_t *restrict src_addr,
				  isc_sockaddr_t *restrict dst_addr) {
	size_t addr_size = 0;
	void *psrc_addr = NULL, *pdst_addr = NULL;
	uint16_t src_port = 0, dst_port = 0;

	switch (handler->proxy_addr_family) {
	case ISC_PROXY2_AF_UNSPEC:
		/* in this case we are instructed to skip over the data */
		INSIST(handler->tlv_data_size == 0);
		isc_buffer_forward(hdrbuf, handler->expect_data);
		break;
	case ISC_PROXY2_AF_INET:
		addr_size = sizeof(src_addr->type.sin.sin_addr.s_addr);
		/*
		 * IPv4 source and destination endpoint addresses can be
		 * described as follows:
		 *
		 * struct {        // for TCP/UDP over IPv4, len = 12
		 *   uint32_t src_addr;
		 *   uint32_t dst_addr;
		 *   uint16_t src_port;
		 *   uint16_t dst_port;
		 * } ipv4_addr;
		 */
		psrc_addr = isc_buffer_current(hdrbuf);
		isc_buffer_forward(hdrbuf, addr_size);

		pdst_addr = isc_buffer_current(hdrbuf);
		isc_buffer_forward(hdrbuf, addr_size);

		src_port = isc_buffer_getuint16(hdrbuf);
		dst_port = isc_buffer_getuint16(hdrbuf);

		if (src_addr != NULL) {
			isc_sockaddr_fromin(src_addr, psrc_addr, src_port);
		}
		if (dst_addr != NULL) {
			isc_sockaddr_fromin(dst_addr, pdst_addr, dst_port);
		}
		break;
	case ISC_PROXY2_AF_INET6:
		addr_size = sizeof(src_addr->type.sin6.sin6_addr);
		/*
		 * IPv4 source and destination endpoint addresses can be
		 * described as follows:
		 *
		 * struct {        // for TCP/UDP over IPv6, len = 36
		 *    uint8_t  src_addr[16];
		 *    uint8_t  dst_addr[16];
		 *    uint16_t src_port;
		 *    uint16_t dst_port;
		 * } ipv6_addr;
		 */
		psrc_addr = isc_buffer_current(hdrbuf);
		isc_buffer_forward(hdrbuf, addr_size);

		pdst_addr = isc_buffer_current(hdrbuf);
		isc_buffer_forward(hdrbuf, addr_size);

		src_port = isc_buffer_getuint16(hdrbuf);
		dst_port = isc_buffer_getuint16(hdrbuf);

		if (src_addr != NULL) {
			isc_sockaddr_fromin6(src_addr, psrc_addr, src_port);
		}

		if (dst_addr != NULL) {
			isc_sockaddr_fromin6(dst_addr, pdst_addr, dst_port);
		}
		break;
	case ISC_PROXY2_AF_UNIX: {
		/*
		 * UNIX domain sockets source and destination endpoint
		 * addresses can be described as follows:
		 *
		 * struct {        // for AF_UNIX sockets, len = 216
		 *    uint8_t src_addr[108];
		 *    uint8_t dst_addr[108];
		 * } unix_addr;
		 *
		 * We currently have no use for this address type, but we can
		 * validate the data.
		 */
		unsigned char *ret = NULL;

		addr_size = ISC_PROXY2_AF_UNIX_MAX_PATH_LEN;

		ret = memchr(isc_buffer_current(hdrbuf), '\0', addr_size);
		if (ret == NULL) {
			/*
			 * Someone has attempted to send us a path string
			 * without a terminating '\0' byte - not a friend
			 * knocking at the door.
			 */
			return ISC_R_RANGE;
		}
		isc_buffer_forward(hdrbuf, addr_size);

		ret = memchr(isc_buffer_current(hdrbuf), '\0', addr_size);
		if (ret == NULL) {
			return ISC_R_RANGE;
		}
		isc_buffer_forward(hdrbuf, addr_size);
	} break;
	default:
		UNREACHABLE();
	}

	return ISC_R_SUCCESS;
}

static inline void
isc__proxy2_handler_handle_payload(isc_proxy2_handler_t *restrict handler) {
	isc_result_t result;
	isc_sockaddr_t src_addr = { 0 }, dst_addr = { 0 };

	result = isc__proxy2_handler_get_addresses(handler, &handler->hdrbuf,
						   &src_addr, &dst_addr);

	if (result != ISC_R_SUCCESS) {
		isc__proxy2_handler_error(handler, result);
		return;
	}

	if (handler->tlv_data_size > 0) {
		isc_buffer_remainingregion(&handler->hdrbuf,
					   &handler->tlv_data);
		handler->tlv_data.length = handler->tlv_data_size;
		isc_buffer_forward(&handler->hdrbuf, handler->tlv_data_size);
		result = isc_proxy2_tlv_data_verify(&handler->tlv_data);
		if (result != ISC_R_SUCCESS) {
			isc__proxy2_handler_error(handler, result);
			return;
		}
	}

	isc_buffer_remainingregion(&handler->hdrbuf, &handler->extra_data);
	handler->expect_data = 0;

	handler->state++;

	/*
	 * Treat AF_UNIX as AF_UNSPEC as we have no use for it, although
	 * at this point we have fully verified the header.
	 */
	if (handler->proxy_addr_family == ISC_PROXY2_AF_UNIX) {
		handler->proxy_addr_family = ISC_PROXY2_AF_UNSPEC;
		handler->proxy_socktype = ISC_PROXY2_SOCK_UNSPEC;
		handler->tlv_data = (isc_region_t){ 0 };
	}

	isc__proxy2_handler_callcb(
		handler, ISC_R_SUCCESS, handler->cmd, handler->proxy_socktype,
		&src_addr, &dst_addr, &handler->tlv_data, &handler->extra_data);

	return;
}

static inline bool
isc__proxy2_handler_handle_data(isc_proxy2_handler_t *restrict handler) {
	if (isc_buffer_remaininglength(&handler->hdrbuf) < handler->expect_data)
	{
		isc__proxy2_handler_error(handler, ISC_R_NOMORE);
		return false;
	}

	switch (handler->state) {
	case ISC_PROXY2_STATE_WAITING_SIGNATURE:
		/*
		 * We check for signature no matter how many bytes of it we
		 * have received. The idea is to not wait for the whole
		 * signature to verify it at once, but to detect, e.g. port
		 * scanners as early as possible. Should we receive data byte
		 * by byte, we would detect the problem when processing the
		 * first unexpected byte.
		 */
		return isc__proxy2_handler_handle_signature(handler);
	case ISC_PROXY2_STATE_WAITING_HEADER:
		/*
		 * Handle the rest of the header (except signature which we
		 * heave verified by now).
		 */
		return isc__proxy2_handler_handle_header(handler);
	case ISC_PROXY2_STATE_WAITING_PAYLOAD:
		/*
		 * Handle the PROXYv2 header payload - addresses and TLVs.
		 */
		isc__proxy2_handler_handle_payload(handler);
		break;
	default:
		UNREACHABLE();
		break;
	};

	return false;
}

static inline isc_result_t
isc__proxy2_handler_process_data(isc_proxy2_handler_t *restrict handler) {
	while (isc__proxy2_handler_handle_data(handler)) {
		if (handler->state == ISC_PROXY2_STATE_END) {
			break;
		}
	}

	return handler->result;
}

isc_result_t
isc_proxy2_handler_push_data(isc_proxy2_handler_t *restrict handler,
			     const void *restrict buf,
			     const unsigned int buf_size) {
	isc_result_t result;

	REQUIRE(handler != NULL);
	REQUIRE(buf != NULL && buf_size != 0);

	INSIST(!handler->calling_cb);

	if (handler->state == ISC_PROXY2_STATE_END) {
		isc_proxy2_handler_clear(handler);
	}

	isc_buffer_putmem(&handler->hdrbuf, buf, buf_size);

	result = isc__proxy2_handler_process_data(handler);

	return result;
}

isc_result_t
isc_proxy2_handler_push(isc_proxy2_handler_t *restrict handler,
			const isc_region_t *restrict region) {
	isc_result_t result;

	REQUIRE(handler != NULL);
	REQUIRE(region != NULL);

	result = isc_proxy2_handler_push_data(handler, region->base,
					      region->length);

	return result;
}

static inline bool
proxy2_payload_is_processed(const isc_proxy2_handler_t *restrict handler) {
	if (handler->state < ISC_PROXY2_STATE_END ||
	    handler->result != ISC_R_SUCCESS)
	{
		return false;
	}

	return true;
}

size_t
isc_proxy2_handler_header(const isc_proxy2_handler_t *restrict handler,
			  isc_region_t *restrict region) {
	REQUIRE(handler != NULL);
	REQUIRE(region == NULL ||
		(region->base == NULL && region->length == 0));

	if (!proxy2_payload_is_processed(handler)) {
		return 0;
	}

	if (region != NULL) {
		region->base = isc_buffer_base(&handler->hdrbuf);
		region->length = handler->header_size;
	}

	return handler->header_size;
}

size_t
isc_proxy2_handler_tlvs(const isc_proxy2_handler_t *restrict handler,
			isc_region_t *restrict region) {
	REQUIRE(handler != NULL);
	REQUIRE(region == NULL ||
		(region->base == NULL && region->length == 0));

	if (!proxy2_payload_is_processed(handler)) {
		return 0;
	}

	SET_IF_NOT_NULL(region, handler->tlv_data);

	return handler->tlv_data.length;
}

size_t
isc_proxy2_handler_extra(const isc_proxy2_handler_t *restrict handler,
			 isc_region_t *restrict region) {
	REQUIRE(handler != NULL);
	REQUIRE(region == NULL ||
		(region->base == NULL && region->length == 0));

	if (!proxy2_payload_is_processed(handler)) {
		return 0;
	}

	SET_IF_NOT_NULL(region, handler->extra_data);

	return handler->extra_data.length;
}

isc_result_t
isc_proxy2_handler_result(const isc_proxy2_handler_t *restrict handler) {
	REQUIRE(handler != NULL);

	return handler->result;
}

isc_result_t
isc_proxy2_handler_addresses(const isc_proxy2_handler_t *restrict handler,
			     int *restrict psocktype,
			     isc_sockaddr_t *restrict psrc_addr,
			     isc_sockaddr_t *restrict pdst_addr) {
	isc_result_t result;
	size_t ret;
	isc_region_t header_region = { 0 };
	isc_buffer_t buf = { 0 };

	REQUIRE(handler != NULL);

	if (!proxy2_payload_is_processed(handler)) {
		return ISC_R_UNEXPECTED;
	}

	ret = isc_proxy2_handler_header(handler, &header_region);
	RUNTIME_CHECK(ret > 0);

	isc_buffer_init(&buf, header_region.base, header_region.length);
	isc_buffer_add(&buf, header_region.length);
	isc_buffer_forward(&buf, ISC_PROXY2_HEADER_SIZE);

	INSIST(handler->expect_data == 0);

	result = isc__proxy2_handler_get_addresses(
		(isc_proxy2_handler_t *)handler, &buf, psrc_addr, pdst_addr);

	if (result != ISC_R_SUCCESS) {
		return result;
	}

	SET_IF_NOT_NULL(psocktype,
			proxy2_socktype_to_socktype(handler->proxy_socktype));

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_tlv_iterate(const isc_region_t *restrict tlv_data,
		       const isc_proxy2_tlv_cb_t cb, void *cbarg) {
	isc_result_t result = ISC_R_SUCCESS;
	isc_buffer_t tlvs = { 0 };
	size_t remaining;

	/*
	 * TLV header can be described as follows:
	 *
	 *   struct {
	 *       uint8_t type;
	 *       uint8_t length_hi;
	 *       uint8_t length_lo;
	 *   };
	 *
	 */

	REQUIRE(tlv_data != NULL);
	REQUIRE(cb != NULL);

	isc_buffer_init(&tlvs, tlv_data->base, tlv_data->length);
	isc_buffer_add(&tlvs, tlv_data->length);

	while ((remaining = isc_buffer_remaininglength(&tlvs)) > 0) {
		uint8_t type = 0;
		uint16_t len = 0;
		isc_region_t current_tlv_data = { 0 };
		bool ret = false;

		/* not enough data for a TLV header */
		if (remaining < ISC_PROXY2_TLV_HEADER_SIZE) {
			result = ISC_R_RANGE;
			break;
		}

		type = isc_buffer_getuint8(&tlvs);
		len = isc_buffer_getuint16(&tlvs);

		if ((remaining - ISC_PROXY2_TLV_HEADER_SIZE) < len) {
			result = ISC_R_RANGE;
			break;
		}

		current_tlv_data.base = isc_buffer_current(&tlvs);
		current_tlv_data.length = len;
		isc_buffer_forward(&tlvs, len);

		ret = cb((isc_proxy2_tlv_type_t)type, &current_tlv_data, cbarg);
		if (!ret) {
			break;
		}
	}

	return result;
}

typedef struct proxy2_tls_cbarg {
	uint8_t client;
	bool client_cert_verified;
	isc_proxy2_tls_subtlv_cb_t cb;
	void *cbarg;
} tls_cbarg_t;

static bool
proxy2_tls_iter_cb(const isc_proxy2_tlv_type_t tlv_type,
		   const isc_region_t *restrict data, void *cbarg) {
	bool ret = false;
	tls_cbarg_t *tls_cbarg = (tls_cbarg_t *)cbarg;

	ret = tls_cbarg->cb(tls_cbarg->client, tls_cbarg->client_cert_verified,
			    (isc_proxy2_tlv_subtype_tls_t)tlv_type, data,
			    tls_cbarg->cbarg);

	return ret;
}

isc_result_t
isc_proxy2_subtlv_tls_header_data(const isc_region_t *restrict tls_tlv_data,
				  uint8_t *restrict pclient_flags,
				  bool *restrict pclient_cert_verified) {
	/*
	 * SSL/TLS TLV header can be described as follows:
	 *
	 *   struct {
	 *       uint8_t  client_flags;
	 *       uint32_t client_cert_not_verified;
	 *   }
	 */
	uint8_t *p = NULL;
	uint8_t client_flags = 0;
	bool client_cert_verified = false;
	uint32_t client_cert_verified_data = 0;

	REQUIRE(tls_tlv_data != NULL);
	REQUIRE(pclient_flags == NULL || *pclient_flags == 0);
	REQUIRE(pclient_cert_verified == NULL ||
		*pclient_cert_verified == false);

	if (tls_tlv_data->length < ISC_PROXY2_TLS_SUBHEADER_MIN_SIZE) {
		return ISC_R_RANGE;
	}

	p = tls_tlv_data->base;

	client_flags = *p;
	p++;
	/* We need this to avoid ASAN complain about unaligned access */
	memmove(&client_cert_verified_data, p, sizeof(uint32_t));
	client_cert_verified = ntohl(client_cert_verified_data) == 0;

	SET_IF_NOT_NULL(pclient_flags, client_flags);
	SET_IF_NOT_NULL(pclient_cert_verified, client_cert_verified);

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_subtlv_tls_iterate(const isc_region_t *restrict tls_tlv_data,
			      const isc_proxy2_tls_subtlv_cb_t cb,
			      void *cbarg) {
	tls_cbarg_t tls_cbarg;
	isc_result_t result = ISC_R_SUCCESS;
	uint8_t *p = NULL;
	uint8_t client_flags = 0;
	bool client_cert_verified = false;

	REQUIRE(tls_tlv_data != NULL);
	REQUIRE(cb != NULL);

	if (tls_tlv_data->length < ISC_PROXY2_TLS_SUBHEADER_MIN_SIZE) {
		return ISC_R_RANGE;
	}

	result = isc_proxy2_subtlv_tls_header_data(tls_tlv_data, &client_flags,
						   &client_cert_verified);

	if (result != ISC_R_SUCCESS) {
		return result;
	}

	p = tls_tlv_data->base;
	p += ISC_PROXY2_TLS_SUBHEADER_MIN_SIZE;

	if (cb != NULL) {
		isc_region_t data = {
			.base = p,
			.length = tls_tlv_data->length -
				  ISC_PROXY2_TLS_SUBHEADER_MIN_SIZE
		};
		tls_cbarg = (tls_cbarg_t){ .client = client_flags,
					   .client_cert_verified =
						   client_cert_verified,
					   .cb = cb,
					   .cbarg = cbarg };
		result = isc_proxy2_tlv_iterate(&data, proxy2_tls_iter_cb,
						&tls_cbarg);
	}

	return result;
}

typedef struct tls_subtlv_verify_cbarg {
	uint16_t *count;
	isc_result_t verif_result;
} tls_subtlv_verify_cbarg_t;

static bool
proxy2_subtlv_verify_iter_cb(const uint8_t client,
			     const bool client_cert_verified,
			     const isc_proxy2_tlv_subtype_tls_t tls_subtlv_type,
			     const isc_region_t *restrict data, void *cbarg) {
	bool verify_count = false;
	tls_subtlv_verify_cbarg_t *restrict arg =
		(tls_subtlv_verify_cbarg_t *)cbarg;
	uint8_t type = tls_subtlv_type;

	UNUSED(client);
	UNUSED(client_cert_verified);

	if (type <= ISC_PROXY2_TLV_TYPE_TLS ||
	    type == ISC_PROXY2_TLV_TYPE_NETNS)
	{
		arg->verif_result = ISC_R_UNEXPECTED;
		return false;
	}

	switch (tls_subtlv_type) {
	case ISC_PROXY2_TLV_SUBTYPE_TLS_VERSION:
	case ISC_PROXY2_TLV_SUBTYPE_TLS_CN:
	case ISC_PROXY2_TLV_SUBTYPE_TLS_SIG_ALG:
	case ISC_PROXY2_TLV_SUBTYPE_TLS_KEY_ALG:
		if (data->length == 0) {
			arg->verif_result = ISC_R_RANGE;
			return false;
		}
		arg->count[tls_subtlv_type]++;
		verify_count = true;
		break;
	default:
		break;
	};

	if (verify_count && arg->count[tls_subtlv_type] > 1) {
		arg->verif_result = ISC_R_UNEXPECTED;
		return false;
	}

	return true;
}

typedef struct tlv_verify_cbarg {
	uint16_t count[256];
	isc_result_t verify_result;
} tlv_verify_cbarg_t;

static bool
isc_proxy2_tlv_verify_cb(const isc_proxy2_tlv_type_t tlv_type,
			 const isc_region_t *restrict data, void *cbarg) {
	bool verify_count = false;
	uint8_t client = 0;
	tlv_verify_cbarg_t *arg = (tlv_verify_cbarg_t *)cbarg;

	if (tlv_type == 0) {
		/* the TLV values start from 1 */
		goto error_unexpected;
	}

	switch (tlv_type) {
	case ISC_PROXY2_TLV_TYPE_ALPN:
	case ISC_PROXY2_TLV_TYPE_AUTHORITY:
	case ISC_PROXY2_TLV_TYPE_NETNS:
		/* these values need to be more than 0 bytes long */
		if (data->length == 0) {
			goto error_range;
		}
		arg->count[tlv_type]++;
		verify_count = true;
		break;
	case ISC_PROXY2_TLV_TYPE_CRC32C:
		if (data->length != sizeof(uint32_t)) {
			goto error_range;
		}
		arg->count[tlv_type]++;
		verify_count = true;
		break;
	case ISC_PROXY2_TLV_TYPE_UNIQUE_ID:
		if (data->length > 128) {
			goto error_range;
		}
		arg->count[tlv_type]++;
		verify_count = true;
		break;
	case ISC_PROXY2_TLV_TYPE_TLS: {
		tls_subtlv_verify_cbarg_t tls_cbarg = {
			.verif_result = ISC_R_SUCCESS, .count = arg->count
		};
		size_t tls_version_count, tls_cn_count;

		arg->verify_result =
			isc_proxy2_subtlv_tls_header_data(data, &client, NULL);

		if (arg->verify_result != ISC_R_SUCCESS) {
			return false;
		}

		arg->verify_result = isc_proxy2_subtlv_tls_iterate(
			data, proxy2_subtlv_verify_iter_cb, &tls_cbarg);

		if (arg->verify_result != ISC_R_SUCCESS) {
			return false;
		} else if (tls_cbarg.verif_result != ISC_R_SUCCESS) {
			arg->verify_result = tls_cbarg.verif_result;
			return false;
		}

		/*
		 * if CLIENT_TLS flag is set - TLS version TLV must be present
		 */
		tls_version_count =
			arg->count[ISC_PROXY2_TLV_SUBTYPE_TLS_VERSION];

		if ((client & ISC_PROXY2_CLIENT_TLS) != 0) {
			if (tls_version_count != 1) {
				goto error_unexpected;
			}
		} else if (tls_version_count > 0) {
			/* unexpected TLS version TLV */
			goto error_unexpected;
		}

		/*
		 * If client cert was submitted, CLIENT_CERT_CONN or
		 * CLIENT_CERT_SESS flags must be present alongside the
		 * CLIENT_TLS flag.
		 */
		tls_cn_count = arg->count[ISC_PROXY2_TLV_SUBTYPE_TLS_CN];

		if ((client & (ISC_PROXY2_CLIENT_CERT_CONN |
			       ISC_PROXY2_CLIENT_CERT_SESS)) != 0)
		{
			if (tls_cn_count != 1 ||
			    (client & ISC_PROXY2_CLIENT_TLS) == 0)
			{
				goto error_unexpected;
			}
		} else if (tls_cn_count > 0) {
			/* unexpected Common Name TLV */
			goto error_unexpected;
		}

		arg->count[tlv_type]++;
		verify_count = true;
	} break;
	default:
		break;
	};

	if (verify_count && arg->count[tlv_type] > 1) {
		goto error_unexpected;
	}

	return true;

error_unexpected:
	arg->verify_result = ISC_R_UNEXPECTED;
	return false;

error_range:
	arg->verify_result = ISC_R_RANGE;
	return false;
}

isc_result_t
isc_proxy2_tlv_data_verify(const isc_region_t *restrict tlv_data) {
	isc_result_t result;
	tlv_verify_cbarg_t cbarg = { .verify_result = ISC_R_SUCCESS };

	result = isc_proxy2_tlv_iterate(tlv_data, isc_proxy2_tlv_verify_cb,
					&cbarg);
	if (result != ISC_R_SUCCESS) {
		return result;
	}

	return cbarg.verify_result;
}

isc_result_t
isc_proxy2_header_handle_directly(const isc_region_t *restrict header_data,
				  const isc_proxy2_handler_cb_t cb,
				  void *cbarg) {
	isc_result_t result;
	isc_proxy2_handler_t handler = { 0 };

	REQUIRE(header_data != NULL);
	REQUIRE(cb != NULL);

	isc__proxy2_handler_init_direct(&handler, 0, header_data, cb, cbarg);

	result = isc__proxy2_handler_process_data(&handler);

	return result;
}

isc_result_t
isc_proxy2_make_header(isc_buffer_t *restrict outbuf,
		       const isc_proxy2_command_t cmd, const int socktype,
		       const isc_sockaddr_t *restrict src_addr,
		       const isc_sockaddr_t *restrict dst_addr,
		       const isc_region_t *restrict tlv_data) {
	size_t total_size = ISC_PROXY2_HEADER_SIZE;
	uint8_t family = ISC_PROXY2_AF_UNSPEC;
	isc_proxy2_socktype_t proxy_socktype = ISC_PROXY2_SOCK_UNSPEC;

	uint8_t ver_cmd = 0;
	uint8_t fam_socktype = 0;
	uint16_t len = 0;

	size_t addr_size = 0;
	void *psrc_addr = NULL, *pdst_addr = NULL;
	/*
	 * The complete PROXYv2 header can be described as follows:
	 *
	 * 1. Header:
	 *
	 * struct proxy_hdr_v2 {
	 *   uint8_t sig[12];      // hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A
	 *   uint8_t ver_cmd;      // protocol version and command
	 *   uint8_t fam_socktype; // protocol family and socket type
	 *   uint16_t len;         // number of following bytes
	 * };
	 *
	 * 2. Addresses:
	 *
	 * union proxy_addr {
	 *   struct {        // for TCP/UDP over IPv4, len = 12
	 *       uint32_t src_addr;
	 *       uint32_t dst_addr;
	 *       uint16_t src_port;
	 *       uint16_t dst_port;
	 *   } ipv4_addr;
	 *   struct {        // for TCP/UDP over IPv6, len = 36
	 *        uint8_t  src_addr[16];
	 *        uint8_t  dst_addr[16];
	 *        uint16_t src_port;
	 *        uint16_t dst_port;
	 *   } ipv6_addr;
	 *   struct {        // for AF_UNIX sockets, len = 216
	 *        uint8_t src_addr[108];
	 *        uint8_t dst_addr[108];
	 *   } unix_addr;
	 * };
	 *
	 * 3. TLVs (optional)
	 */

	REQUIRE(outbuf != NULL);
	REQUIRE(cmd == ISC_PROXY2_CMD_PROXY || socktype == 0);
	REQUIRE((src_addr == NULL && dst_addr == NULL) ||
		(src_addr != NULL && dst_addr != NULL));
	REQUIRE(src_addr == NULL ||
		(isc_sockaddr_pf(src_addr) == isc_sockaddr_pf(dst_addr)));

	switch (cmd) {
	case ISC_PROXY2_CMD_LOCAL:
		family = ISC_PROXY2_AF_UNSPEC;
		break;
	case ISC_PROXY2_CMD_PROXY:
		if (socktype == 0) {
			family = ISC_PROXY2_AF_UNSPEC;
		} else {
			switch (isc_sockaddr_pf(src_addr)) {
			case AF_INET:
				family = ISC_PROXY2_AF_INET;
				addr_size = sizeof(src_addr->type.sin.sin_addr);
				total_size += addr_size * 2 +
					      sizeof(uint16_t) * 2;
				psrc_addr = (void *)&src_addr->type.sin.sin_addr
						    .s_addr;
				pdst_addr = (void *)&dst_addr->type.sin.sin_addr
						    .s_addr;
				break;
			case AF_INET6:
				family = ISC_PROXY2_AF_INET6;
				addr_size =
					sizeof(src_addr->type.sin6.sin6_addr);
				total_size += addr_size * 2 +
					      sizeof(uint16_t) * 2;
				psrc_addr =
					(void *)&src_addr->type.sin6.sin6_addr;
				pdst_addr =
					(void *)&dst_addr->type.sin6.sin6_addr;
				break;
			default:
				return ISC_R_UNEXPECTED;
			}
		}
		break;
	default:
		return ISC_R_UNEXPECTED;
	}

	switch (socktype) {
	case 0:
		proxy_socktype = ISC_PROXY2_SOCK_UNSPEC;
		break;
	case SOCK_STREAM:
		proxy_socktype = ISC_PROXY2_SOCK_STREAM;
		break;
	case SOCK_DGRAM:
		proxy_socktype = ISC_PROXY2_SOCK_DGRAM;
		break;
	default:
		return ISC_R_UNEXPECTED;
	}

	if (tlv_data != NULL) {
		if (tlv_data->length > UINT16_MAX) {
			return ISC_R_RANGE;
		}
		total_size += tlv_data->length;
	}

	if (isc_buffer_availablelength(outbuf) < total_size) {
		return ISC_R_NOSPACE;
	} else if (total_size > UINT16_MAX) {
		return ISC_R_RANGE;
	}

	/*
	 * Combine version 2 (highest four bits) and command (lowest four
	 * bits).
	 */
	ver_cmd = (((2 << 4) & 0xF0U) | cmd);

	/*
	 * Combine address family (highest four bits) and socket type
	 * (lowest four bits).
	 */
	fam_socktype = (((family << 4) & 0xF0U) | proxy_socktype);

	len = (uint16_t)(total_size - ISC_PROXY2_HEADER_SIZE);

	/* Write signature */
	isc_buffer_putmem(outbuf, (uint8_t *)ISC_PROXY2_HEADER_SIGNATURE,
			  ISC_PROXY2_HEADER_SIGNATURE_SIZE);
	/* Write version and command */
	isc_buffer_putuint8(outbuf, ver_cmd);
	/* Write address family and socket type */
	isc_buffer_putuint8(outbuf, fam_socktype);
	/* Write header payload size (addresses + TLVs) */
	isc_buffer_putuint16(outbuf, len);

	/* Write source and destination addresses (if we should) */
	if (psrc_addr != NULL) {
		isc_buffer_putmem(outbuf, psrc_addr, addr_size);
	}

	if (pdst_addr != NULL) {
		isc_buffer_putmem(outbuf, pdst_addr, addr_size);
	}

	/* Write source and destination ports (if we should) */
	if (family == ISC_PROXY2_AF_INET || family == ISC_PROXY2_AF_INET6) {
		isc_buffer_putuint16(outbuf, isc_sockaddr_getport(src_addr));
		isc_buffer_putuint16(outbuf, isc_sockaddr_getport(dst_addr));
	}

	if (tlv_data != NULL) {
		isc_buffer_putmem(outbuf, tlv_data->base, tlv_data->length);
	}

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_header_append(isc_buffer_t *restrict outbuf,
			 const isc_region_t *restrict data) {
	const size_t len_offset = ISC_PROXY2_HEADER_SIZE - sizeof(uint16_t);
	isc_region_t header_data = { 0 };
	uint16_t new_len = 0;

	REQUIRE(outbuf != NULL);

	isc_buffer_usedregion(outbuf, &header_data);

	REQUIRE(header_data.length >= ISC_PROXY2_HEADER_SIZE);
	REQUIRE(data != NULL);

	if (isc_buffer_availablelength(outbuf) < data->length) {
		return ISC_R_NOSPACE;
	} else if ((data->length + header_data.length) > UINT16_MAX) {
		return ISC_R_RANGE;
	}

	INSIST(memcmp(header_data.base, ISC_PROXY2_HEADER_SIGNATURE,
		      ISC_PROXY2_HEADER_SIGNATURE_SIZE) == 0);

	/* fixup length of the header payload */
	/* load */
	memmove(&new_len, &header_data.base[len_offset], sizeof(new_len));
	new_len = ntohs(new_len);
	/* check */
	if ((data->length + new_len) > UINT16_MAX) {
		return ISC_R_RANGE;
	}
	/* update */
	new_len += (uint16_t)data->length;
	/* store */
	new_len = htons(new_len);
	memmove(&header_data.base[len_offset], &new_len, sizeof(new_len));

	isc_buffer_putmem(outbuf, data->base, data->length);

	return ISC_R_SUCCESS;
}

static inline void
append_type_and_length(isc_buffer_t *restrict outbuf, const uint8_t type,
		       const uint16_t tlv_length, const bool update_header) {
	uint16_t length;
	isc_region_t type_region = { 0 }, length_region = { 0 };

	type_region = (isc_region_t){ .base = (uint8_t *)&type,
				      .length = sizeof(type) };
	length = htons(tlv_length);
	length_region = (isc_region_t){ .base = (uint8_t *)&length,
					.length = sizeof(length) };

	if (update_header) {
		isc_result_t result = isc_proxy2_header_append(outbuf,
							       &type_region);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
		result = isc_proxy2_header_append(outbuf, &length_region);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
	} else {
		isc_buffer_putmem(outbuf, type_region.base, type_region.length);
		isc_buffer_putmem(outbuf, length_region.base,
				  length_region.length);
	}
}

isc_result_t
isc_proxy2_header_append_tlv(isc_buffer_t *restrict outbuf,
			     const isc_proxy2_tlv_type_t tlv_type,
			     const isc_region_t *restrict tlv_data) {
	size_t new_data_len = 0;
	REQUIRE(outbuf != NULL);
	REQUIRE(tlv_data != NULL);

	/*
	 * TLV header can be described as follows:
	 *
	 *   struct {
	 *       uint8_t type;
	 *       uint8_t length_hi;
	 *       uint8_t length_lo;
	 *   };
	 *
	 */
	new_data_len = tlv_data->length + 3;

	if (isc_buffer_availablelength(outbuf) < (new_data_len)) {
		return ISC_R_NOSPACE;
	} else if ((isc_buffer_usedlength(outbuf) + new_data_len) > UINT16_MAX)
	{
		return ISC_R_RANGE;
	}

	append_type_and_length(outbuf, (uint8_t)tlv_type,
			       ((uint16_t)tlv_data->length), true);

	if (tlv_data->length > 0) {
		isc_result_t result = isc_proxy2_header_append(outbuf,
							       tlv_data);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
	}

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_header_append_tlv_string(isc_buffer_t *restrict outbuf,
				    const isc_proxy2_tlv_type_t tlv_type,
				    const char *restrict str) {
	isc_result_t result;
	isc_region_t region = { 0 };

	REQUIRE(str != NULL && *str != '\0');

	region.base = (uint8_t *)str;
	region.length = strlen(str);

	result = isc_proxy2_header_append_tlv(outbuf, tlv_type, &region);

	return result;
}

isc_result_t
isc_proxy2_make_tls_subheader(isc_buffer_t *restrict outbuf,
			      const uint8_t client_flags,
			      const bool client_cert_verified,
			      const isc_region_t *restrict tls_subtlvs_data) {
	size_t total_size = ISC_PROXY2_TLS_SUBHEADER_MIN_SIZE;
	uint32_t client_cert_not_verified = 1;
	REQUIRE(outbuf != NULL);

	if (tls_subtlvs_data != NULL) {
		total_size += tls_subtlvs_data->length;
	}

	if (isc_buffer_availablelength(outbuf) < total_size) {
		return ISC_R_NOSPACE;
	} else if (total_size > UINT16_MAX) {
		return ISC_R_RANGE;
	}

	isc_buffer_putuint8(outbuf, client_flags);
	client_cert_not_verified = htonl(!client_cert_verified);
	isc_buffer_putmem(outbuf, (uint8_t *)&client_cert_not_verified,
			  sizeof(client_cert_not_verified));

	if (tls_subtlvs_data != NULL) {
		isc_buffer_putmem(outbuf, tls_subtlvs_data->base,
				  tls_subtlvs_data->length);
	}

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_append_tlv(isc_buffer_t *restrict outbuf, const uint8_t type,
		      const isc_region_t *restrict data) {
	size_t new_data_len = 0;
	REQUIRE(outbuf != NULL);
	REQUIRE(data != NULL);

	new_data_len = (data->length + 3);

	if (isc_buffer_availablelength(outbuf) < new_data_len) {
		return ISC_R_NOSPACE;
	} else if ((isc_buffer_usedlength(outbuf) + (data->length + 3)) >
		   UINT16_MAX)
	{
		return ISC_R_RANGE;
	}

	append_type_and_length(outbuf, (uint8_t)type, ((uint16_t)data->length),
			       false);

	if (data->length > 0) {
		isc_buffer_putmem(outbuf, data->base, data->length);
	}

	return ISC_R_SUCCESS;
}

isc_result_t
isc_proxy2_append_tlv_string(isc_buffer_t *restrict outbuf, const uint8_t type,
			     const char *restrict str) {
	isc_result_t result;
	isc_region_t region = { 0 };

	REQUIRE(str != NULL && *str != '\0');

	region.base = (uint8_t *)str;
	region.length = strlen(str);

	result = isc_proxy2_append_tlv(outbuf, type, &region);

	return result;
}
