/*
 * Copyright (C) 2013-2023 Nikos Mavrogiannopoulos
 * Copyright (C) 2015, 2016 Red Hat, Inc.
 *
 * This file is part of ocserv.
 *
 * ocserv is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * ocserv is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <gnutls/gnutls.h>
#include <gnutls/dtls.h>
#include <gnutls/crypto.h>
#include <errno.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <system.h>
#include <time.h>
#include <gettime.h>
#include <common.h>
#include <html.h>
#include <ctype.h>
#include <worker-bandwidth.h>
#include <signal.h>
#include <poll.h>
#include <math.h>
#include <ev.h>

#if defined(__linux__) && !defined(IPV6_PATHMTU)
#define IPV6_PATHMTU 61
#endif

#include <vpn.h>
#include "ipc.pb-c.h"
#include <worker.h>
#include <tlslib.h>
#include <llhttp.h>

#if defined(CAPTURE_LATENCY_SUPPORT)
#include <linux/net_tstamp.h>
#include <linux/errqueue.h>
#include <worker-latency.h>
#endif

#define MIN_MTU(ws) (((ws)->vinfo.ipv6 != NULL) ? 1280 : 800)

#define PERIODIC_CHECK_TIME 30
#define MIN_STATS_INTERVAL 10

/* The number of DPD packets a client skips before he's kicked */
#define DPD_TRIES 2
#define DPD_MAX_TRIES 3

/* HTTP requests prior to disconnection */
#define MAX_HTTP_REQUESTS 16

#define CSTP_DTLS_OVERHEAD 1
#define CSTP_OVERHEAD 8

#define IP_HEADER_SIZE 20
#define IPV6_HEADER_SIZE 40
#define TCP_HEADER_SIZE 20
#define UDP_HEADER_SIZE 8

#define MSS_ADJUST(x)          \
	x += TCP_HEADER_SIZE + \
	     ((ws->proto == AF_INET) ? (IP_HEADER_SIZE) : (IPV6_HEADER_SIZE))

#define WORKER_MAINTENANCE_TIME (10.)

struct worker_st *global_ws;

static int terminate;
static int terminate_reason = REASON_SERVER_DISCONNECT;

static struct ev_loop *worker_loop;
ev_io command_watcher;
ev_io tls_watcher;
ev_io tun_watcher;
ev_timer period_check_watcher;
ev_signal term_sig_watcher;
ev_signal int_sig_watcher;
ev_signal alarm_sig_watcher;

static void term_sig_watcher_cb(struct ev_loop *loop, ev_signal *w,
				int revents);

static int worker_event_loop(struct worker_st *ws);

static int parse_cstp_data(struct worker_st *ws, uint8_t *buf, size_t buf_size,
			   time_t);
static int parse_dtls_data(struct worker_st *ws, uint8_t *buf, size_t buf_size,
			   time_t);
static int connect_handler(worker_st *ws);
static void session_info_send(worker_st *ws);
static void set_net_priority(worker_st *ws, int fd, int priority);
static void set_socket_timeout(worker_st *ws, int fd);

static void link_mtu_set(worker_st *ws, struct dtls_st *dtls, unsigned int mtu);

static int test_for_tcp_health_probe(struct worker_st *ws);

static void dtls_watcher_cb(EV_P_ ev_io *w, int revents);

static void handle_alarm(int signo)
{
	if (global_ws)
		exit_worker_reason(global_ws, terminate_reason);

	_exit(EXIT_FAILURE);
}

static void handle_term(int signo)
{
	terminate = 1;
	terminate_reason = REASON_SERVER_DISCONNECT;
	alarm(2); /* force exit by SIGALRM */
}

/* we override that function to force gnutls use poll()
 */
static int tls_pull_timeout(gnutls_transport_ptr_t ptr, unsigned int ms)
{
	int ret;
	int fd = (long)ptr;
	struct pollfd pfd;

	pfd.fd = fd;
	pfd.events = POLLIN;
	pfd.revents = 0;

	ret = poll(&pfd, 1, ms);
	if (ret <= 0)
		return ret;

	return ret;
}

inline static ssize_t dtls_pull_buffer_non_empty(gnutls_transport_ptr_t ptr)
{
	dtls_transport_ptr *p = ptr;

	if (p->msg)
		return 1;
	return 0;
}

#if !defined(CAPTURE_LATENCY_SUPPORT)
static ssize_t dtls_pull(gnutls_transport_ptr_t ptr, void *data, size_t size)
{
	dtls_transport_ptr *p = ptr;

	if (p->msg) {
		ssize_t need = p->msg->data.len;

		if (need > size) {
			need = size;
		}
		memcpy(data, p->msg->data.data, need);

		udp_fd_msg__free_unpacked(p->msg, NULL);
		p->msg = NULL;
		return need;
	}
	return recv(p->fd, data, size, 0);
}
#endif

static int dtls_pull_timeout(gnutls_transport_ptr_t ptr, unsigned int ms)
{
	int ret;
	dtls_transport_ptr *p = ptr;
	int fd = p->fd;
	struct pollfd pfd;

	if (dtls_pull_buffer_non_empty(ptr)) {
		return 1;
	}

	pfd.fd = fd;
	pfd.events = POLLIN;
	pfd.revents = 0;

	ret = poll(&pfd, 1, ms);
	if (ret <= 0)
		return ret;

	return ret;
}

static ssize_t dtls_push(gnutls_transport_ptr_t ptr, const void *data,
			 size_t size)
{
	dtls_transport_ptr *p = ptr;

	return send(p->fd, data, size, 0);
}

int get_psk_key(gnutls_session_t session, const char *username,
		gnutls_datum_t *key)
{
	struct worker_st *ws = gnutls_session_get_ptr(session);

	key->data = gnutls_malloc(PSK_KEY_SIZE);
	if (key->data == NULL)
		return GNUTLS_E_MEMORY_ERROR;

	memcpy(key->data, ws->master_secret, PSK_KEY_SIZE);
	key->size = PSK_KEY_SIZE;

	return 0;
}

#if GNUTLS_VERSION_NUMBER < 0x030318
#define VERS_STRING "-VERS-TLS-ALL"
#else
#define VERS_STRING "-VERS-ALL"
#endif

#define PSK_LABEL "EXPORTER-openconnect-psk"
#define PSK_LABEL_SIZE sizeof(PSK_LABEL) - 1
/* We initial a PSK connection with ciphers and MAC matching the TLS negotiated
 * ciphers and MAC. The key is 32-bytes generated from gnutls_prf_rfc5705()
 * with label being the PSK_LABEL.
 */
static int setup_dtls_psk_keys(gnutls_session_t session, struct worker_st *ws)
{
	int ret;
	char prio_string[256];
	gnutls_mac_algorithm_t mac;
	gnutls_cipher_algorithm_t cipher;

	gnutls_psk_set_server_credentials_function(WSCREDS(ws)->pskcred,
						   get_psk_key);

	if (!ws->session) {
		oclog(ws, LOG_ERR,
		      "cannot setup PSK keys without an encrypted CSTP channel");
		return -1;
	}

	if (WSCONFIG(ws)->match_dtls_and_tls) {
		cipher = gnutls_cipher_get(ws->session);
		mac = gnutls_mac_get(ws->session);

		snprintf(
			prio_string, sizeof(prio_string),
			"%s:" VERS_STRING
			":-CIPHER-ALL:-MAC-ALL:-KX-ALL:+PSK:+VERS-DTLS-ALL:+%s:+%s",
			WSCONFIG(ws)->priorities, gnutls_mac_get_name(mac),
			gnutls_cipher_get_name(cipher));
	} else {
		/* if we haven't an associated session, enable all ciphers we would have enabled
		 * otherwise for TLS. */
		snprintf(prio_string, sizeof(prio_string),
			 "%s:" VERS_STRING ":-KX-ALL:+PSK:+VERS-DTLS-ALL",
			 WSCONFIG(ws)->priorities);
	}

	ret = gnutls_priority_set_direct(session, prio_string, NULL);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not set TLS priority: '%s': %s",
		      prio_string, gnutls_strerror(ret));
		return ret;
	}

	/* we should have used gnutls_prf_rfc5705() but since we don't use
	 * the RFC5705 context, the output is identical with gnutls_prf(). The
	 * latter is available in much earlier versions of gnutls. */
	ret = gnutls_prf(ws->session, PSK_LABEL_SIZE, PSK_LABEL, 0, 0, 0,
			 PSK_KEY_SIZE, (char *)ws->master_secret);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "error in PSK key generation: %s",
		      gnutls_strerror(ret));
		return ret;
	}

	ret = gnutls_credentials_set(session, GNUTLS_CRD_PSK,
				     WSCREDS(ws)->pskcred);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not set TLS PSK credentials: %s",
		      gnutls_strerror(ret));
		return ret;
	}

	return 0;
}

static int setup_legacy_dtls_keys(gnutls_session_t session,
				  struct worker_st *ws)
{
	int ret;
	gnutls_datum_t master = { ws->master_secret,
				  sizeof(ws->master_secret) };
	gnutls_datum_t sid = { ws->session_id, sizeof(ws->session_id) };

	if (ws->req.selected_ciphersuite == NULL) {
		oclog(ws, LOG_ERR, "no DTLS ciphersuite negotiated");
		return -1;
	}

	ret = gnutls_priority_set_direct(
		session, ws->req.selected_ciphersuite->gnutls_name, NULL);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not set TLS priority: %s",
		      gnutls_strerror(ret));
		return ret;
	}

	ret = gnutls_session_set_premaster(
		session, GNUTLS_SERVER,
		ws->req.selected_ciphersuite->gnutls_version,
		ws->req.selected_ciphersuite->gnutls_kx,
		ws->req.selected_ciphersuite->gnutls_cipher,
		ws->req.selected_ciphersuite->gnutls_mac, GNUTLS_COMP_NULL,
		&master, &sid);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not set TLS premaster: %s",
		      gnutls_strerror(ret));
		return ret;
	}

	gnutls_certificate_server_set_request(session, GNUTLS_CERT_IGNORE);

	ret = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
				     WSCREDS(ws)->xcred);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not set TLS credentials: %s",
		      gnutls_strerror(ret));
		return ret;
	}

	return 0;
}

static int setup_dtls_connection(struct worker_st *ws, struct dtls_st *dtls)
{
	int ret;
	gnutls_session_t session;
#if defined(CAPTURE_LATENCY_SUPPORT)
	int ts_socket_opt = SOF_TIMESTAMPING_RX_SOFTWARE |
			    SOF_TIMESTAMPING_SOFTWARE;
#endif

	/* DTLS cookie verified.
	 * Initialize session.
	 */
	ret = gnutls_init(&session,
			  GNUTLS_SERVER | GNUTLS_DATAGRAM | GNUTLS_NONBLOCK);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "could not initialize TLS session: %s",
		      gnutls_strerror(ret));
		return -1;
	}

	gnutls_session_set_ptr(session, ws);

	if (ws->req.use_psk && ws->session) {
		oclog(ws, LOG_INFO, "setting up DTLS-PSK connection");
		ret = setup_dtls_psk_keys(session, ws);
	} else {
		if (!WSCONFIG(ws)->dtls_legacy) {
			oclog(ws, LOG_INFO,
			      "CISCO client compatibility (dtls-legacy) is disabled; will not setup a DTLS session");
			goto fail;
		}
		oclog(ws, LOG_INFO,
		      "setting up legacy DTLS (resumption) connection");
		ret = setup_legacy_dtls_keys(session, ws);
	}

	if (ret < 0) {
		goto fail;
	}

	gnutls_transport_set_push_function(session, dtls_push);
#if defined(CAPTURE_LATENCY_SUPPORT)
	gnutls_transport_set_pull_function(session, dtls_pull_latency);
#else
	gnutls_transport_set_pull_function(session, dtls_pull);
#endif
	gnutls_transport_set_pull_timeout_function(session, dtls_pull_timeout);
	gnutls_transport_set_ptr(session, &dtls->dtls_tptr);

	/* we decrease the default retransmission timeout to bring
	 * our DTLS support in par with the DTLS1.3 recommendations.
	 */
	gnutls_dtls_set_timeouts(session, 400, 60 * 1000);

	dtls->udp_state = UP_HANDSHAKE;

#if defined(CAPTURE_LATENCY_SUPPORT)
	ret = setsockopt(dtls->dtls_tptr.fd, SOL_SOCKET, SO_TIMESTAMPING,
			 &ts_socket_opt, sizeof(ts_socket_opt));
	if (ret == -1)
		oclog(ws, LOG_DEBUG,
		      "setsockopt(UDP, SO_TIMESTAMPING), failed.");
#endif

	/* Setup the fd settings */
	if (WSCONFIG(ws)->output_buffer > 0) {
		int t = MIN(2048, ws->link_mtu * WSCONFIG(ws)->output_buffer);

		ret = setsockopt(dtls->dtls_tptr.fd, SOL_SOCKET, SO_SNDBUF, &t,
				 sizeof(t));
		if (ret == -1)
			oclog(ws, LOG_DEBUG,
			      "setsockopt(UDP, SO_SNDBUF) to %u, failed.", t);
	}
	set_net_priority(ws, dtls->dtls_tptr.fd, ws->user_config->net_priority);
	set_socket_timeout(ws, dtls->dtls_tptr.fd);

	/* reset MTU */
	link_mtu_set(ws, dtls, ws->adv_link_mtu);

	if (dtls->dtls_session != NULL) {
		gnutls_deinit(dtls->dtls_session);
	}

	dtls->dtls_session = session;
	ev_io_stop(worker_loop, &dtls->io);
	ev_io_set(&dtls->io, dtls->dtls_tptr.fd, EV_READ);
	ev_io_start(worker_loop, &dtls->io);
	ev_invoke(worker_loop, &dtls->io, EV_READ);

	return 0;
fail:
	gnutls_deinit(session);
	return -1;
}

void ws_add_score_to_ip(worker_st *ws, unsigned int points, unsigned int final,
			unsigned int discon_reason)
{
	int ret, e;
	BanIpMsg msg = BAN_IP_MSG__INIT;
	BanIpReplyMsg *reply = NULL;

	PROTOBUF_ALLOCATOR(pa, ws);

	/* no reporting if banning is disabled */
	if (WSCONFIG(ws)->max_ban_score == 0)
		return;

	/* In final call, no score added, we simply send */
	if (final == 0) {
		ws->ban_points += points;
		/* do not use IPC for small values */
		if (points < WSCONFIG(ws)->ban_points_wrong_password)
			return;
	}

	msg.ip = ws->remote_ip_str;
	msg.score = points;
	if (final) {
		msg.has_discon_reason = 1;
		msg.discon_reason = discon_reason;
	}

	ret = send_msg(ws, ws->cmd_fd, CMD_BAN_IP, &msg,
		       (pack_size_func)ban_ip_msg__get_packed_size,
		       (pack_func)ban_ip_msg__pack);
	if (ret < 0) {
		e = errno;
		oclog(ws, LOG_WARNING, "error in sending BAN IP message: %s",
		      strerror(e));
		return;
	}

	ret = recv_msg(ws, ws->cmd_fd, CMD_BAN_IP_REPLY, (void *)&reply,
		       (unpack_func)ban_ip_reply_msg__unpack,
		       DEFAULT_SOCKET_TIMEOUT);
	if (ret < 0) {
		oclog(ws, LOG_ERR, "error receiving BAN IP reply message");
		return;
	}

	if (final == 0 && reply->reply != AUTH__REP__OK) {
		/* we have exceeded the maximum score */
		exit(EXIT_FAILURE);
	}

	ban_ip_reply_msg__free_unpacked(reply, &pa);
}

static void send_stats_to_secmod(worker_st *ws, time_t now,
				 unsigned int discon_reason)
{
	CliStatsMsg msg = CLI_STATS_MSG__INIT;
	int sd, ret, e;

	/* this is only used by certain tests */
	if (WSPCONFIG(ws)->debug_no_secmod_stats != 0)
		return;

	ws->last_stats_msg = now;

	sd = connect_to_secmod(ws);
	if (sd >= 0) {
		char buf[64];

		msg.bytes_in = ws->tun_bytes_in;
		msg.bytes_out = ws->tun_bytes_out;
		msg.uptime = now - ws->session_start_time;
		msg.sid.len = sizeof(ws->sid);
		msg.sid.data = ws->sid;
		msg.has_sid = 1;

		if (discon_reason) {
			msg.has_discon_reason = 1;
			msg.discon_reason = discon_reason;
		}

		msg.remote_ip = human_addr2((void *)&ws->remote_addr,
					    ws->remote_addr_len, buf,
					    sizeof(buf), 0);

		msg.ipv4 = ws->vinfo.ipv4;
		msg.ipv6 = ws->vinfo.ipv6;

		ret = send_msg_to_secmod(
			ws, sd, CMD_SEC_CLI_STATS, &msg,
			(pack_size_func)cli_stats_msg__get_packed_size,
			(pack_func)cli_stats_msg__pack);
		if (discon_reason) { /* wait for sec-mod to close connection to verify data have been accounted */
			e = read(sd, buf, sizeof(buf));
			if (e == -1) {
				e = errno;
				oclog(ws, LOG_DEBUG,
				      "could not wait for sec-mod: %s\n",
				      strerror(e));
			}
		}
		close(sd);

		if (ret >= 0) {
			oclog(ws, LOG_INFO,
			      "sent periodic stats (in: %lu, out: %lu) to sec-mod",
			      (unsigned long)msg.bytes_in,
			      (unsigned long)msg.bytes_out);
		} else {
			e = errno;
			oclog(ws, LOG_WARNING,
			      "could not send periodic stats to sec-mod: %s\n",
			      strerror(e));
		}
	}
}

/* Terminates the worker process, but communicates any required
 * data to main process before (stats/ban points).
 */
void exit_worker(worker_st *ws)
{
	exit_worker_reason(ws, REASON_ANY);
}

void exit_worker_reason(worker_st *ws, unsigned int reason)
{
	/* send statistics to parent */
	if (ws->auth_state == S_AUTH_COMPLETE) {
		send_stats_to_secmod(ws, time(NULL), reason);
	}

	if (ws->ban_points > 0)
		ws_add_score_to_ip(ws, 0, 1, reason);

	talloc_free(ws->main_pool);
	closelog();
	_exit(EXIT_FAILURE);
}

#define HANDSHAKE_SESSION_ID_POS (34)
#define SKIP_V16(pos, total)                                     \
	{                                                        \
		uint16_t _s;                                     \
		if (pos + 2 > total)                             \
			goto finish;                             \
		_s = (msg->data[pos] << 8) | msg->data[pos + 1]; \
		if (pos + 2 + _s > total)                        \
			goto finish;                             \
		pos += 2 + _s;                                   \
	}

#define SKIP16(pos, total)           \
	{                            \
		if (pos + 2 > total) \
			goto finish; \
		pos += 2;            \
	}

#define SKIP8(pos, total)            \
	{                            \
		if (pos + 1 > total) \
			goto finish; \
		pos++;               \
	}

#define SKIP_V8(pos, total)               \
	{                                 \
		uint8_t _s;               \
		if (pos + 1 > total)      \
			goto finish;      \
		_s = msg->data[pos];      \
		if (pos + 1 + _s > total) \
			goto finish;      \
		pos += 1 + _s;            \
	}

#define SET_VHOST_CREDS                                                        \
	do {                                                                   \
		ret = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,  \
					     WSCREDS(ws)->xcred);              \
		GNUTLS_FATAL_ERR(ret);                                         \
		gnutls_certificate_server_set_request(session,                 \
						      WSCONFIG(ws)->cert_req); \
		ret = gnutls_priority_set(session, WSCREDS(ws)->cprio);        \
		GNUTLS_FATAL_ERR(ret);                                         \
		gnutls_db_set_cache_expiration(                                \
			session, TLS_SESSION_EXPIRATION_TIME(WSCONFIG(ws)));   \
	} while (0)

/* Parse the TLS client hello to figure vhost */
static int hello_hook_func(gnutls_session_t session, unsigned int htype,
			   unsigned int when, unsigned int incoming,
			   const gnutls_datum_t *msg)

{
	ssize_t ret;
	size_t pos;
	size_t hsize;
	struct worker_st *ws = gnutls_session_get_ptr(session);

	if (htype != GNUTLS_HANDSHAKE_CLIENT_HELLO || when != GNUTLS_HOOK_PRE)
		goto finish;

	/* find the server name extension */

	pos = HANDSHAKE_SESSION_ID_POS;
	if (msg->size <= pos)
		goto finish;

	if (msg->data[0] != 0x03) {
		/* unknown packet version */
		goto finish;
	}

	/* skip session id */
	SKIP_V8(pos, msg->size);

	/* CipherSuites */
	SKIP_V16(pos, msg->size);

	/* legacy_compression_methods */
	SKIP_V8(pos, msg->size);

	/* Skip extension total size */
	SKIP16(pos, msg->size);

	while (pos < msg->size) {
		uint16_t type;

		/* read ExtensionType */
		SKIP16(pos, msg->size);
		type = (msg->data[pos - 2] << 8) | msg->data[pos - 1];

		if (type == 0) { /* server name ext */
			SKIP16(pos, msg->size);
			SKIP16(pos,
			       msg->size); /* we don't support anything but a single name */

			SKIP8(pos, msg->size);
			if (msg->data[pos - 1] != 0) { /* HostName */
				oclog(ws, LOG_DEBUG,
				      "received server name extension with invalid name type field");
				goto finish;
			}

			SKIP16(pos, msg->size);
			hsize = (msg->data[pos - 2] << 8) | msg->data[pos - 1];

			if (hsize == 0 || hsize + pos > msg->size ||
			    hsize > sizeof(ws->buffer) - 1) {
				oclog(ws, LOG_DEBUG,
				      "received server name extension with too large name");
				goto finish;
			}

			memcpy(ws->buffer, &msg->data[pos], hsize);
			ws->buffer[hsize] = 0;

			oclog(ws, LOG_DEBUG, "client requested hostname: %s",
			      (char *)ws->buffer);

			ws->vhost = find_vhost(ws->vconfig, (char *)ws->buffer);
			if (ws->vhost->name &&
			    strcasecmp(ws->vhost->name, (char *)ws->buffer) !=
				    0) {
				oclog(ws, LOG_INFO,
				      "client requested hostname %s does not match known vhost",
				      (char *)ws->buffer);
			}

			goto finish;
		} else {
			SKIP_V16(pos, msg->size);
		}
	}

finish:
	/* We set credentials irrespective of whether a virtual host was found,
	 * as they have not been previously set. */
	SET_VHOST_CREDS;

	return 0;
}

#if GNUTLS_VERSION_NUMBER < 0x030400
#define SIMULATE_CLIENT_HELLO_HOOK
#endif

#ifdef SIMULATE_CLIENT_HELLO_HOOK
#define TLS_RECORD_HEADER 5
#define TLS_HANDSHAKE_HEADER 4

/* In gnutls 3.3 we don't get the size in the handshake callback
 * so we try to simulate.
 */
static void peek_client_hello(struct worker_st *ws, gnutls_session_t session,
			      int fd)
{
	unsigned int read_tries = 0;
	int ret;
	size_t size, hsize;
	gnutls_datum_t msg;

	do {
		if (read_tries > 0) {
			if (read_tries > 5)
				goto fallback;
			ms_sleep(150);
		}
		read_tries++;

		ret = recv(fd, ws->buffer, sizeof(ws->buffer), MSG_PEEK);
		if (ret == -1)
			goto fallback;
		size = ret;

		if (size < TLS_RECORD_HEADER)
			goto fallback;

		hsize = (ws->buffer[3] << 8) | ws->buffer[4];
	} while (hsize + TLS_RECORD_HEADER > size);

	if (size <
	    TLS_RECORD_HEADER + TLS_HANDSHAKE_HEADER + HANDSHAKE_SESSION_ID_POS)
		goto fallback;

	msg.data = ws->buffer + TLS_RECORD_HEADER + TLS_HANDSHAKE_HEADER;
	msg.size = size - (TLS_RECORD_HEADER + TLS_HANDSHAKE_HEADER);
	hello_hook_func(session, GNUTLS_HANDSHAKE_CLIENT_HELLO, GNUTLS_HOOK_PRE,
			1, &msg);

	return;

fallback:
	SET_VHOST_CREDS;
}
#endif

static void check_camouflage_url(struct worker_st *ws)
{
	if (WSCONFIG(ws)->camouflage_secret == NULL)
		return;

	if (ws->auth_state >= S_AUTH_COOKIE) {
		ws->camouflage_check_passed = 1;
		return;
	}

	char *url_camouflage_part = strchr(ws->req.url, '?');

	if (url_camouflage_part &&
	    !strcmp(url_camouflage_part + 1, WSCONFIG(ws)->camouflage_secret)) {
		*url_camouflage_part = '\0';
		ws->camouflage_check_passed = 1;
	}
}

/* vpn_server:
 * @ws: an initialized worker structure
 *
 * This is the main worker process. It is executed
 * by the main server after fork and drop of privileges.
 *
 * It handles the client connection including:
 *  - HTTPS authentication using XML forms that are parsed and
 *    forwarded to main.
 *  - TLS authentication (using certificate)
 *  - TCP VPN tunnel establishment (after HTTP CONNECT)
 *  - UDP VPN tunnel establishment (once an FD is forwarded by main)
 *
 */
void vpn_server(struct worker_st *ws)
{
	int ret;
	ssize_t nrecvd;
	gnutls_session_t session = NULL;
	llhttp_t parser;
	llhttp_settings_t settings;
	url_handler_fn fn;
	int requests_left = MAX_HTTP_REQUESTS;
	enum llhttp_errno lerr;

	ocsigaltstack(ws);

	ocsignal(SIGTERM, handle_term);
	ocsignal(SIGINT, handle_term);
	ocsignal(SIGHUP, SIG_IGN);
	ocsignal(SIGALRM, handle_alarm);

	global_ws = ws;
	if (GETCONFIG(ws)->auth_timeout) {
		terminate_reason = REASON_SERVER_DISCONNECT;
		alarm(GETCONFIG(ws)->auth_timeout);
	}

	/* do not allow this process to be traced. That
	 * prevents worker processes tracing each other. */
	if (GETPCONFIG(ws)->pr_dumpable != 1)
		pr_set_undumpable("worker");
	if (GETCONFIG(ws)->isolate != 0) {
		ret = disable_system_calls(ws);
		if (ret < 0) {
			oclog(ws, LOG_INFO,
			      "could not disable system calls, kernel might not support seccomp");
		}
	}

	if (ws->remote_addr_len == sizeof(struct sockaddr_in))
		ws->proto = AF_INET;
	else
		ws->proto = AF_INET6;

	if (GETCONFIG(ws)->listen_proxy_proto) {
		oclog(ws, LOG_DEBUG, "accepted proxy protocol connection");
		ret = parse_proxy_proto_header(ws, ws->conn_fd);
		if (ret < 0) {
			oclog(ws, LOG_ERR,
			      "could not parse proxy protocol header; discarding connection");
			exit_worker(ws);
		}

		/* update ws->remote_ip_str */
		human_addr2((const struct sockaddr *)&ws->remote_addr,
			    ws->remote_addr_len, ws->remote_ip_str,
			    sizeof(ws->remote_ip_str), 0);
	} else {
		oclog(ws, LOG_DEBUG, "accepted connection");
	}

	if (ws->conn_type != SOCK_TYPE_UNIX) {
		/* ws->vhost is being assigned in gnutls_handshake()
		 * after client hello is received. We set temporarily a value
		 * as we need to set some cipher priorities for handshake to start. */
		ws->vhost = find_vhost(ws->vconfig, NULL);

		if (test_for_tcp_health_probe(ws) != 0) {
			oclog(ws, LOG_DEBUG,
			      "Received TCP health probe from load-balancer");
			exit_worker_reason(ws, REASON_HEALTH_PROBE);
		}

		/* initialize the session */
		ret = gnutls_init(&session, GNUTLS_SERVER);
		GNUTLS_FATAL_ERR(ret);

		ret = gnutls_priority_set(session, WSCREDS(ws)->cprio);
		GNUTLS_FATAL_ERR(ret);
		gnutls_session_set_ptr(session, ws);

		/* if we have a single vhost, avoid going through a callback to set credentials. */
		if (!HAVE_VHOSTS(ws)) {
			SET_VHOST_CREDS;
		} else {
#ifdef SIMULATE_CLIENT_HELLO_HOOK
			peek_client_hello(ws, session, ws->conn_fd);
#else
			gnutls_handshake_set_hook_function(
				session, GNUTLS_HANDSHAKE_CLIENT_HELLO,
				GNUTLS_HOOK_PRE, hello_hook_func);
#endif
		}

		gnutls_transport_set_ptr(
			session, (gnutls_transport_ptr_t)(long)ws->conn_fd);

		set_resume_db_funcs(session);
		gnutls_db_set_ptr(session, ws);

		gnutls_handshake_set_timeout(session,
					     GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
		gnutls_transport_set_pull_timeout_function(session,
							   tls_pull_timeout);
		do {
			ret = gnutls_handshake(session);
		} while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
		GNUTLS_ALERT_PRINT(ws, session, ret);
		GNUTLS_FATAL_ERR(ret);

		oclog(ws, LOG_DEBUG, "TLS handshake completed");
	} else {
		ws->vhost = find_vhost(ws->vconfig, NULL);

		oclog(ws, LOG_DEBUG, "Accepted unix connection");
	}

	ws->session = session;

	session_info_send(ws);

	llhttp_settings_init(&settings);

	ws->selected_auth = &WSPCONFIG(ws)->auth[0];
	if (ws->cert_auth_ok)
		ws_switch_auth_to(ws, AUTH_TYPE_CERTIFICATE);

	settings.on_url = http_url_cb;
	settings.on_header_field = http_header_field_cb;
	settings.on_header_value = http_header_value_cb;
	settings.on_headers_complete = http_header_complete_cb;
	settings.on_message_complete = http_message_complete_cb;
	settings.on_body = http_body_cb;
	http_req_init(ws);

	if (WSCONFIG(ws)->listen_proxy_proto) {
		oclog(ws, LOG_DEBUG, "proxy-hdr: peer is %s\n",
		      ws->remote_ip_str);
	}

	ws->parser = &parser;

restart:
	if (requests_left-- <= 0) {
		oclog(ws, LOG_INFO, "maximum number of HTTP requests reached");
		exit_worker(ws);
	}

	llhttp_init(&parser, HTTP_REQUEST, &settings);
	parser.data = ws;
	http_req_reset(ws);
	/* parse as we go */
	do {
		nrecvd = cstp_recv(ws, ws->buffer, sizeof(ws->buffer));
		if (nrecvd <= 0) {
			if (nrecvd == 0)
				goto finish;
			if (nrecvd != GNUTLS_E_PREMATURE_TERMINATION)
				oclog(ws, LOG_WARNING,
				      "error receiving client data");
			exit_worker(ws);
		}

		lerr = llhttp_execute(&parser, (void *)ws->buffer, nrecvd);
		if (lerr == HPE_PAUSED_UPGRADE &&
		    parser.method == HTTP_CONNECT) {
			llhttp_resume_after_upgrade(&parser);
			break;
		} else if (lerr != HPE_OK) {
			oclog(ws, LOG_INFO, "error parsing HTTP request: %s",
			      llhttp_errno_name(lerr));
			exit_worker(ws);
		}
	} while (ws->req.headers_complete == 0);

	if ((parser.method == HTTP_GET || parser.method == HTTP_POST) &&
	    (WSCONFIG(ws)->camouflage && ws->camouflage_check_passed == 0)) {
		check_camouflage_url(ws);
		if (ws->camouflage_check_passed == 0) {
			oclog(ws, LOG_INFO,
			      "Secret not found in URL, declining...");
			if (WSCONFIG(ws)->camouflage_realm)
				response_401(ws, parser.http_minor,
					     WSCONFIG(ws)->camouflage_realm);
			else
				response_404(ws, parser.http_minor);
			goto finish;
		}
	}

	if (parser.method == HTTP_GET) {
		oclog(ws, LOG_HTTP_DEBUG, "HTTP GET %s", ws->req.url);
		fn = http_get_url_handler(ws->req.url);
		if (fn == NULL) {
			oclog(ws, LOG_HTTP_DEBUG, "unexpected URL %s",
			      ws->req.url);
			response_404(ws, parser.http_minor);
			goto finish;
		}
		ret = fn(ws, parser.http_minor);
		if (ret == 0 &&
		    (parser.http_major != 1 || parser.http_minor != 0))
			goto restart;

	} else if (parser.method == HTTP_POST) {
		/* continue reading */
		oclog(ws, LOG_HTTP_DEBUG, "HTTP POST %s", ws->req.url);
		while (ws->req.message_complete == 0) {
			nrecvd = cstp_recv(ws, ws->buffer, sizeof(ws->buffer));
			CSTP_FATAL_ERR(ws, nrecvd);

			if (nrecvd == 0) {
				oclog(ws, LOG_HTTP_DEBUG,
				      "EOF while receiving HTTP POST request");
				exit_worker(ws);
			}

			lerr = llhttp_execute(&parser, (void *)ws->buffer,
					      nrecvd);
			if (lerr != HPE_OK) {
				oclog(ws, LOG_HTTP_DEBUG,
				      "error parsing HTTP POST request: %s",
				      llhttp_errno_name(lerr));
				exit_worker(ws);
			}
		}

		fn = http_post_url_handler(ws, ws->req.url);
		if (fn == NULL) {
			oclog(ws, LOG_HTTP_DEBUG, "unexpected POST URL %s",
			      ws->req.url);
			response_404(ws, parser.http_minor);
			goto finish;
		}

		ret = fn(ws, parser.http_minor);
		if (ret == 0 &&
		    (parser.http_major != 1 || parser.http_minor != 0))
			goto restart;

	} else if (parser.method == HTTP_CONNECT) {
		oclog(ws, LOG_HTTP_DEBUG, "HTTP CONNECT %s", ws->req.url);
		ret = connect_handler(ws);
		if (ret == 0 &&
		    (parser.http_major != 1 || parser.http_minor != 0))
			goto restart;

	} else {
		oclog(ws, LOG_HTTP_DEBUG, "unexpected HTTP method %s",
		      llhttp_method_name(parser.method));
		response_404(ws, parser.http_minor);
	}

finish:
	llhttp_finish(&parser);
	cstp_close(ws);
}

static void data_mtu_send(worker_st *ws, unsigned int mtu)
{
	TunMtuMsg msg = TUN_MTU_MSG__INIT;

	msg.mtu = mtu;
	send_msg_to_main(ws, CMD_TUN_MTU, &msg,
			 (pack_size_func)tun_mtu_msg__get_packed_size,
			 (pack_func)tun_mtu_msg__pack);

	oclog(ws, LOG_DEBUG, "setting data MTU to %u", msg.mtu);
}

static void session_info_send(worker_st *ws)
{
	SessionInfoMsg msg = SESSION_INFO_MSG__INIT;

	if (ws->session) {
		msg.tls_ciphersuite = gnutls_session_get_desc(ws->session);
		if (ws->cstp_selected_comp)
			msg.cstp_compr = (char *)ws->cstp_selected_comp->name;
	}

	if (DTLS_ACTIVE(ws)->udp_state != UP_DISABLED &&
	    DTLS_ACTIVE(ws)->dtls_session) {
		msg.dtls_ciphersuite =
			gnutls_session_get_desc(DTLS_ACTIVE(ws)->dtls_session);
		if (ws->dtls_selected_comp)
			msg.dtls_compr = (char *)ws->dtls_selected_comp->name;
	}

	if (WSCONFIG(ws)->listen_proxy_proto) {
		msg.our_addr.data = (uint8_t *)&ws->our_addr;
		msg.our_addr.len = ws->our_addr_len;
		msg.has_our_addr = 1;
		msg.remote_addr.data = (uint8_t *)&ws->remote_addr;
		msg.remote_addr.len = ws->remote_addr_len;
		msg.has_remote_addr = 1;
	}

	send_msg_to_main(ws, CMD_SESSION_INFO, &msg,
			 (pack_size_func)session_info_msg__get_packed_size,
			 (pack_func)session_info_msg__pack);

	gnutls_free(msg.tls_ciphersuite);
	gnutls_free(msg.dtls_ciphersuite);
}

/* link_mtu_set: Sets the link MTU for the session
 *
 * @ws: a worker structure
 * @mtu: the link MTU
 */
static void link_mtu_set(struct worker_st *ws, struct dtls_st *dtls,
			 unsigned int mtu)
{
	if (ws->link_mtu == mtu || mtu > sizeof(ws->buffer))
		return;

	ws->link_mtu = mtu;

	oclog(ws, LOG_DEBUG, "setting connection link MTU to %u", mtu);
	if (dtls->dtls_session)
		gnutls_dtls_set_mtu(dtls->dtls_session,
				    ws->link_mtu - ws->dtls_proto_overhead);

	data_mtu_send(ws, DATA_MTU(ws, ws->link_mtu));
}

/* data_mtu_set: Sets the data MTU for the session
 *
 * @ws: a worker structure
 * @mtu: the "plaintext" data MTU (not including the DTLS protocol byte)
 */
static void data_mtu_set(worker_st *ws, struct dtls_st *dtls, unsigned int mtu)
{
	if (dtls->dtls_session) {
		gnutls_dtls_set_data_mtu(dtls->dtls_session, mtu + 1);

		mtu = gnutls_dtls_get_mtu(dtls->dtls_session);
		if (mtu <= 0 || mtu == ws->link_mtu)
			return;

		mtu += ws->dtls_proto_overhead;
		link_mtu_set(ws, dtls, mtu);
	}
}

static void disable_mtu_disc(worker_st *ws, struct dtls_st *dtls)
{
	oclog(ws, LOG_DEBUG, "disabling MTU discovery on UDP socket");
	set_mtu_disc(dtls->dtls_tptr.fd, ws->proto, 0);
	link_mtu_set(ws, dtls, ws->adv_link_mtu);
	WSCONFIG(ws)->try_mtu = 0;
}

/* sets the current value of mtu as bad,
 * and returns an estimation of good.
 *
 * Returns -1 on failure.
 */
static int mtu_not_ok(worker_st *ws, struct dtls_st *dtls)
{
	if (WSCONFIG(ws)->try_mtu == 0 || dtls->dtls_session == NULL)
		return 0;

	if (ws->proto == AF_INET) {
		const unsigned int min = MIN_MTU(ws);

		ws->last_bad_mtu = ws->link_mtu;

		if (ws->last_good_mtu == min) {
			oclog(ws, LOG_INFO,
			      "could not calculate a sufficient MTU; disabling MTU discovery");
			disable_mtu_disc(ws, dtls);
			link_mtu_set(ws, dtls, min);
			return 0;
		}

		if (ws->last_good_mtu >= ws->link_mtu) {
			ws->last_good_mtu =
				MAX(((2 * (ws->link_mtu)) / 3), min);
		}

		link_mtu_set(ws, dtls, ws->last_good_mtu);
		oclog(ws, LOG_INFO, "MTU %u is too large, switching to %u",
		      ws->last_bad_mtu, ws->link_mtu);
	} else if (ws->proto == AF_INET6) { /* IPv6 */
#ifdef IPV6_PATHMTU
		struct ip6_mtuinfo mtuinfo;
		socklen_t len = sizeof(mtuinfo);

		if (getsockopt(dtls->dtls_tptr.fd, IPPROTO_IPV6, IPV6_PATHMTU,
			       &mtuinfo, &len) < 0 ||
		    mtuinfo.ip6m_mtu < 1280) {
			oclog(ws, LOG_INFO,
			      "cannot obtain IPv6 MTU (was %u); disabling MTU discovery",
			      ws->link_mtu);
			disable_mtu_disc(ws, dtls);
			link_mtu_set(ws, dtls, MIN_MTU(ws));
			return 0;
		}

		oclog(ws, LOG_DEBUG,
		      "setting (via IPV6_PATHMTU) connection MTU to %u",
		      mtuinfo.ip6m_mtu);
		link_mtu_set(ws, dtls, mtuinfo.ip6m_mtu);

		if (mtuinfo.ip6m_mtu > ws->adv_link_mtu) {
			oclog(ws, LOG_INFO,
			      "the discovered IPv6 MTU (%u) is larger than the advertised (%u); disabling MTU discovery",
			      (unsigned int)mtuinfo.ip6m_mtu, ws->adv_link_mtu);
			return 0;
		}
#else
		link_mtu_set(ws, dtls, MIN_MTU(ws));
#endif
	}

	return 0;
}

/* mtu_discovery_init: initiates MTU discovery
 *
 * @ws: a worker structure
 * @mtu: the current "plaintext" data MTU
 */
static void mtu_discovery_init(worker_st *ws, struct dtls_st *dtls,
			       unsigned int mtu)
{
	const unsigned int min = MIN_MTU(ws);

	if (mtu <= min) {
		oclog(ws, LOG_INFO,
		      "our initial MTU is too low; disabling MTU discovery");
		disable_mtu_disc(ws, dtls);
	}

	if (!WSCONFIG(ws)->try_mtu)
		oclog(ws, LOG_DEBUG,
		      "Initializing MTU discovery; initial MTU: %u\n", mtu);

	ws->last_good_mtu = mtu;
	ws->last_bad_mtu = mtu;
}

static void mtu_ok(worker_st *ws, struct dtls_st *dtls)
{
	unsigned int c;

	if (WSCONFIG(ws)->try_mtu == 0 || ws->proto == AF_INET6)
		return;

	if (ws->last_bad_mtu == (ws->link_mtu) + 1 ||
	    ws->last_bad_mtu == (ws->link_mtu))
		return;

	ws->last_good_mtu = ws->link_mtu;
	c = (ws->link_mtu + ws->last_bad_mtu) / 2;

	link_mtu_set(ws, dtls, c);
}

#define FUZZ(x, diff, rnd)               \
	{                                \
		if (x > diff) {          \
			int16_t r = rnd; \
			x += r % diff;   \
		}                        \
	}

int get_pmtu_approx(worker_st *ws)
{
	socklen_t sl;
	int ret, e;

#if defined(__linux__) && defined(TCP_INFO)
	struct tcp_info ti;

	sl = sizeof(ti);

	ret = getsockopt(ws->conn_fd, IPPROTO_TCP, TCP_INFO, &ti, &sl);
	if (ret == -1) {
		e = errno;
		oclog(ws, LOG_INFO, "error in getting TCP_INFO: %s",
		      strerror(e));
		return -1;
	} else {
		return ti.tcpi_pmtu;
	}
#else
	int max = -1;

	sl = sizeof(max);
	ret = getsockopt(ws->conn_fd, IPPROTO_TCP, TCP_MAXSEG, &max, &sl);
	if (ret == -1) {
		e = errno;
		oclog(ws, LOG_INFO, "error in getting TCP_MAXSEG: %s",
		      strerror(e));
		return -1;
	} else {
		MSS_ADJUST(max);
		return max;
	}
#endif
}

static int periodic_check(worker_st *ws, struct timespec *tnow,
			  unsigned int dpd)
{
	int max, ret;
	time_t now = tnow->tv_sec;

	if (now - ws->last_periodic_check < PERIODIC_CHECK_TIME)
		return 0;

	/* we set an alarm at each periodic check to prevent any
	 * freezes in the worker due to an unexpected block (due to worker
	 * bug or kernel bug). In that case the worker will be killed due
	 * the alarm instead of hanging. */
	terminate_reason = REASON_SERVER_DISCONNECT;
	alarm(1800);

	if (WSCONFIG(ws)->idle_timeout > 0) {
		if (now - ws->last_nc_msg > WSCONFIG(ws)->idle_timeout) {
			oclog(ws, LOG_ERR,
			      "idle timeout reached for process (%d secs)",
			      (int)(now - ws->last_nc_msg));
			terminate = 1;
			terminate_reason = REASON_IDLE_TIMEOUT;
			goto cleanup;
		}
	}

	if (ws->user_config->session_timeout_secs > 0) {
		if (now - ws->session_start_time >
		    ws->user_config->session_timeout_secs) {
			oclog(ws, LOG_NOTICE,
			      "session timeout reached for process (%d secs)",
			      (int)(now - ws->session_start_time));
			terminate = 1;
			terminate_reason = REASON_SESSION_TIMEOUT;
			goto cleanup;
		}
	}

	if (ws->user_config->interim_update_secs > 0 &&
	    now - ws->last_stats_msg >= ws->user_config->interim_update_secs &&
	    ws->sid_set) {
		send_stats_to_secmod(ws, now, 0);
	}

#if defined(CAPTURE_LATENCY_SUPPORT)
	if (now - ws->latency.last_stats_msg >=
	    LATENCY_WORKER_AGGREGATION_TIME) {
		send_latency_stats_delta_to_main(ws, now);
	}
#endif

	/* check DPD. Otherwise exit */
	if (DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE &&
	    now - ws->last_msg_udp > DPD_TRIES * dpd && dpd > 0) {
		unsigned int data_mtu = DATA_MTU(ws, ws->link_mtu);

		oclog(ws, LOG_NOTICE,
		      "have not received any UDP message or DPD for long (%d secs, DPD is %u)",
		      (int)(now - ws->last_msg_udp), dpd);

		memset(ws->buffer + 1, 0, data_mtu);
		ws->buffer[0] = AC_PKT_DPD_OUT;

		ret = dtls_send(DTLS_ACTIVE(ws), ws->buffer, data_mtu + 1);
		DTLS_FATAL_ERR_CMD(ret, exit_worker_reason(ws, REASON_ERROR));

		if (now - ws->last_msg_udp > DPD_MAX_TRIES * dpd) {
			oclog(ws, LOG_ERR,
			      "have not received UDP message or DPD for very long; disabling UDP port");
			DTLS_ACTIVE(ws)->udp_state = UP_INACTIVE;
		}
	}
	if (dpd > 0 && now - ws->last_msg_tcp > DPD_TRIES * dpd) {
		oclog(ws, LOG_NOTICE,
		      "have not received TCP DPD for long (%d secs)",
		      (int)(now - ws->last_msg_tcp));
		ws->buffer[0] = 'S';
		ws->buffer[1] = 'T';
		ws->buffer[2] = 'F';
		ws->buffer[3] = 1;
		ws->buffer[4] = 0;
		ws->buffer[5] = 0;
		ws->buffer[6] = AC_PKT_DPD_OUT;
		ws->buffer[7] = 0;

		ret = cstp_send(ws, ws->buffer, 8);
		CSTP_FATAL_ERR_CMD(ws, ret,
				   exit_worker_reason(ws, REASON_ERROR));

		if (now - ws->last_msg_tcp > DPD_MAX_TRIES * dpd) {
			oclog(ws, LOG_NOTICE,
			      "connection timeout (DPD); tearing down connection");
			exit_worker_reason(ws, REASON_DPD_TIMEOUT);
		}
	}

	if (ws->conn_type != SOCK_TYPE_UNIX &&
	    DTLS_ACTIVE(ws)->udp_state != UP_DISABLED) {
		max = get_pmtu_approx(ws);
		if (max > 0 && max < ws->link_mtu) {
			oclog(ws, LOG_DEBUG,
			      "reducing MTU due to TCP/PMTU to %u", max);
			link_mtu_set(ws, DTLS_ACTIVE(ws), max);
		}
	}

cleanup:
	ws->last_periodic_check = now;

	return 0;
}

/* Disable any TCP queuing on the TLS port. This allows a connection that works over
 * TCP instead of UDP to still be interactive.
 */
static void set_no_delay(worker_st *ws, int fd)
{
	int flag = 1;
	int ret;

	ret = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
	if (ret == -1)
		oclog(ws, LOG_DEBUG, "setsockopt(TCP_NODELAY) to %x, failed.",
		      (unsigned int)flag);
}

#define TOSCLASS(x) (IPTOS_CLASS_CS##x)

static void set_net_priority(worker_st *ws, int fd, int priority)
{
	int t;
	int ret;
#if defined(IP_TOS)
	if (priority != 0 && IS_TOS(priority)) {
		t = TOS_UNPACK(priority);
		ret = setsockopt(fd, IPPROTO_IP, IP_TOS, &t, sizeof(t));
		if (ret == -1)
			oclog(ws, LOG_DEBUG,
			      "setsockopt(IP_TOS) to %x, failed.",
			      (unsigned int)t);

		return;
	}
#endif

#if defined(SO_PRIORITY)
	if (priority != 0 && priority <= 7) {
		t = ws->user_config->net_priority - 1;
		ret = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &t, sizeof(t));
		if (ret == -1)
			oclog(ws, LOG_DEBUG,
			      "setsockopt(SO_PRIORITY) to %d, failed.", t);

		return;
	}
#endif
}

#define SEND_ERR(x)                      \
	{                                \
		if (x < 0)               \
			goto send_error; \
	}

static int dtls_mainloop(worker_st *ws, struct dtls_st *dtls,
			 struct timespec *tnow)
{
	int ret;
	gnutls_datum_t data;
	void *packet = NULL;

	switch (dtls->udp_state) {
	case UP_ACTIVE:
	case UP_INACTIVE:
		ret = dtls_recv_packet(dtls, &data, &packet);
		oclog(ws, LOG_TRANSFER_DEBUG, "received %d byte(s) (DTLS)",
		      ret);

		DTLS_FATAL_ERR_CMD(ret, exit_worker_reason(ws, REASON_ERROR));

		if (ret == GNUTLS_E_REHANDSHAKE) {
			if (dtls->last_dtls_rehandshake > 0 &&
			    tnow->tv_sec - dtls->last_dtls_rehandshake <
				    WSCONFIG(ws)->rekey_time / 2) {
				oclog(ws, LOG_INFO,
				      "client requested DTLS rehandshake too soon");
				ret = -1;
				goto cleanup;
			}

			/* there is not much we can rehandshake on the DTLS channel,
			 * at least not the way AnyConnect sets it up.
			 */
			oclog(ws, LOG_DEBUG,
			      "client requested rehandshake on DTLS channel");

			do {
				ret = gnutls_handshake(dtls->dtls_session);
			} while (ret == GNUTLS_E_AGAIN ||
				 ret == GNUTLS_E_INTERRUPTED);

			GNUTLS_ALERT_PRINT(ws, dtls->dtls_session, ret);
			DTLS_FATAL_ERR_CMD(
				ret, exit_worker_reason(ws, REASON_ERROR));
			oclog(ws, LOG_DEBUG, "DTLS rehandshake completed");

			dtls->last_dtls_rehandshake = tnow->tv_sec;
		} else if (ret >= 1) {
			/* where we receive any DTLS UDP packet we reset the state
			 * to active */
			dtls->udp_state = UP_ACTIVE;

			if (bandwidth_update(&ws->b_rx,
					     data.size - CSTP_DTLS_OVERHEAD,
					     tnow) != 0) {
				ret = parse_dtls_data(ws, data.data, data.size,
						      tnow->tv_sec);
				if (ret < 0) {
					oclog(ws, LOG_INFO,
					      "error parsing DTLS data");
					goto cleanup;
				}
			}
		} else
			oclog(ws, LOG_TRANSFER_DEBUG, "no data received (%d)",
			      ret);

		ws->udp_recv_time = tnow->tv_sec;
		break;
	case UP_SETUP:
		ret = setup_dtls_connection(ws, dtls);
		if (ret < 0) {
			ret = -1;
			goto cleanup;
		}

		gnutls_dtls_set_mtu(dtls->dtls_session,
				    ws->link_mtu - ws->dtls_proto_overhead);
		mtu_discovery_init(ws, dtls, ws->link_mtu);
		break;

	case UP_HANDSHAKE:
hsk_restart:
		ret = gnutls_handshake(dtls->dtls_session);
		if (ret < 0 && gnutls_error_is_fatal(ret) != 0) {
			if (ret == GNUTLS_E_FATAL_ALERT_RECEIVED)
				oclog(ws, LOG_ERR,
				      "error in DTLS handshake: %s: %s\n",
				      gnutls_strerror(ret),
				      gnutls_alert_get_name(gnutls_alert_get(
					      dtls->dtls_session)));
			else
				oclog(ws, LOG_ERR,
				      "error in DTLS handshake: %s\n",
				      gnutls_strerror(ret));
			dtls->udp_state = UP_DISABLED;
			ev_io_stop(worker_loop, &dtls->io);
			break;
		}

		if (ret == GNUTLS_E_LARGE_PACKET) {
			/* adjust mtu */
			mtu_not_ok(ws, dtls);
			goto hsk_restart;
		} else if (ret == 0) {
			unsigned int data_mtu;

			/* gnutls_dtls_get_data_mtu() already subtracts the crypto overhead */
			data_mtu =
				gnutls_dtls_get_data_mtu(dtls->dtls_session) -
				CSTP_DTLS_OVERHEAD;

			dtls->udp_state = UP_ACTIVE;
			oclog(ws, LOG_DEBUG,
			      "DTLS handshake completed (link MTU: %u, data MTU: %u)\n",
			      ws->link_mtu, data_mtu);
			ws->dtls_active_session++;
			oclog(ws, LOG_DEBUG,
			      "Main DTLS session %" PRIu8 " active",
			      ws->dtls_active_session);
			session_info_send(ws);
		}

		break;
	default:
		break;
	}

	ret = 0;
cleanup:
	packet_deinit(packet);
	return ret;
}

static int tls_mainloop(struct worker_st *ws, struct timespec *tnow)
{
	int ret;
	gnutls_datum_t data;
	void *packet = NULL;

	ret = cstp_recv_packet(ws, &data, &packet);
	if (ret == GNUTLS_E_PREMATURE_TERMINATION) {
		oclog(ws, LOG_DEBUG, "client disconnected prematurely");
		ret = -1;
		goto cleanup;
	}

	CSTP_FATAL_ERR_CMD(ws, ret, exit_worker_reason(ws, REASON_ERROR));

	if (ret == 0) { /* disconnect */
		oclog(ws, LOG_DEBUG, "client disconnected");
		ret = -1;
		goto cleanup;
	} else if (ret >= 8) {
		oclog(ws, LOG_TRANSFER_DEBUG, "received %d byte(s) (TLS)",
		      data.size);

		if (bandwidth_update(&ws->b_rx, data.size - 8, tnow) != 0) {
			ret = parse_cstp_data(ws, data.data, data.size,
					      tnow->tv_sec);
			if (ret < 0) {
				oclog(ws, LOG_ERR, "error parsing CSTP data");
				goto cleanup;
			}

			if ((ret == AC_PKT_DATA || ret == AC_PKT_COMPRESSED) &&
			    DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE) {
				/* client switched to TLS for some reason */
				if (tnow->tv_sec - ws->udp_recv_time >
				    UDP_SWITCH_TIME)
					DTLS_ACTIVE(ws)->udp_state =
						UP_INACTIVE;
			}
		}

	} else if (ret == GNUTLS_E_REHANDSHAKE) {
		/* rekey? */
		if (ws->last_tls_rehandshake > 0 &&
		    tnow->tv_sec - ws->last_tls_rehandshake <
			    WSCONFIG(ws)->rekey_time / 2) {
			oclog(ws, LOG_INFO,
			      "client requested TLS rehandshake too soon");
			ret = -1;
			goto cleanup;
		}

		oclog(ws, LOG_INFO,
		      "client requested rehandshake on TLS channel");
		do {
			ret = gnutls_handshake(ws->session);
		} while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
		GNUTLS_ALERT_PRINT(ws, ws->session, ret);
		DTLS_FATAL_ERR_CMD(ret, exit_worker_reason(ws, REASON_ERROR));

		ws->last_tls_rehandshake = tnow->tv_sec;
		oclog(ws, LOG_INFO, "TLS rehandshake completed");
	}

	ret = 0;
cleanup:
	packet_deinit(packet);
	return ret;
}

/* Returns true if the data provided are not IP control messages
 * (ICMP, IGMP). */
static bool is_data(const uint8_t *data, size_t size)
{
	if (size > 20) {
		uint8_t version = data[0] >> 4;

		if (version == 0x04) {
			if (data[9] == 0x01 || data[9] == 0x02) /* ICMP/IGMP */
				return 0;
		} else if (version == 0x06) {
			if (data[9] == 0x3A || data[9] == 0x80)
				return 0;
		}
	}
	return 1;
}

static int tun_mainloop(struct worker_st *ws, struct timespec *tnow)
{
	int ret, l, e;
	unsigned int tls_retry;
	int dtls_type = AC_PKT_DATA;
	int cstp_type = AC_PKT_DATA;
	gnutls_datum_t dtls_to_send;
	gnutls_datum_t cstp_to_send;

	l = tun_read(ws->tun_fd, ws->buffer + 8, DATA_MTU(ws, ws->link_mtu));
	if (l < 0) {
		e = errno;

		if (e != EAGAIN && e != EINTR) {
			oclog(ws, LOG_ERR,
			      "received corrupt data from tun (%d): %s", l,
			      strerror(e));
			return -1;
		}

		return 0;
	}

	if (l == 0) {
		oclog(ws, LOG_INFO, "TUN device returned zero");
		return 0;
	}

	dtls_to_send.data = ws->buffer;
	dtls_to_send.size = l;

	cstp_to_send.data = ws->buffer;
	cstp_to_send.size = l;

	if (WSCONFIG(ws)->switch_to_tcp_timeout &&
	    DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE &&
	    tnow->tv_sec >
		    ws->udp_recv_time + WSCONFIG(ws)->switch_to_tcp_timeout) {
		oclog(ws, LOG_DEBUG,
		      "No UDP data received for %li seconds, using TCP instead\n",
		      tnow->tv_sec - ws->udp_recv_time);
		DTLS_ACTIVE(ws)->udp_state = UP_INACTIVE;
	}

#ifdef ENABLE_COMPRESSION
	if (DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE &&
	    ws->dtls_selected_comp != NULL &&
	    l > WSCONFIG(ws)->no_compress_limit) {
		/* otherwise don't compress */
		ret = ws->dtls_selected_comp->compress(ws->decomp + 8,
						       sizeof(ws->decomp) - 8,
						       ws->buffer + 8, l);
		oclog(ws, LOG_TRANSFER_DEBUG, "compressed %d to %d\n", (int)l,
		      ret);
		if (ret > 0 && ret < l) {
			dtls_to_send.data = ws->decomp;
			dtls_to_send.size = ret;
			dtls_type = AC_PKT_COMPRESSED;

			if (ws->cstp_selected_comp) {
				if (ws->cstp_selected_comp->id ==
				    ws->dtls_selected_comp->id) {
					cstp_to_send.data = ws->decomp;
					cstp_to_send.size = ret;
					cstp_type = AC_PKT_COMPRESSED;
				}
			}
		}
	} else if (ws->cstp_selected_comp != NULL &&
		   l > WSCONFIG(ws)->no_compress_limit) {
		/* otherwise don't compress */
		ret = ws->cstp_selected_comp->compress(ws->decomp + 8,
						       sizeof(ws->decomp) - 8,
						       ws->buffer + 8, l);
		oclog(ws, LOG_TRANSFER_DEBUG, "compressed %d to %d\n", (int)l,
		      ret);
		if (ret > 0 && ret < l) {
			cstp_to_send.data = ws->decomp;
			cstp_to_send.size = ret;
			cstp_type = AC_PKT_COMPRESSED;
		}
	}
#endif

	/* only transmit if allowed */
	if (bandwidth_update(&ws->b_tx, dtls_to_send.size, tnow) != 0) {
		tls_retry = 0;

		oclog(ws, LOG_TRANSFER_DEBUG, "sending %d byte(s)\n", l);

		if (DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE) {
			ws->tun_bytes_out += dtls_to_send.size;

			dtls_to_send.data[7] = dtls_type;
			ret = dtls_send(DTLS_ACTIVE(ws), dtls_to_send.data + 7,
					dtls_to_send.size + 1);
			DTLS_FATAL_ERR_CMD(
				ret, exit_worker_reason(ws, REASON_ERROR));

			if (ret == GNUTLS_E_LARGE_PACKET) {
				mtu_not_ok(ws, DTLS_ACTIVE(ws));

				oclog(ws, LOG_TRANSFER_DEBUG,
				      "retrying (TLS) %d\n", l);
				tls_retry = 1;
			} else if (ret >= 1 + DATA_MTU(ws, ws->link_mtu) &&
				   WSCONFIG(ws)->try_mtu != 0) {
				mtu_ok(ws, DTLS_ACTIVE(ws));
			}
		}

		if (DTLS_ACTIVE(ws)->udp_state != UP_ACTIVE || tls_retry != 0) {
			cstp_to_send.data[0] = 'S';
			cstp_to_send.data[1] = 'T';
			cstp_to_send.data[2] = 'F';
			cstp_to_send.data[3] = 1;
			cstp_to_send.data[4] = cstp_to_send.size >> 8;
			cstp_to_send.data[5] = cstp_to_send.size & 0xff;
			cstp_to_send.data[6] = cstp_type;
			cstp_to_send.data[7] = 0;

			ws->tun_bytes_out += cstp_to_send.size;

			ret = cstp_send(ws, cstp_to_send.data,
					cstp_to_send.size + 8);
			CSTP_FATAL_ERR_CMD(
				ws, ret, exit_worker_reason(ws, REASON_ERROR));
		}

		if (is_data(ws->buffer + 8, l)) /* do not account ICMP */
			ws->last_nc_msg = tnow->tv_sec;
	}

	return 0;
}

static char *replace_vals(worker_st *ws, const char *txt)
{
	str_st str;
	int ret;
	str_rep_tab tab[3];

	STR_TAB_INIT(tab, sizeof(tab));
	STR_TAB_SET(0, "%{U}", ws->username);
	STR_TAB_SET(1, "%{G}", ws->groupname);
	STR_TAB_TERM(2);

	str_init(&str, ws);

	ret = str_append_str(&str, txt);
	if (ret < 0)
		return NULL;

	ret = str_replace_str(&str, tab);
	if (ret < 0) {
		str_clear(&str);
		return NULL;
	}

	return (char *)str.data;
}

static int send_routes(worker_st *ws, struct http_req_st *req, char **routes,
		       unsigned int routes_size, bool include)
{
	unsigned int i;
	unsigned int ip6;
	const char *txt;
	int ret;

	if (include)
		txt = "Include";
	else
		txt = "Exclude";

	for (i = 0; i < routes_size; i++) {
		if (strchr(routes[i], ':') != 0)
			ip6 = 1;
		else
			ip6 = 0;

		if (req->no_ipv6 != 0 && ip6 != 0)
			continue;
		if (req->no_ipv4 != 0 && ip6 == 0)
			continue;
		oclog(ws, LOG_DEBUG, "%s route %s", txt, routes[i]);

		if (ip6 != 0 && ws->full_ipv6) {
			ret = cstp_printf(ws, "X-CSTP-Split-%s-IP6: %s\r\n",
					  txt, routes[i]);
		} else {
			ret = cstp_printf(ws, "X-CSTP-Split-%s: %s\r\n", txt,
					  routes[i]);
		}
		if (ret < 0)
			return ret;
	}
	return 0;
}

/* Enforces a socket timeout. That is because, although we
 * use poll() to see whether a call to recv() would block,
 * there are certain cases in Linux where recv() blocks even
 * though poll() notified of data */
static void set_socket_timeout(worker_st *ws, int fd)
{
	struct timeval tval;
	int ret;

	tval.tv_sec = DEFAULT_SOCKET_TIMEOUT;
	tval.tv_usec = 0;
	ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tval, sizeof(tval));
	if (ret == -1) {
		int e = errno;

		oclog(ws, LOG_DEBUG, "setsockopt(%s, SO_RCVTIMEO) failed: %s",
		      (fd == ws->conn_fd) ? "TCP" : "UDP", strerror(e));
	}
}

/* wild but conservative guess; this ciphersuite has the largest overhead */
#define MAX_CSTP_CRYPTO_OVERHEAD                                     \
	(CSTP_OVERHEAD + tls_get_overhead(GNUTLS_TLS1_0,             \
					  GNUTLS_CIPHER_AES_128_CBC, \
					  GNUTLS_MAC_SHA1))
#define MAX_DTLS_CRYPTO_OVERHEAD                                          \
	(CSTP_DTLS_OVERHEAD + tls_get_overhead(GNUTLS_DTLS1_0,            \
					       GNUTLS_CIPHER_AES_128_CBC, \
					       GNUTLS_MAC_SHA1))
#define MAX_DTLS_PROTO_OVERHEAD(ws)                                    \
	((ws->proto == AF_INET) ? (IP_HEADER_SIZE + UDP_HEADER_SIZE) : \
				  (IPV6_HEADER_SIZE + UDP_HEADER_SIZE))

/* Calculate MTU for CSTP and DTLS channels.
 */
static void calc_mtu_values(worker_st *ws)
{
	/* assume that if IPv6 is used over TCP then the same would be used over UDP */
	if (ws->proto == AF_INET) {
		ws->cstp_proto_overhead = IP_HEADER_SIZE;
		ws->dtls_proto_overhead = IP_HEADER_SIZE;
	} else {
		ws->cstp_proto_overhead = IPV6_HEADER_SIZE;
		ws->dtls_proto_overhead = IPV6_HEADER_SIZE;
	}
	ws->cstp_proto_overhead += TCP_HEADER_SIZE;
	ws->dtls_proto_overhead += UDP_HEADER_SIZE;

	if (ws->session == NULL) {
		ws->cstp_crypto_overhead = MAX_CSTP_CRYPTO_OVERHEAD;
	} else {
		ws->cstp_crypto_overhead =
			CSTP_OVERHEAD +
			tls_get_overhead(
				gnutls_protocol_get_version(ws->session),
				gnutls_cipher_get(ws->session),
				gnutls_mac_get(ws->session));
	}

	/* link MTU is the device MTU */
	ws->link_mtu = ws->vinfo.mtu;

	if (DTLS_ACTIVE(ws)->udp_state != UP_DISABLED) {
		/* crypto overhead for DTLS */
		if (ws->req.use_psk) {
			if (ws->session == NULL) {
				ws->dtls_crypto_overhead =
					MAX_DTLS_CRYPTO_OVERHEAD;
			} else {
				ws->dtls_crypto_overhead = tls_get_overhead(
					GNUTLS_DTLS1_0,
					gnutls_cipher_get(ws->session),
					gnutls_mac_get(ws->session));
			}
		} else if (ws->req.selected_ciphersuite) {
			ws->dtls_crypto_overhead = tls_get_overhead(
				ws->req.selected_ciphersuite->gnutls_version,
				ws->req.selected_ciphersuite->gnutls_cipher,
				ws->req.selected_ciphersuite->gnutls_mac);
		}
		ws->dtls_crypto_overhead += CSTP_DTLS_OVERHEAD;

		oclog(ws, LOG_DEBUG, "DTLS overhead is %u",
		      ws->dtls_proto_overhead + ws->dtls_crypto_overhead);
	}

	/* This is the data MTU we advertised to peer, we will never exceed this value */
	ws->adv_link_mtu = ws->link_mtu;
}

/* connect_handler:
 * @ws: an initialized worker structure
 *
 * This function handles the HTTPS session after a CONNECT
 * command has been issued by the peer. The @ws->auth_state
 * should be set to %S_AUTH_COMPLETE or the client will be
 * disconnected.
 *
 * If the user is authenticate it handles the TCP and UDP VPN
 * tunnels.
 *
 */
static int connect_handler(worker_st *ws)
{
	struct http_req_st *req = &ws->req;
	int max, ret, t;
	char *p;
	unsigned int rnd;
	unsigned int i;
	unsigned int ip6;
	time_t now = time(NULL);

	ret = gnutls_rnd(GNUTLS_RND_NONCE, &rnd, sizeof(rnd));
	if (ret < 0) {
		oclog(ws, LOG_ERR, "error in the random generator: %s",
		      gnutls_strerror(ret));
		cstp_puts(ws, "HTTP/1.1 503 Service Unavailable\r\n");
		cstp_puts(ws, "X-Reason: Server error\r\n\r\n");
		return -1;
	}

	ws->buffer_size = sizeof(ws->buffer);

	cookie_authenticate_or_exit(ws);

	/* The Clavister Android VPN client has a defect and
	 * asks for CSCOSSLC/tunnel instead of /CSCOSSLC/tunnel */
	if (strcmp(req->url, "/CSCOSSLC/tunnel") != 0 &&
	    strcmp(req->url, "CSCOSSLC/tunnel") != 0) {
		oclog(ws, LOG_INFO, "bad connect request: '%s'\n", req->url);
		response_404(ws, 1);
		cstp_fatal_close(ws, GNUTLS_A_ACCESS_DENIED);
		exit_worker(ws);
	}

	if (WSCONFIG(ws)->network.name[0] == 0) {
		oclog(ws, LOG_ERR,
		      "no networks are configured; rejecting client");
		cstp_puts(ws, "HTTP/1.1 503 Service Unavailable\r\n");
		cstp_puts(ws, "X-Reason: Server configuration error\r\n\r\n");
		return -1;
	}

	ret = complete_vpn_info(ws, &ws->vinfo);
	if (ret < 0) {
		oclog(ws, LOG_ERR,
		      "no networks are configured; rejecting client");
		cstp_puts(ws, "HTTP/1.1 503 Service Unavailable\r\n");
		cstp_puts(ws, "X-Reason: Server configuration error\r\n\r\n");
		return -1;
	}

	/* override any hostname sent by the peer if we have one already configured */
	if (ws->user_config->hostname) {
		strlcpy(ws->req.hostname, ws->user_config->hostname,
			sizeof(ws->req.hostname));
	}

	FUZZ(WSCONFIG(ws)->rekey_time, 30, rnd);

	/* Connected. Turn of the alarm */
	if (WSCONFIG(ws)->auth_timeout)
		alarm(0);
	http_req_deinit(ws);

	cstp_cork(ws);
	ret = cstp_puts(ws, "HTTP/1.1 200 CONNECTED\r\n");
	SEND_ERR(ret);

	ret = add_owasp_headers(ws);
	SEND_ERR(ret);

	ret = cstp_puts(ws, "X-CSTP-Version: 1\r\n");
	SEND_ERR(ret);

	ret = cstp_puts(ws, "X-CSTP-Server-Name: " PACKAGE_NAME "\r\n");
	SEND_ERR(ret);

	if (req->is_mobile) {
		ws->user_config->dpd = ws->user_config->mobile_dpd;
		WSCONFIG(ws)->idle_timeout = WSCONFIG(ws)->mobile_idle_timeout;
	}

	/* Notify back the client about the accepted hostname */
	if (ws->req.hostname[0] != 0) {
		ret = cstp_printf(ws, "X-CSTP-Hostname: %s\r\n",
				  ws->req.hostname);
		SEND_ERR(ret);
	}

	oclog(ws, LOG_INFO, "suggesting DPD of %d secs", ws->user_config->dpd);
	if (ws->user_config->dpd > 0) {
		ret = cstp_printf(ws, "X-CSTP-DPD: %" PRIu32 "\r\n",
				  ws->user_config->dpd);
		SEND_ERR(ret);
	}

	if (WSCONFIG(ws)->default_domain) {
		ret = cstp_printf(ws, "X-CSTP-Default-Domain: %s\r\n",
				  WSCONFIG(ws)->default_domain);
		SEND_ERR(ret);
	}

	ws->dtls_active_session = 0;
	DTLS_ACTIVE(ws)->udp_state = UP_DISABLED;
	DTLS_INACTIVE(ws)->udp_state = UP_DISABLED;

	if (WSPCONFIG(ws)->udp_port != 0 && req->master_secret_set != 0) {
		memcpy(ws->master_secret, req->master_secret, TLS_MASTER_SIZE);
		DTLS_ACTIVE(ws)->udp_state = UP_WAIT_FD;
		DTLS_INACTIVE(ws)->udp_state = UP_WAIT_FD;
	} else {
		oclog(ws, LOG_DEBUG, "disabling UDP (DTLS) connection");
	}

	if (ws->user_config->mtu > 0)
		ws->vinfo.mtu = ws->user_config->mtu;
	oclog(ws, LOG_INFO, "configured link MTU is %u", ws->vinfo.mtu);

	if (req->link_mtu > 0) {
		oclog(ws, LOG_INFO, "peer's link MTU is %u", req->link_mtu);
		ws->vinfo.mtu = MIN(ws->vinfo.mtu, req->link_mtu);
	} else if (req->tunnel_mtu > 0) {
		/* Old clients didn't send their link MTU, they send the plaintext MTU
		 * they can transfer. */
		ws->vinfo.mtu =
			MIN(ws->vinfo.mtu, req->tunnel_mtu +
						   MAX_DTLS_PROTO_OVERHEAD(ws) +
						   MAX_DTLS_CRYPTO_OVERHEAD);
		oclog(ws, LOG_INFO, "peer's data MTU is %u / link is %u",
		      req->tunnel_mtu, ws->vinfo.mtu);
	}

	/* Attempt to use the TCP connection maximum segment size to set a more
	 * precise MTU. */
	if (ws->conn_type != SOCK_TYPE_UNIX) {
		max = get_pmtu_approx(ws);
		if (max > 0 && max < ws->vinfo.mtu) {
			oclog(ws, LOG_DEBUG,
			      "reducing MTU due to TCP/PMTU to %u", max);
			link_mtu_set(ws, DTLS_ACTIVE(ws), max);
		}
	}

	calc_mtu_values(ws);

	if (DATA_MTU(ws, ws->link_mtu) < 1280 && ws->vinfo.ipv6 &&
	    req->no_ipv6 == 0) {
		oclog(ws, LOG_INFO,
		      "Connection MTU (link: %u, data: %u) is not sufficient for IPv6 (1280)",
		      ws->link_mtu, DATA_MTU(ws, ws->link_mtu));
		req->no_ipv6 = 1;
	}

	/* Send IP addresses */
	if (ws->vinfo.ipv4 && req->no_ipv4 == 0) {
		oclog(ws, LOG_NOTICE, "sending IPv4 %s", ws->vinfo.ipv4);
		ret = cstp_printf(ws, "X-CSTP-Address: %s\r\n", ws->vinfo.ipv4);
		SEND_ERR(ret);

		if (ws->user_config->ipv4_netmask) {
			ret = cstp_printf(ws, "X-CSTP-Netmask: %s\r\n",
					  ws->user_config->ipv4_netmask);
			SEND_ERR(ret);
		}
	}

	if (ws->vinfo.ipv6 && req->no_ipv6 == 0 &&
	    ws->user_config->ipv6_prefix != 0) {
		oclog(ws, LOG_NOTICE, "sending IPv6 %s/%u", ws->vinfo.ipv6,
		      ws->user_config->ipv6_subnet_prefix);
		if (ws->full_ipv6 && ws->user_config->ipv6_subnet_prefix) {
			ret = cstp_printf(ws, "X-CSTP-Address-IP6: %s/%u\r\n",
					  ws->vinfo.ipv6,
					  ws->user_config->ipv6_subnet_prefix);
			SEND_ERR(ret);
		} else {
			const char *net;

			ret = cstp_printf(ws, "X-CSTP-Address: %s\r\n",
					  ws->vinfo.ipv6);
			SEND_ERR(ret);

			net = ws->user_config->ipv6_net;
			if (net == NULL)
				net = ws->vinfo.ipv6;

			ret = cstp_printf(ws, "X-CSTP-Netmask: %s/%u\r\n", net,
					  ws->user_config->ipv6_subnet_prefix);
			SEND_ERR(ret);
		}
	}

	if (ws->full_ipv6 == 0) {
		req->no_ipv6 = 1;
		oclog(ws, LOG_INFO,
		      "IPv6 routes/DNS disabled because IPv6 support was not requested.");
	} else {
		switch (req->user_agent_type) {
		case AGENT_OPENCONNECT_V3:
			req->no_ipv6 = 1;
			oclog(ws, LOG_INFO,
			      "IPv6 routes/DNS disabled because the agent is known not to support them.");
			break;
		case AGENT_OPENCONNECT:
		case AGENT_ANYCONNECT:
		case AGENT_OPENCONNECT_CLAVISTER:
		case AGENT_ANYLINK:
			oclog(ws, LOG_DEBUG,
			      "Enabling IPv6 routes/DNS because the agent is known to support them.");
			break;
		case AGENT_UNKNOWN:
		default:
			oclog(ws, LOG_NOTICE,
			      "Enabling IPv6 routes/DNS although the agent is unknown.");
			break;
		}
	}

	for (i = 0; i < ws->user_config->n_dns; i++) {
		if (strchr(ws->user_config->dns[i], ':') != 0)
			ip6 = 1;
		else
			ip6 = 0;

		if (req->no_ipv6 != 0 && ip6 != 0)
			continue;
		if (req->no_ipv4 != 0 && ip6 == 0)
			continue;

		oclog(ws, LOG_INFO, "adding DNS %s", ws->user_config->dns[i]);
		if (req->user_agent_type == AGENT_ANYCONNECT) {
			ret = cstp_printf(ws, "X-CSTP-%s: %s\r\n",
					  ip6 ? "DNS-IP6" : "DNS",
					  ws->user_config->dns[i]);
		} else { /* openconnect does not require the split
			  * of DNS and DNS-IP6 and only recent versions
			  * understand the IP6 variant. */
			ret = cstp_printf(ws, "X-CSTP-DNS: %s\r\n",
					  ws->user_config->dns[i]);
		}
		SEND_ERR(ret);
	}

	for (i = 0; i < ws->user_config->n_nbns; i++) {
		if (strchr(ws->user_config->nbns[i], ':') != 0)
			ip6 = 1;
		else
			ip6 = 0;

		if (req->no_ipv6 != 0 && ip6 != 0)
			continue;
		if (req->no_ipv4 != 0 && ip6 == 0)
			continue;

		oclog(ws, LOG_INFO, "adding NBNS %s", ws->user_config->nbns[i]);
		ret = cstp_printf(ws, "X-CSTP-NBNS: %s\r\n",
				  ws->user_config->nbns[i]);
		SEND_ERR(ret);
	}

	for (i = 0; i < ws->user_config->n_split_dns; i++) {
		if (strchr(ws->user_config->split_dns[i], ':') != 0)
			ip6 = 1;
		else
			ip6 = 0;

		if (req->no_ipv6 != 0 && ip6 != 0)
			continue;
		if (req->no_ipv4 != 0 && ip6 == 0)
			continue;

		oclog(ws, LOG_INFO, "adding split DNS %s",
		      ws->user_config->split_dns[i]);
		ret = cstp_printf(ws, "X-CSTP-Split-DNS: %s\r\n",
				  ws->user_config->split_dns[i]);
		SEND_ERR(ret);
	}

	/* Anyconnect on IOS requires this route in order to use IPv6 */
	if (ws->full_ipv6 && req->is_ios &&
	    (ws->user_config->n_routes == 0 || ws->default_route == 0)) {
		oclog(ws, LOG_INFO, "adding special split DNS for Apple");
		ret = cstp_printf(ws, "X-CSTP-Split-Include-IP6: 2000::/3\r\n");
		SEND_ERR(ret);
	}

	if (ws->default_route == 0) {
		ret = send_routes(ws, req, ws->user_config->routes,
				  ws->user_config->n_routes, 1);
		SEND_ERR(ret);

	} else {
		/* default route */
		WSCONFIG(ws)->tunnel_all_dns = 1;
	}

	if (WSCONFIG(ws)->tunnel_all_dns) {
		ret = cstp_puts(ws, "X-CSTP-Tunnel-All-DNS: true\r\n");
	} else {
		ret = cstp_puts(ws, "X-CSTP-Tunnel-All-DNS: false\r\n");
	}
	SEND_ERR(ret);

	if (WSCONFIG(ws)->client_bypass_protocol) {
		ret = cstp_puts(ws, "X-CSTP-Client-Bypass-Protocol: true\r\n");
	} else {
		ret = cstp_puts(ws, "X-CSTP-Client-Bypass-Protocol: false\r\n");
	}
	SEND_ERR(ret);

	ret = send_routes(ws, req, ws->user_config->no_routes,
			  ws->user_config->n_no_routes, 0);
	SEND_ERR(ret);

	ret = cstp_printf(ws, "X-CSTP-Keepalive: %u\r\n",
			  ws->user_config->keepalive);
	SEND_ERR(ret);

	if (WSCONFIG(ws)->idle_timeout > 0) {
		ret = cstp_printf(ws, "X-CSTP-Idle-Timeout: %u\r\n",
				  (unsigned int)WSCONFIG(ws)->idle_timeout);
	} else {
		ret = cstp_puts(ws, "X-CSTP-Idle-Timeout: none\r\n");
	}
	SEND_ERR(ret);

	ret = cstp_puts(ws, "X-CSTP-Smartcard-Removal-Disconnect: true\r\n");
	SEND_ERR(ret);

	if (WSCONFIG(ws)->is_dyndns != 0) {
		ret = cstp_puts(ws, "X-CSTP-DynDNS: true\r\n");
		SEND_ERR(ret);
	}

	if (WSCONFIG(ws)->rekey_time > 0) {
		unsigned int method;

		ret = cstp_printf(ws, "X-CSTP-Rekey-Time: %u\r\n",
				  (unsigned int)(WSCONFIG(ws)->rekey_time));
		SEND_ERR(ret);

		/* if the peer isn't patched for safe renegotiation, always
		 * require him to open a new tunnel. */
		if (ws->session != NULL &&
		    gnutls_safe_renegotiation_status(ws->session) != 0)
			method = WSCONFIG(ws)->rekey_method;
		else
			method = REKEY_METHOD_NEW_TUNNEL;

		ret = cstp_printf(ws, "X-CSTP-Rekey-Method: %s\r\n",
				  (method == REKEY_METHOD_SSL) ? "ssl" :
								 "new-tunnel");
		SEND_ERR(ret);
	} else {
		ret = cstp_puts(ws, "X-CSTP-Rekey-Method: none\r\n");
		SEND_ERR(ret);
	}

	if (WSCONFIG(ws)->proxy_url != NULL) {
		char *url = replace_vals(ws, WSCONFIG(ws)->proxy_url);

		if (url != NULL) {
			ret = cstp_printf(
				ws, "X-CSTP-MSIE-Proxy-Pac-URL: %s\r\n", url);
			SEND_ERR(ret);
			talloc_free(url);
		}
	}

	if (!ws->user_config->has_session_timeout_secs) {
		ret = cstp_puts(ws, "X-CSTP-Session-Timeout: none\r\n");
		SEND_ERR(ret);
	} else {
		time_t expiration = ws->session_start_time +
				    ws->user_config->session_timeout_secs;

		ret = cstp_printf(ws,
				  "X-CSTP-Session-Timeout: %u\r\n"
				  "X-CSTP-Session-Timeout-Remaining: %ld\r\n",
				  ws->user_config->session_timeout_secs,
				  MAX(expiration - now, 0));
		SEND_ERR(ret);
	}

	ret = cstp_puts(ws, "X-CSTP-Disconnected-Timeout: none\r\n"
			    "X-CSTP-Keep: true\r\n"
			    "X-CSTP-TCP-Keepalive: true\r\n"
			    "X-CSTP-License: accept\r\n");
	SEND_ERR(ret);

	for (i = 0; i < WSCONFIG(ws)->custom_header_size; i++) {
		char *h = replace_vals(ws, WSCONFIG(ws)->custom_header[i]);

		if (h) {
			oclog(ws, LOG_INFO, "adding custom header '%s'", h);
			ret = cstp_printf(ws, "%s\r\n", h);
			SEND_ERR(ret);
			talloc_free(h);
		}
	}

	/* set TCP socket options */
	if (WSCONFIG(ws)->output_buffer > 0) {
		t = ws->link_mtu;
		t *= WSCONFIG(ws)->output_buffer;

		ret = setsockopt(ws->conn_fd, SOL_SOCKET, SO_SNDBUF, &t,
				 sizeof(t));
		if (ret == -1)
			oclog(ws, LOG_DEBUG,
			      "setsockopt(TCP, SO_SNDBUF) to %u, failed.", t);
	}

	set_socket_timeout(ws, ws->conn_fd);
	set_non_block(ws->conn_fd);
	set_net_priority(ws, ws->conn_fd, ws->user_config->net_priority);
	set_no_delay(ws, ws->conn_fd);

	if (DTLS_ACTIVE(ws)->udp_state != UP_DISABLED) {
		if (ws->user_config->dpd > 0) {
			ret = cstp_printf(ws, "X-DTLS-DPD: %u\r\n",
					  ws->user_config->dpd);
			SEND_ERR(ret);
		}

		ret = cstp_printf(ws, "X-DTLS-Port: %u\r\n",
				  WSPCONFIG(ws)->udp_port);
		SEND_ERR(ret);

		if (WSCONFIG(ws)->rekey_time > 0) {
			ret = cstp_printf(
				ws, "X-DTLS-Rekey-Time: %u\r\n",
				(unsigned int)(WSCONFIG(ws)->rekey_time + 10));
			SEND_ERR(ret);

			/* This is our private extension */
			if (WSCONFIG(ws)->rekey_method == REKEY_METHOD_SSL) {
				ret = cstp_puts(ws,
						"X-DTLS-Rekey-Method: ssl\r\n");
				SEND_ERR(ret);
			}
		}

		ret = cstp_printf(ws, "X-DTLS-Keepalive: %u\r\n",
				  ws->user_config->keepalive);
		SEND_ERR(ret);

		p = (char *)ws->buffer;
		for (i = 0; i < sizeof(ws->session_id); i++) {
			sprintf(p, "%.2x", (unsigned int)ws->session_id[i]);
			p += 2;
		}

		if (ws->req.use_psk || !WSCONFIG(ws)->dtls_legacy) {
			oclog(ws, LOG_INFO, "X-DTLS-App-ID: %s", ws->buffer);

			ret = cstp_printf(ws, "X-DTLS-App-ID: %s\r\n",
					  ws->buffer);
			SEND_ERR(ret);

			oclog(ws, LOG_INFO,
			      "DTLS ciphersuite: " DTLS_PROTO_INDICATOR);
			ret = cstp_printf(
				ws, "X-DTLS-CipherSuite: " DTLS_PROTO_INDICATOR
				    "\r\n");
		} else if (ws->req.selected_ciphersuite) {
			oclog(ws, LOG_INFO, "X-DTLS-Session-ID: %s",
			      ws->buffer);

			ret = cstp_printf(ws, "X-DTLS-Session-ID: %s\r\n",
					  ws->buffer);
			SEND_ERR(ret);

			oclog(ws, LOG_INFO, "DTLS ciphersuite: %s",
			      ws->req.selected_ciphersuite->oc_name);
			ret = cstp_printf(
				ws, "X-DTLS%s-CipherSuite: %s\r\n",
				(ws->req.selected_ciphersuite->dtls12_mode !=
				 0) ?
					"12" :
					"",
				ws->req.selected_ciphersuite->oc_name);
			SEND_ERR(ret);

			/* only send the X-DTLS-MTU in the legacy protocol, as there
			 * the DTLS ciphersuite/version is negotiated and we cannot predict
			 * the actual tunnel size */
			ret = cstp_printf(ws, "X-DTLS-MTU: %u\r\n",
					  DATA_MTU(ws, ws->link_mtu));
			SEND_ERR(ret);
			oclog(ws, LOG_INFO, "DTLS data MTU %u",
			      DATA_MTU(ws, ws->link_mtu));
		}
		SEND_ERR(ret);
	}

	/* hack for openconnect. It uses only a single MTU value */
	ret = cstp_printf(ws, "X-CSTP-Base-MTU: %u\r\n", ws->link_mtu);
	SEND_ERR(ret);
	oclog(ws, LOG_INFO, "Link MTU is %u bytes", ws->link_mtu);

	ret = cstp_printf(ws, "X-CSTP-MTU: %u\r\n", DATA_MTU(ws, ws->link_mtu));
	SEND_ERR(ret);

	if (ws->buffer_size < ws->link_mtu + 16) {
		oclog(ws, LOG_ERR, "buffer size is smaller than MTU (%u < %u)",
		      ws->buffer_size, ws->link_mtu);
		goto exit;
	}

	data_mtu_send(ws, DATA_MTU(ws, ws->link_mtu));

	if (WSCONFIG(ws)->banner) {
		ret = cstp_printf(ws, "X-CSTP-Banner: %s\r\n",
				  WSCONFIG(ws)->banner);
		SEND_ERR(ret);
	}

	/* send any compression methods */
	if (ws->dtls_selected_comp) {
		oclog(ws, LOG_INFO, "selected DTLS compression method %s\n",
		      ws->dtls_selected_comp->name);
		ret = cstp_printf(ws, "X-DTLS-Content-Encoding: %s\r\n",
				  ws->dtls_selected_comp->name);
		SEND_ERR(ret);
	}

	if (ws->cstp_selected_comp) {
		oclog(ws, LOG_INFO, "selected CSTP compression method %s\n",
		      ws->cstp_selected_comp->name);
		ret = cstp_printf(ws, "X-CSTP-Content-Encoding: %s\r\n",
				  ws->cstp_selected_comp->name);
		SEND_ERR(ret);
	}

	ret = cstp_puts(ws, "\r\n");
	SEND_ERR(ret);

	ret = cstp_uncork(ws);
	SEND_ERR(ret);

	ret = worker_event_loop(ws);
	if (ret != 0) {
		goto exit;
	}

	return 0;

exit:
	cstp_close(ws);
	/*gnutls_deinit(ws->session); */
	if (DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE &&
	    DTLS_ACTIVE(ws)->dtls_session) {
		dtls_close(DTLS_ACTIVE(ws));
	}
	if (DTLS_INACTIVE(ws)->udp_state == UP_ACTIVE &&
	    DTLS_INACTIVE(ws)->dtls_session) {
		dtls_close(DTLS_INACTIVE(ws));
	}

	exit_worker_reason(ws, terminate_reason);

send_error:
	oclog(ws, LOG_DEBUG, "error sending data\n");
	exit_worker(ws);

	return -1;
}

static int parse_data(struct worker_st *ws, uint8_t *buf, size_t buf_size,
		      time_t now, unsigned int is_dtls)
{
	int ret, e;
	uint8_t *plain;
	ssize_t plain_size;
	unsigned int head;

	if (is_dtls == 0) { /* CSTP */
		plain = buf + 8;
		plain_size = buf_size - 8;
		head = buf[6];
	} else {
		plain = buf + 1;
		plain_size = buf_size - 1;
		head = buf[0];
	}

	switch (head) {
	case AC_PKT_DPD_RESP:
		oclog(ws, LOG_TRANSFER_DEBUG, "received DPD response");
		break;
	case AC_PKT_KEEPALIVE:
		oclog(ws, LOG_TRANSFER_DEBUG, "received keepalive");
		break;
	case AC_PKT_DPD_OUT:
		if (is_dtls == 0) {
			buf[6] = AC_PKT_DPD_RESP;
			ret = cstp_send(ws, buf, buf_size);

			oclog(ws, LOG_TRANSFER_DEBUG,
			      "received TLS DPD; sent response (%d bytes)",
			      ret);

			if (ret < 0) {
				oclog(ws, LOG_ERR, "could not send data: %d",
				      ret);
				return -1;
			}
		} else {
			/* Use DPD for MTU discovery in DTLS */
			buf[0] = AC_PKT_DPD_RESP;

			if (buf_size - CSTP_DTLS_OVERHEAD >
			    DATA_MTU(ws, ws->link_mtu)) {
				/* peer is doing MTU discovery */
				data_mtu_set(ws, DTLS_ACTIVE(ws),
					     buf_size - CSTP_DTLS_OVERHEAD);
			}

			ret = dtls_send(DTLS_ACTIVE(ws), buf, buf_size);
			if (ret == GNUTLS_E_LARGE_PACKET) {
				oclog(ws, LOG_TRANSFER_DEBUG,
				      "could not send DPD of %zu bytes",
				      buf_size);
				mtu_not_ok(ws, DTLS_ACTIVE(ws));
				ret = dtls_send(DTLS_ACTIVE(ws), buf, 1);
			}

			if (ret < 0) {
				oclog(ws, LOG_TRANSFER_DEBUG,
				      "received DTLS DPD; error in sending response: %s",
				      gnutls_strerror(ret));
			} else {
				oclog(ws, LOG_TRANSFER_DEBUG,
				      "received DTLS DPD; sent response (%d bytes)",
				      ret);
			}
		}

		break;
	case AC_PKT_DISCONN:
		oclog(ws, LOG_INFO, "received BYE packet; exiting");
		/* In openconnect the BYE packet indicates an explicit
		 * user disconnect. In anyconnect clients it may indicate
		 * an intention to reconnect (e.g., because network was
		 * changed). We separate the error codes to ensure we do
		 * not interpret the intention incorrectly (see #281). */
		if (plain_size > 0 && plain[0] == 0xb0) {
			exit_worker_reason(ws, REASON_USER_DISCONNECT);
		} else {
			if (plain_size > 0) {
				oclog_hex(ws, LOG_DEBUG,
					  "bye packet with unknown payload",
					  plain, plain_size, 0);
				return -1;
			}

			exit_worker_reason(ws, REASON_TEMP_DISCONNECT);
		}
		break;
	case AC_PKT_COMPRESSED:
		/* decompress */
		if (is_dtls == 0) { /* CSTP */
			if (ws->cstp_selected_comp == NULL) {
				oclog(ws, LOG_ERR,
				      "received compressed data but no compression was negotiated");
				return -1;
			}

			plain_size = ws->cstp_selected_comp->decompress(
				ws->decomp, sizeof(ws->decomp), plain,
				plain_size);
			oclog(ws, LOG_DEBUG, "decompressed %zu to %zd\n",
			      buf_size - 8, plain_size);
		} else { /* DTLS */
			if (ws->dtls_selected_comp == NULL) {
				oclog(ws, LOG_ERR,
				      "received compressed data but no compression was negotiated");
				return -1;
			}

			plain_size = ws->dtls_selected_comp->decompress(
				ws->decomp, sizeof(ws->decomp), plain,
				plain_size);
			oclog(ws, LOG_DEBUG, "decompressed %zu to %zd\n",
			      buf_size - 1, plain_size);
		}

		if (plain_size <= 0) {
			oclog(ws, LOG_ERR, "decompression error %zd",
			      plain_size);
			return -1;
		}
		plain = ws->decomp;
		/* fall through */
	case AC_PKT_DATA:
		oclog(ws, LOG_TRANSFER_DEBUG, "writing %zd byte(s) to TUN",
		      plain_size);
		ret = tun_write(ws->tun_fd, plain, plain_size);
		if (ret == -1) {
			e = errno;
			oclog(ws, LOG_ERR, "could not write data to tun: %s",
			      strerror(e));
			return -1;
		}
		ws->tun_bytes_in += plain_size;

		if (is_data(plain, plain_size)) /* do not account ICMP */
			ws->last_nc_msg = now;

		break;
	default:
		oclog(ws, LOG_DEBUG, "received unknown packet %u/size: %u",
		      (unsigned int)head, (unsigned int)buf_size);
	}

	return 0;
}

static int parse_cstp_data(struct worker_st *ws, uint8_t *buf, size_t buf_size,
			   time_t now)
{
	unsigned pktlen;
	int ret;

	if (buf_size < 8) {
		oclog(ws, LOG_INFO,
		      "can't read CSTP header (only %zu bytes are available)",
		      buf_size);
		return -1;
	}

	if (buf[0] != 'S' || buf[1] != 'T' || buf[2] != 'F' || buf[3] != 1 ||
	    buf[7]) {
		oclog(ws, LOG_INFO, "can't recognise CSTP header");
		return -1;
	}

	pktlen = (buf[4] << 8) + buf[5];
	if (buf_size != 8 + pktlen) {
		oclog(ws, LOG_INFO,
		      "unexpected CSTP length (have %u, should be %zu)", pktlen,
		      buf_size - 8);
		return -1;
	}

	if (buf[6] == AC_PKT_DATA && DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE) {
		/* if we received a data packet in the CSTP channel we assume that
		 * our peer wants to switch to it as the communication channel */
		DTLS_ACTIVE(ws)->udp_state = UP_INACTIVE;
	}

	ret = parse_data(ws, buf, buf_size, now, 0);
	/* whatever we received treat it as DPD response.
	 * it indicates that the channel is alive */
	ws->last_msg_tcp = now;

	return ret;
}

static int parse_dtls_data(struct worker_st *ws, uint8_t *buf, size_t buf_size,
			   time_t now)
{
	int ret;

	if (buf_size < 1) {
		oclog(ws, LOG_INFO,
		      "can't read DTLS header (only %d bytes are available)",
		      (int)buf_size);
		return -1;
	}

	ret = parse_data(ws, buf, buf_size, now, 1);
	ws->last_msg_udp = now;
	return ret;
}

static int test_for_tcp_health_probe(struct worker_st *ws)
{
	int ret;
	uint8_t buffer[1];

	ret = recv(ws->conn_fd, buffer, sizeof(buffer), MSG_PEEK);

	// If we get back an error, assume this was a tcp health probe
	if (ret > 0)
		return 0;
	else
		return 1;
}

static void syserr_cb(const char *msg)
{
	struct worker_st *ws = ev_userdata(worker_loop);
	int err = errno;

	oclog(ws, LOG_ERR, "libev fatal error: %s / %s", msg, strerror(err));

	terminate_reason = REASON_ERROR;
	exit_worker_reason(ws, terminate_reason);
}

static void cstp_send_terminate(struct worker_st *ws)
{
	ws->buffer[0] = 'S';
	ws->buffer[1] = 'T';
	ws->buffer[2] = 'F';
	ws->buffer[3] = 1;
	ws->buffer[4] = 0;
	ws->buffer[5] = 0;
	ws->buffer[6] = AC_PKT_DISCONN;
	ws->buffer[7] = 0;

	oclog(ws, LOG_TRANSFER_DEBUG,
	      "sending disconnect message in TLS channel");
	cstp_send(ws, ws->buffer, 8);
	exit_worker_reason(ws, terminate_reason);
}

static void command_watcher_cb(EV_P_ ev_io *w, int revents)
{
	struct worker_st *ws = ev_userdata(worker_loop);

	int ret = handle_commands_from_main(ws);

	if (ret == ERR_NO_CMD_FD) {
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
	}

	if (ret < 0) {
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
	}

	if (DTLS_ACTIVE(ws)->udp_state == UP_SETUP) {
		ev_invoke(loop, &DTLS_ACTIVE(ws)->io, EV_READ);
	}
	if (DTLS_INACTIVE(ws)->udp_state == UP_SETUP) {
		ev_invoke(loop, &DTLS_INACTIVE(ws)->io, EV_READ);
	}
}

static void tls_watcher_cb(EV_P_ ev_io *w, int revents)
{
	struct timespec tnow;
	struct worker_st *ws = ev_userdata(loop);
	int ret;

	gettime(&tnow);

	ret = tls_mainloop(ws, &tnow);
	if (ret < 0) {
		oclog(ws, LOG_DEBUG, "tls_mainloop failed %d", ret);
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
	}
}

static void tun_watcher_cb(EV_P_ ev_io *w, int revents)
{
	struct timespec tnow;
	struct worker_st *ws = ev_userdata(loop);
	int ret;

	gettime(&tnow);

	ret = tun_mainloop(ws, &tnow);
	if (ret < 0) {
		oclog(ws, LOG_DEBUG, "tun_mainloop failed %d", ret);
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
	}
}

static void dtls_watcher_cb(EV_P_ ev_io *w, int revents)
{
	struct timespec tnow;
	struct worker_st *ws = ev_userdata(loop);
	struct dtls_st *dtls = (struct dtls_st *)w;
	int ret;

	gettime(&tnow);

	ret = dtls_mainloop(ws, dtls, &tnow);
	if (ret < 0) {
		oclog(ws, LOG_DEBUG, "dtls_mainloop failed %d", ret);
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
	}

#if defined(CAPTURE_LATENCY_SUPPORT)
	if (dtls->dtls_tptr.rx_time.tv_sec != 0) {
		capture_latency_sample(ws, &dtls->dtls_tptr.rx_time);
		dtls->dtls_tptr.rx_time.tv_sec = 0;
		dtls->dtls_tptr.rx_time.tv_nsec = 0;
	}
#endif
}

static void term_sig_watcher_cb(struct ev_loop *loop, ev_signal *w, int revents)
{
	struct worker_st *ws = ev_userdata(loop);

	cstp_send_terminate(ws);
}

static void invoke_dtls_if_needed(struct dtls_st *dtls)
{
	if ((dtls->udp_state > UP_WAIT_FD) && (dtls->dtls_session != NULL) &&
	    (gnutls_record_check_pending(dtls->dtls_session))) {
		ev_invoke(worker_loop, &dtls->io, EV_READ);
	}
}

static void periodic_check_watcher_cb(EV_P_ ev_timer *w, int revents)
{
	struct worker_st *ws = ev_userdata(loop);
	struct timespec tnow;

	gettime(&tnow);

	if (periodic_check(ws, &tnow, ws->user_config->dpd) < 0) {
		terminate_reason = REASON_ERROR;
		cstp_send_terminate(ws);
		return;
	}

	if (terminate)
		cstp_send_terminate(ws);

	if (gnutls_record_check_pending(ws->session)) {
		ev_invoke(loop, &tls_watcher, EV_READ);
	}

	invoke_dtls_if_needed(DTLS_ACTIVE(ws));
	invoke_dtls_if_needed(DTLS_INACTIVE(ws));
}

static int worker_event_loop(struct worker_st *ws)
{
	struct timespec tnow;
	unsigned int rnd;
	int ret;

#if defined(__linux__) && defined(HAVE_LIBSECCOMP)
	worker_loop = ev_default_loop(EVFLAG_NOENV | EVBACKEND_EPOLL);
#else
	worker_loop = EV_DEFAULT;
#endif

	// Restore the signal handlers
	ocsignal(SIGTERM, SIG_DFL);
	ocsignal(SIGINT, SIG_DFL);
	ocsignal(SIGALRM, SIG_DFL);

	ev_init(&alarm_sig_watcher, term_sig_watcher_cb);
	ev_signal_set(&alarm_sig_watcher, SIGALRM);
	ev_signal_start(worker_loop, &alarm_sig_watcher);

	ev_init(&int_sig_watcher, term_sig_watcher_cb);
	ev_signal_set(&int_sig_watcher, SIGINT);
	ev_signal_start(worker_loop, &int_sig_watcher);

	ev_init(&term_sig_watcher, term_sig_watcher_cb);
	ev_signal_set(&term_sig_watcher, SIGTERM);
	ev_signal_start(worker_loop, &term_sig_watcher);

	ev_set_userdata(worker_loop, ws);
	ev_set_syserr_cb(syserr_cb);

	ev_init(&command_watcher, command_watcher_cb);
	ev_io_set(&command_watcher, ws->cmd_fd, EV_READ);
	ev_io_start(worker_loop, &command_watcher);

	ev_init(&tls_watcher, tls_watcher_cb);
	ev_io_set(&tls_watcher, ws->conn_fd, EV_READ);
	ev_io_start(worker_loop, &tls_watcher);

	ev_init(&DTLS_ACTIVE(ws)->io, dtls_watcher_cb);
	ev_init(&DTLS_INACTIVE(ws)->io, dtls_watcher_cb);

	ev_init(&tun_watcher, tun_watcher_cb);
	ev_io_set(&tun_watcher, ws->tun_fd, EV_READ);
	ev_io_start(worker_loop, &tun_watcher);

	ev_init(&period_check_watcher, periodic_check_watcher_cb);
	/* generate a random offset to prevent all worker processes from
	 * acting at exactly the same time (e.g., after a server restart
	 * on which all clients reconnect at the same time). */
	ret = gnutls_rnd(GNUTLS_RND_NONCE, &rnd, sizeof(rnd));
	if (ret < 0) {
		oclog(ws, LOG_ERR, "error in the random generator: %s",
		      gnutls_strerror(ret));
		goto exit;
	}
	ev_timer_set(&period_check_watcher,
		     rnd % (unsigned int)WORKER_MAINTENANCE_TIME,
		     WORKER_MAINTENANCE_TIME);
	ev_timer_start(worker_loop, &period_check_watcher);

	/* start dead peer detection */
	gettime(&tnow);
	ws->last_msg_tcp = ws->last_msg_udp = ws->last_nc_msg = tnow.tv_sec;

	bandwidth_init(&ws->b_rx, ws->user_config->rx_per_sec);
	bandwidth_init(&ws->b_tx, ws->user_config->tx_per_sec);

	ev_run(worker_loop, 0);
	if (terminate != 0) {
		goto exit;
	}
	return 0;

exit:
	cstp_close(ws);
	/*gnutls_deinit(ws->session); */
	if (DTLS_ACTIVE(ws)->udp_state == UP_ACTIVE &&
	    DTLS_ACTIVE(ws)->dtls_session) {
		dtls_close(DTLS_ACTIVE(ws));
	}
	if (DTLS_INACTIVE(ws)->udp_state == UP_ACTIVE &&
	    DTLS_INACTIVE(ws)->dtls_session) {
		dtls_close(DTLS_INACTIVE(ws));
	}

	exit_worker_reason(ws, terminate_reason);

	return 1;
}
