/*
 * 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/netmgr.h>

#include "netmgr-int.h"

/*
 * The idea behind the transport is simple after accepting the
 * connection or connecting to a remote server it enters PROXYv2
 * handling mode: that is, it either attempts to read (when accepting
 * the connection) or send (when establishing a connection) a PROXYv2
 * header. After that it works like a mere wrapper on top of the
 * underlying stream-based transport (TCP).
 */

typedef struct proxystream_send_req {
	isc_nm_cb_t cb;		     /* send callback */
	void *cbarg;		     /* send callback argument */
	isc_nmhandle_t *proxyhandle; /* PROXY Stream socket handle */
} proxystream_send_req_t;

static void
proxystream_on_header_data_cb(const isc_result_t result,
			      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_blob,
			      const isc_region_t *restrict extra, void *cbarg);

static isc_nmsocket_t *
proxystream_sock_new(isc__networker_t *worker, const isc_nmsocket_type_t type,
		     isc_sockaddr_t *addr, const bool is_server);

static isc_result_t
proxystream_accept_cb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg);

static void
proxystream_connect_cb(isc_nmhandle_t *handle, isc_result_t result,
		       void *cbarg);

static void
proxystream_failed_read_cb_async(void *arg);

static void
proxystream_clear_proxy_header_data(isc_nmsocket_t *sock);

static void
proxystream_read_start(isc_nmsocket_t *sock);

static void
proxystream_read_stop(isc_nmsocket_t *sock);

static void
proxystream_try_close_unused(isc_nmsocket_t *sock);

static void
proxystream_call_connect_cb(isc_nmsocket_t *sock, isc_nmhandle_t *handle,
			    isc_result_t result);

static bool
proxystream_closing(isc_nmsocket_t *sock);

static void
proxystream_failed_read_cb(isc_nmsocket_t *sock, const isc_result_t result);

static void
proxystream_read_cb(isc_nmhandle_t *handle, isc_result_t result,
		    isc_region_t *region, void *cbarg);

static void
proxystream_read_extra_cb(void *arg);

static proxystream_send_req_t *
proxystream_get_send_req(isc_mem_t *mctx, isc_nmsocket_t *sock,
			 isc_nmhandle_t *proxyhandle, isc_nm_cb_t cb,
			 void *cbarg);

static void
proxystream_put_send_req(isc_mem_t *mctx, proxystream_send_req_t *send_req,
			 const bool force_destroy);

static void
proxystream_send_cb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg);

static void
proxystream_send(isc_nmhandle_t *handle, isc_region_t *region, isc_nm_cb_t cb,
		 void *cbarg, const bool dnsmsg);

static void
proxystream_on_header_data_cb(const isc_result_t result,
			      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 tlvs,
			      const isc_region_t *restrict extra, void *cbarg) {
	isc_nmsocket_t *sock = (isc_nmsocket_t *)cbarg;

	switch (result) {
	case ISC_R_SUCCESS: {
		isc_nmhandle_t *proxyhandle = NULL;
		isc_result_t accept_result = ISC_R_FAILURE;
		bool call_accept = false;
		bool is_unspec = false;

		/*
		 * After header has been processed - stop reading (thus,
		 * stopping the timer) and disable manual timer control as in
		 * the case of TCP it is disabled by default
		 */
		proxystream_read_stop(sock);
		isc__nmsocket_timer_stop(sock);
		isc__nmhandle_set_manual_timer(sock->outerhandle, false);

		sock->proxy.header_processed = true;
		if (extra == NULL) {
			sock->proxy.extra_processed = true;
		}

		/* Process header data */
		if (cmd == ISC_PROXY2_CMD_LOCAL) {
			is_unspec = true;
			call_accept = true;
		} else if (cmd == ISC_PROXY2_CMD_PROXY) {
			switch (socktype) {
			case 0:
				/*
				 * Treat unsupported addresses (aka AF_UNSPEC)
				 * as LOCAL.
				 */
				is_unspec = true;
				call_accept = true;
				break;
			case SOCK_DGRAM:
				/*
				 * In some cases proxies can do protocol
				 * conversion. In this case, the original
				 * request might have arrived over UDP-based
				 * transport and, thus, the PROXYv2 header can
				 * contain SOCK_DGRAM, while for TCP one would
				 * expect SOCK_STREAM. That might be unexpected,
				 * but, as the main idea behind PROXYv2 is to
				 * carry the original endpoint information to
				 * back-ends, that is fine.
				 *
				 * At least "dnsdist" does that when redirecting
				 * a UDP request to a TCP or TLS-only server.
				 */
			case SOCK_STREAM:
				INSIST(isc_sockaddr_pf(src_addr) ==
				       isc_sockaddr_pf(dst_addr));
				/* We will treat AF_UNIX as unspec */
				if (isc_sockaddr_pf(src_addr) == AF_UNIX) {
					is_unspec = true;
				}

				if (!is_unspec &&
				    !isc__nm_valid_proxy_addresses(src_addr,
								   dst_addr))
				{
					break;
				}

				call_accept = true;
				break;
			default:
				break;
			}
		}

		if (call_accept) {
			if (is_unspec) {
				proxyhandle = isc__nmhandle_get(
					sock, &sock->peer, &sock->iface);
			} else {
				INSIST(src_addr != NULL);
				INSIST(dst_addr != NULL);
				proxyhandle = isc__nmhandle_get(sock, src_addr,
								dst_addr);
			}
			proxyhandle->proxy_is_unspec = is_unspec;
			isc__nm_received_proxy_header_log(proxyhandle, cmd,
							  socktype, src_addr,
							  dst_addr, tlvs);
			accept_result = sock->accept_cb(proxyhandle, result,
							sock->accept_cbarg);
			isc_nmhandle_detach(&proxyhandle);
		}

		if (accept_result != ISC_R_SUCCESS) {
			isc__nmsocket_detach(&sock->listener);
			isc_nmhandle_detach(&sock->outerhandle);
			sock->closed = true;
		}

		sock->accepting = false;

		proxystream_try_close_unused(sock);
	} break;
	case ISC_R_NOMORE:
		/*
		 * That is fine, wait for more data to complete the PROXY
		 * header
		 */
		break;
	default:
		proxystream_failed_read_cb(sock, result);
		break;
	};
}

static void
proxystream_handle_incoming_header_data(isc_nmsocket_t *sock,
					isc_region_t *restrict data) {
	isc_proxy2_handler_t *restrict handler = sock->proxy.proxy2.handler;

	(void)isc_proxy2_handler_push(handler, data);
	proxystream_try_close_unused(sock);
}

static isc_nmsocket_t *
proxystream_sock_new(isc__networker_t *worker, const isc_nmsocket_type_t type,
		     isc_sockaddr_t *addr, const bool is_server) {
	isc_nmsocket_t *sock;
	INSIST(type == isc_nm_proxystreamsocket ||
	       type == isc_nm_proxystreamlistener);

	sock = isc_mempool_get(worker->nmsocket_pool);
	isc__nmsocket_init(sock, worker, type, addr, NULL);
	sock->result = ISC_R_UNSET;
	if (type == isc_nm_proxystreamsocket) {
		sock->read_timeout = isc_nm_getinitialtimeout();
		sock->client = !is_server;
		sock->connecting = !is_server;
		if (is_server) {
			/*
			 * Smallest TCP (over IPv6) segment size we required to
			 * support. An adequate value for both IPv4 and IPv6.
			 */
			sock->proxy.proxy2.handler = isc_proxy2_handler_new(
				worker->mctx, NM_MAXSEG,
				proxystream_on_header_data_cb, sock);
		} else {
			isc_buffer_allocate(worker->mctx,
					    &sock->proxy.proxy2.outbuf,
					    ISC_NM_PROXY2_DEFAULT_BUFFER_SIZE);
		}
	}

	return sock;
}

static isc_result_t
proxystream_accept_cb(isc_nmhandle_t *handle, isc_result_t result,
		      void *cbarg) {
	isc_nmsocket_t *listensock = (isc_nmsocket_t *)cbarg;
	isc_nmsocket_t *nsock = NULL;
	isc_sockaddr_t iface;

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

	INSIST(VALID_NMHANDLE(handle));
	INSIST(VALID_NMSOCK(handle->sock));
	INSIST(VALID_NMSOCK(listensock));
	INSIST(listensock->type == isc_nm_proxystreamlistener);

	if (isc__nm_closing(handle->sock->worker)) {
		return ISC_R_SHUTTINGDOWN;
	} else if (isc__nmsocket_closing(handle->sock)) {
		return ISC_R_CANCELED;
	}

	iface = isc_nmhandle_localaddr(handle);
	nsock = proxystream_sock_new(handle->sock->worker,
				     isc_nm_proxystreamsocket, &iface, true);
	INSIST(listensock->accept_cb != NULL);
	nsock->accept_cb = listensock->accept_cb;
	nsock->accept_cbarg = listensock->accept_cbarg;

	nsock->peer = isc_nmhandle_peeraddr(handle);
	nsock->tid = isc_tid();
	nsock->accepting = true;
	nsock->active = true;

	isc__nmsocket_attach(listensock, &nsock->listener);
	isc_nmhandle_attach(handle, &nsock->outerhandle);
	handle->sock->proxy.sock = nsock;

	/*
	 * We need to control the timer manually as we do *not* want it to
	 * be reset on partial header data reads.
	 */
	isc__nmhandle_set_manual_timer(nsock->outerhandle, true);
	isc__nmsocket_timer_restart(nsock);

	proxystream_read_start(nsock);

	return ISC_R_SUCCESS;
}

isc_result_t
isc_nm_listenproxystream(uint32_t workers, isc_sockaddr_t *iface,
			 isc_nm_accept_cb_t accept_cb, void *accept_cbarg,
			 int backlog, isc_quota_t *quota, isc_tlsctx_t *tlsctx,
			 isc_nmsocket_t **sockp) {
	isc_result_t result;
	isc_nmsocket_t *listener = NULL;
	isc__networker_t *worker = isc__networker_current();

	REQUIRE(isc_tid() == 0);
	REQUIRE(sockp != NULL && *sockp == NULL);

	if (isc__nm_closing(worker)) {
		return ISC_R_SHUTTINGDOWN;
	}

	listener = proxystream_sock_new(worker, isc_nm_proxystreamlistener,
					iface, true);
	listener->accept_cb = accept_cb;
	listener->accept_cbarg = accept_cbarg;

	if (tlsctx == NULL) {
		result = isc_nm_listentcp(workers, iface, proxystream_accept_cb,
					  listener, backlog, quota,
					  &listener->outer);
	} else {
		result = isc_nm_listentls(workers, iface, proxystream_accept_cb,
					  listener, backlog, quota, tlsctx,
					  false, &listener->outer);
	}

	if (result != ISC_R_SUCCESS) {
		listener->closed = true;
		isc__nmsocket_detach(&listener);
		return result;
	}

	listener->active = true;
	listener->result = result;
	listener->nchildren = listener->outer->nchildren;

	*sockp = listener;

	return result;
}

static void
proxystream_try_close_unused(isc_nmsocket_t *sock) {
	/* try to close unused socket */
	if (sock->statichandle == NULL && sock->proxy.nsending == 0) {
		isc__nmsocket_prep_destroy(sock);
	}
}

static void
proxystream_call_connect_cb(isc_nmsocket_t *sock, isc_nmhandle_t *handle,
			    isc_result_t result) {
	sock->connecting = false;
	if (sock->connect_cb == NULL) {
		return;
	}

	if (result == ISC_R_SUCCESS) {
		sock->connected = true;
	}

	sock->connect_cb(handle, result, sock->connect_cbarg);
	if (result != ISC_R_SUCCESS) {
		isc__nmsocket_clearcb(handle->sock);
	}
}

static void
proxystream_send_header_cb(isc_nmhandle_t *transphandle, isc_result_t result,
			   void *cbarg) {
	isc_nmsocket_t *sock = (isc_nmsocket_t *)cbarg;
	isc_nmhandle_t *proxyhandle = NULL;

	REQUIRE(VALID_NMHANDLE(transphandle));
	REQUIRE(VALID_NMSOCK(sock));

	sock->proxy.nsending--;
	sock->proxy.header_processed = true;

	if (isc__nm_closing(transphandle->sock->worker)) {
		result = ISC_R_SHUTTINGDOWN;
	}

	proxyhandle = isc__nmhandle_get(sock, &sock->peer, &sock->iface);
	proxystream_call_connect_cb(sock, proxyhandle, result);
	isc_nmhandle_detach(&proxyhandle);

	proxystream_try_close_unused(sock);
}

static void
proxystream_connect_cb(isc_nmhandle_t *handle, isc_result_t result,
		       void *cbarg) {
	isc_nmsocket_t *sock = (isc_nmsocket_t *)cbarg;
	isc_nmhandle_t *proxyhandle = NULL;
	isc_region_t header = { 0 };

	REQUIRE(VALID_NMSOCK(sock));

	sock->tid = isc_tid();

	if (result != ISC_R_SUCCESS) {
		goto error;
	}

	INSIST(VALID_NMHANDLE(handle));

	sock->iface = isc_nmhandle_localaddr(handle);
	sock->peer = isc_nmhandle_peeraddr(handle);
	if (isc__nm_closing(handle->sock->worker)) {
		result = ISC_R_SHUTTINGDOWN;
		goto error;
	} else if (isc__nmsocket_closing(handle->sock)) {
		result = ISC_R_CANCELED;
		goto error;
	}

	isc_nmhandle_attach(handle, &sock->outerhandle);
	handle->sock->proxy.sock = sock;
	sock->active = true;

	isc_buffer_usedregion(sock->proxy.proxy2.outbuf, &header);
	sock->proxy.nsending++;
	isc_nm_send(handle, &header, proxystream_send_header_cb, sock);

	proxystream_try_close_unused(sock);

	return;
error:
	proxyhandle = isc__nmhandle_get(sock, NULL, NULL);
	sock->closed = true;
	proxystream_call_connect_cb(sock, proxyhandle, result);
	isc_nmhandle_detach(&proxyhandle);
	isc__nmsocket_detach(&sock);
}

void
isc_nm_proxystreamconnect(isc_sockaddr_t *local, isc_sockaddr_t *peer,
			  isc_nm_cb_t cb, void *cbarg, unsigned int timeout,
			  isc_tlsctx_t *tlsctx, const char *sni_hostname,
			  isc_tlsctx_client_session_cache_t *client_sess_cache,
			  isc_nm_proxyheader_info_t *proxy_info) {
	isc_result_t result = ISC_R_FAILURE;
	isc_nmsocket_t *nsock = NULL;
	isc__networker_t *worker = isc__networker_current();

	if (isc__nm_closing(worker)) {
		cb(NULL, ISC_R_SHUTTINGDOWN, cbarg);
		return;
	}

	nsock = proxystream_sock_new(worker, isc_nm_proxystreamsocket, local,
				     false);
	nsock->connect_cb = cb;
	nsock->connect_cbarg = cbarg;
	nsock->connect_timeout = timeout;

	if (proxy_info == NULL) {
		result = isc_proxy2_make_header(nsock->proxy.proxy2.outbuf,
						ISC_PROXY2_CMD_LOCAL, 0, NULL,
						NULL, NULL);
	} else if (proxy_info->complete) {
		isc_buffer_putmem(nsock->proxy.proxy2.outbuf,
				  proxy_info->complete_header.base,
				  proxy_info->complete_header.length);
		result = ISC_R_SUCCESS;
	} else if (!proxy_info->complete) {
		result = isc_proxy2_make_header(
			nsock->proxy.proxy2.outbuf, ISC_PROXY2_CMD_PROXY,
			SOCK_STREAM, &proxy_info->proxy_info.src_addr,
			&proxy_info->proxy_info.dst_addr,
			&proxy_info->proxy_info.tlv_data);
	}
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	if (tlsctx == NULL) {
		isc_nm_tcpconnect(local, peer, proxystream_connect_cb, nsock,
				  nsock->connect_timeout);
	} else {
		isc_nm_tlsconnect(local, peer, proxystream_connect_cb, nsock,
				  tlsctx, sni_hostname, client_sess_cache,
				  nsock->connect_timeout, false, NULL);
	}
}

static void
proxystream_failed_read_cb_async(void *arg) {
	isc__nm_uvreq_t *req = (isc__nm_uvreq_t *)arg;

	proxystream_failed_read_cb(req->sock, req->result);
	isc__nm_uvreq_put(&req);
}

void
isc__nm_proxystream_failed_read_cb(isc_nmsocket_t *sock, isc_result_t result,
				   bool async) {
	proxystream_read_stop(sock);

	if (!async) {
		proxystream_failed_read_cb(sock, result);
	} else {
		isc__nm_uvreq_t *req = isc__nm_uvreq_get(sock);
		req->result = result;
		req->cbarg = sock;
		isc_job_run(sock->worker->loop, &req->job,
			    proxystream_failed_read_cb_async, req);
	}
}

void
isc__nm_proxystream_stoplistening(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamlistener);
	REQUIRE(sock->proxy.sock == NULL);

	isc__nmsocket_stop(sock);
}

static void
proxystream_clear_proxy_header_data(isc_nmsocket_t *sock) {
	if (!sock->client && sock->proxy.proxy2.handler != NULL) {
		isc_proxy2_handler_free(&sock->proxy.proxy2.handler);
	} else if (sock->client && sock->proxy.proxy2.outbuf != NULL) {
		isc_buffer_free(&sock->proxy.proxy2.outbuf);
	}
}

void
isc__nm_proxystream_cleanup_data(isc_nmsocket_t *sock) {
	switch (sock->type) {
	case isc_nm_tcpsocket:
	case isc_nm_tlssocket:
		if (sock->proxy.sock != NULL) {
			isc__nmsocket_detach(&sock->proxy.sock);
		}
		break;
	case isc_nm_proxystreamsocket:
		if (sock->proxy.send_req != NULL) {
			proxystream_put_send_req(
				sock->worker->mctx,
				(proxystream_send_req_t *)sock->proxy.send_req,
				true);
		}

		proxystream_clear_proxy_header_data(sock);
		break;
	default:
		break;
	};
}

void
isc__nmhandle_proxystream_cleartimeout(isc_nmhandle_t *handle) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		isc_nmhandle_cleartimeout(sock->outerhandle);
	}
}

void
isc__nmhandle_proxystream_settimeout(isc_nmhandle_t *handle, uint32_t timeout) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		isc_nmhandle_settimeout(sock->outerhandle, timeout);
	}
}

void
isc__nmhandle_proxystream_keepalive(isc_nmhandle_t *handle, bool value) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		isc_nmhandle_keepalive(sock->outerhandle, value);
	}
}

void
isc__nmhandle_proxystream_setwritetimeout(isc_nmhandle_t *handle,
					  uint64_t write_timeout) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		isc_nmhandle_setwritetimeout(sock->outerhandle, write_timeout);
	}
}

void
isc__nmsocket_proxystream_reset(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);

	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		REQUIRE(VALID_NMSOCK(sock->outerhandle->sock));
		isc__nmsocket_reset(sock->outerhandle->sock);
	}
}

bool
isc__nmsocket_proxystream_timer_running(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);

	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		REQUIRE(VALID_NMSOCK(sock->outerhandle->sock));
		return isc__nmsocket_timer_running(sock->outerhandle->sock);
	}

	return false;
}

void
isc__nmsocket_proxystream_timer_restart(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);

	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		REQUIRE(VALID_NMSOCK(sock->outerhandle->sock));
		isc__nmsocket_timer_restart(sock->outerhandle->sock);
	}
}

void
isc__nmsocket_proxystream_timer_stop(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);

	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		REQUIRE(VALID_NMSOCK(sock->outerhandle->sock));
		isc__nmsocket_timer_stop(sock->outerhandle->sock);
	}
}

void
isc__nmhandle_proxystream_set_manual_timer(isc_nmhandle_t *handle,
					   const bool manual) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		isc__nmhandle_set_manual_timer(sock->outerhandle, manual);
	}
}

isc_result_t
isc__nmhandle_proxystream_set_tcp_nodelay(isc_nmhandle_t *handle,
					  const bool value) {
	isc_nmsocket_t *sock = NULL;
	isc_result_t result = ISC_R_FAILURE;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		result = isc_nmhandle_set_tcp_nodelay(sock->outerhandle, value);
	}

	return result;
}

static void
proxystream_read_start(isc_nmsocket_t *sock) {
	if (sock->proxy.reading == true) {
		return;
	}

	sock->proxy.reading = true;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		isc_nm_read(sock->outerhandle, proxystream_read_cb, sock);
	}
}

static void
proxystream_read_stop(isc_nmsocket_t *sock) {
	if (sock->proxy.reading == false) {
		return;
	}

	sock->proxy.reading = false;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));

		isc_nm_read_stop(sock->outerhandle);
	}
}

void
isc__nm_proxystream_read_stop(isc_nmhandle_t *handle) {
	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	handle->sock->reading = false;
	proxystream_read_stop(handle->sock);
}

void
isc__nm_proxystream_close(isc_nmsocket_t *sock) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);
	REQUIRE(sock->tid == isc_tid());

	sock->closing = true;

	/*
	 * At this point we're certain that there are no
	 * external references, we can close everything.
	 */
	proxystream_read_stop(sock);
	isc__nmsocket_timer_stop(sock);
	if (sock->outerhandle != NULL) {
		sock->reading = false;
		isc_nm_read_stop(sock->outerhandle);
		isc_nmhandle_close(sock->outerhandle);
		isc_nmhandle_detach(&sock->outerhandle);
	}

	if (sock->listener != NULL) {
		isc__nmsocket_detach(&sock->listener);
	}

	/* Further cleanup performed in isc__nm_proxystream_cleanup_data() */
	sock->closed = true;
	sock->active = false;
}

static bool
proxystream_closing(isc_nmsocket_t *sock) {
	return isc__nmsocket_closing(sock) || sock->outerhandle == NULL ||
	       (sock->outerhandle != NULL &&
		isc__nmsocket_closing(sock->outerhandle->sock));
}

static void
proxystream_failed_read_cb(isc_nmsocket_t *sock, const isc_result_t result) {
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(result != ISC_R_SUCCESS);

	if (sock->client && sock->connect_cb != NULL && !sock->connected) {
		isc_nmhandle_t *handle = NULL;
		INSIST(sock->statichandle == NULL);
		handle = isc__nmhandle_get(sock, &sock->peer, &sock->iface);
		proxystream_call_connect_cb(sock, handle, result);
		isc__nmsocket_clearcb(sock);
		isc_nmhandle_detach(&handle);

		isc__nmsocket_prep_destroy(sock);
		return;
	}

	isc__nmsocket_timer_stop(sock);

	if (sock->statichandle == NULL) {
		isc__nmsocket_prep_destroy(sock);
		return;
	}

	/* See isc__nmsocket_readtimeout_cb() */
	if (sock->client && result == ISC_R_TIMEDOUT) {
		if (sock->recv_cb != NULL) {
			isc__nm_uvreq_t *req = isc__nm_get_read_req(sock, NULL);
			isc__nm_readcb(sock, req, result, false);
		}

		if (isc__nmsocket_timer_running(sock)) {
			/* Timer was restarted, bail-out */
			return;
		}

		isc__nmsocket_clearcb(sock);

		isc__nmsocket_prep_destroy(sock);
		return;
	}

	if (sock->recv_cb != NULL) {
		isc__nm_uvreq_t *req = isc__nm_get_read_req(sock, NULL);
		isc__nmsocket_clearcb(sock);
		isc__nm_readcb(sock, req, result, false);
	}

	isc__nmsocket_prep_destroy(sock);
}

static void
proxystream_read_cb(isc_nmhandle_t *handle, isc_result_t result,
		    isc_region_t *region, void *cbarg) {
	isc_nmsocket_t *proxysock = (isc_nmsocket_t *)cbarg;

	REQUIRE(VALID_NMSOCK(proxysock));
	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(proxysock->tid == isc_tid());

	if (result != ISC_R_SUCCESS) {
		goto failed;

	} else if (isc__nm_closing(proxysock->worker)) {
		result = ISC_R_SHUTTINGDOWN;
		goto failed;
	} else if (isc__nmsocket_closing(handle->sock)) {
		result = ISC_R_CANCELED;
		goto failed;
	}

	/* Handle initial PROXY header data */
	if (!proxysock->client && !proxysock->proxy.header_processed) {
		proxystream_handle_incoming_header_data(proxysock, region);
		return;
	}

	proxysock->recv_cb(proxysock->statichandle, ISC_R_SUCCESS, region,
			   proxysock->recv_cbarg);

	proxystream_try_close_unused(proxysock);

	return;
failed:
	proxystream_failed_read_cb(proxysock, result);
}

static void
proxystream_read_extra_cb(void *arg) {
	isc_result_t result = ISC_R_SUCCESS;
	isc__nm_uvreq_t *req = arg;
	isc_region_t extra_data = { 0 }; /* data past PROXY header */

	REQUIRE(VALID_UVREQ(req));

	isc_nmsocket_t *sock = req->sock;

	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->tid == isc_tid());

	sock->proxy.extra_processed = true;

	if (isc__nm_closing(sock->worker)) {
		result = ISC_R_SHUTTINGDOWN;
	} else if (proxystream_closing(sock)) {
		result = ISC_R_CANCELED;
	}

	if (result == ISC_R_SUCCESS) {
		extra_data.base = (uint8_t *)req->uvbuf.base;
		extra_data.length = req->uvbuf.len;

		INSIST(extra_data.length > 0);

		req->cb.recv(req->handle, result, &extra_data, req->cbarg);

		if (sock->reading) {
			proxystream_read_start(sock);
		}
	} else {
		isc__nm_proxystream_failed_read_cb(sock, result, false);
	}

	isc__nm_uvreq_put(&req);
}

void
isc__nm_proxystream_read(isc_nmhandle_t *handle, isc_nm_recv_cb_t cb,
			 void *cbarg) {
	isc_nmsocket_t *sock = NULL;
	isc_region_t extra_data = { 0 }; /* data past PROXY header */

	REQUIRE(VALID_NMHANDLE(handle));
	sock = handle->sock;
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);
	REQUIRE(sock->recv_handle == NULL);
	REQUIRE(sock->tid == isc_tid());

	sock->recv_cb = cb;
	sock->recv_cbarg = cbarg;
	sock->reading = true;

	if (isc__nm_closing(sock->worker)) {
		isc__nm_proxystream_failed_read_cb(sock, ISC_R_SHUTTINGDOWN,
						   false);
		return;
	} else if (proxystream_closing(sock)) {
		isc__nm_proxystream_failed_read_cb(sock, ISC_R_CANCELED, true);
		return;
	}

	/* check if there is extra data on the server */
	if (!sock->client && sock->proxy.header_processed &&
	    !sock->proxy.extra_processed &&
	    isc_proxy2_handler_extra(sock->proxy.proxy2.handler, &extra_data) >
		    0)
	{
		isc__nm_uvreq_t *req = isc__nm_uvreq_get(sock);
		isc_nmhandle_attach(handle, &req->handle);
		req->cb.recv = sock->recv_cb;
		req->cbarg = sock->recv_cbarg;

		req->uvbuf.base = (char *)extra_data.base;
		req->uvbuf.len = extra_data.length;

		isc_job_run(sock->worker->loop, &req->job,
			    proxystream_read_extra_cb, req);
		return;
	}

	proxystream_read_start(sock);
}

static proxystream_send_req_t *
proxystream_get_send_req(isc_mem_t *mctx, isc_nmsocket_t *sock,
			 isc_nmhandle_t *proxyhandle, isc_nm_cb_t cb,
			 void *cbarg) {
	proxystream_send_req_t *send_req = NULL;

	if (sock->proxy.send_req != NULL) {
		/*
		 * We have a previously allocated object - let's use that.
		 * That should help reducing stress on the memory allocator.
		 */
		send_req = (proxystream_send_req_t *)sock->proxy.send_req;
		sock->proxy.send_req = NULL;
	} else {
		/* Allocate a new object. */
		send_req = isc_mem_get(mctx, sizeof(*send_req));
		*send_req = (proxystream_send_req_t){ 0 };
	}

	/* Initialise the send request object */
	send_req->cb = cb;
	send_req->cbarg = cbarg;
	isc_nmhandle_attach(proxyhandle, &send_req->proxyhandle);

	sock->proxy.nsending++;

	return send_req;
}

static void
proxystream_put_send_req(isc_mem_t *mctx, proxystream_send_req_t *send_req,
			 const bool force_destroy) {
	/*
	 * Attempt to put the object for reuse later if we are not
	 * wrapping up.
	 */
	if (!force_destroy) {
		isc_nmsocket_t *sock = send_req->proxyhandle->sock;
		sock->proxy.nsending--;
		isc_nmhandle_detach(&send_req->proxyhandle);
		if (sock->proxy.send_req == NULL) {
			sock->proxy.send_req = send_req;
			/*
			 * An object has been recycled,
			 * if not - we are going to destroy it.
			 */
			return;
		}
	}

	isc_mem_put(mctx, send_req, sizeof(*send_req));
}

static void
proxystream_send_cb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) {
	proxystream_send_req_t *send_req = (proxystream_send_req_t *)cbarg;
	isc_mem_t *mctx;
	isc_nm_cb_t cb;
	void *send_cbarg;
	isc_nmhandle_t *proxyhandle = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMHANDLE(send_req->proxyhandle));
	REQUIRE(VALID_NMSOCK(send_req->proxyhandle->sock));
	REQUIRE(send_req->proxyhandle->sock->tid == isc_tid());

	mctx = send_req->proxyhandle->sock->worker->mctx;
	cb = send_req->cb;
	send_cbarg = send_req->cbarg;

	isc_nmhandle_attach(send_req->proxyhandle, &proxyhandle);
	/* try to keep the send request object for reuse */
	proxystream_put_send_req(mctx, send_req, false);
	cb(proxyhandle, result, send_cbarg);
	proxystream_try_close_unused(proxyhandle->sock);
	isc_nmhandle_detach(&proxyhandle);
}

static void
proxystream_send(isc_nmhandle_t *handle, isc_region_t *region, isc_nm_cb_t cb,
		 void *cbarg, const bool dnsmsg) {
	isc_nmsocket_t *sock = NULL;
	proxystream_send_req_t *send_req = NULL;
	isc_result_t result = ISC_R_SUCCESS;
	bool fail_async = true;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));

	sock = handle->sock;

	REQUIRE(sock->type == isc_nm_proxystreamsocket);

	if (isc__nm_closing(sock->worker)) {
		result = ISC_R_SHUTTINGDOWN;
		fail_async = false;
	} else if (proxystream_closing(sock)) {
		result = ISC_R_CANCELED;
		fail_async = true;
	}

	if (result != ISC_R_SUCCESS) {
		isc__nm_uvreq_t *uvreq = isc__nm_uvreq_get(sock);
		isc_nmhandle_attach(handle, &uvreq->handle);
		uvreq->cb.send = cb;
		uvreq->cbarg = cbarg;

		isc__nm_failed_send_cb(sock, uvreq, result, fail_async);
		return;
	}

	send_req = proxystream_get_send_req(sock->worker->mctx, sock, handle,
					    cb, cbarg);
	if (dnsmsg) {
		isc__nm_senddns(sock->outerhandle, region, proxystream_send_cb,
				send_req);
	} else {
		isc_nm_send(sock->outerhandle, region, proxystream_send_cb,
			    send_req);
	}
}

void
isc__nm_proxystream_send(isc_nmhandle_t *handle, isc_region_t *region,
			 isc_nm_cb_t cb, void *cbarg) {
	proxystream_send(handle, region, cb, cbarg, false);
}

void
isc__nm_proxystream_senddns(isc_nmhandle_t *handle, isc_region_t *region,
			    isc_nm_cb_t cb, void *cbarg) {
	proxystream_send(handle, region, cb, cbarg, true);
}

void
isc__nm_proxystream_set_tlsctx(isc_nmsocket_t *listener, isc_tlsctx_t *tlsctx) {
	REQUIRE(VALID_NMSOCK(listener));
	REQUIRE(listener->type == isc_nm_proxystreamlistener);

	if (listener->outer != NULL) {
		INSIST(VALID_NMSOCK(listener->outer));
		isc_nmsocket_set_tlsctx(listener->outer, tlsctx);
	}
}

bool
isc__nm_proxystream_has_encryption(const isc_nmhandle_t *handle) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		return isc_nm_has_encryption(sock->outerhandle);
	}

	return false;
}

const char *
isc__nm_proxystream_verify_tls_peer_result_string(const isc_nmhandle_t *handle) {
	isc_nmsocket_t *sock = NULL;

	REQUIRE(VALID_NMHANDLE(handle));
	REQUIRE(VALID_NMSOCK(handle->sock));
	REQUIRE(handle->sock->type == isc_nm_proxystreamsocket);

	sock = handle->sock;
	if (sock->outerhandle != NULL) {
		INSIST(VALID_NMHANDLE(sock->outerhandle));
		return isc_nm_verify_tls_peer_result_string(sock->outerhandle);
	}

	return NULL;
}

void
isc__nmhandle_proxystream_get_selected_alpn(isc_nmhandle_t *handle,
					    const unsigned char **alpn,
					    unsigned int *alpnlen) {
	isc_nmsocket_t *sock;

	REQUIRE(VALID_NMHANDLE(handle));
	sock = handle->sock;
	REQUIRE(VALID_NMSOCK(sock));
	REQUIRE(sock->type == isc_nm_proxystreamsocket);
	REQUIRE(sock->tid == isc_tid());

	isc__nmhandle_get_selected_alpn(sock->outerhandle, alpn, alpnlen);
}
