dtls.c 19.2 KB
Newer Older
David Woodhouse's avatar
David Woodhouse committed
1
/*
2
 * OpenConnect (SSL + DTLS) VPN client
David Woodhouse's avatar
David Woodhouse committed
3
 *
David Woodhouse's avatar
David Woodhouse committed
4
 * Copyright © 2008-2015 Intel Corporation.
David Woodhouse's avatar
David Woodhouse committed
5
 *
6 7 8
 * Author: David Woodhouse <dwmw2@infradead.org>
 *
 * This program is free software; you can redistribute it and/or
David Woodhouse's avatar
David Woodhouse committed
9
 * modify it under the terms of the GNU Lesser General Public License
10
 * version 2.1, as published by the Free Software Foundation.
David Woodhouse's avatar
David Woodhouse committed
11
 *
12
 * This program is distributed in the hope that it will be useful, but
David Woodhouse's avatar
David Woodhouse committed
13 14 15
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
David Woodhouse's avatar
David Woodhouse committed
16 17
 */

18 19
#include <config.h>

David Woodhouse's avatar
David Woodhouse committed
20
#include <errno.h>
David Woodhouse's avatar
David Woodhouse committed
21 22
#include <sys/types.h>
#include <unistd.h>
23
#include <fcntl.h>
Dirk Hohndel's avatar
Dirk Hohndel committed
24
#include <string.h>
25 26 27
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
Kevin Cernekee's avatar
Kevin Cernekee committed
28 29 30 31
#ifndef _WIN32
#include <netinet/in.h>
#include <sys/socket.h>
#endif
David Woodhouse's avatar
David Woodhouse committed
32

33
#include "openconnect-internal.h"
David Woodhouse's avatar
David Woodhouse committed
34

David Woodhouse's avatar
David Woodhouse committed
35 36 37 38 39 40
/*
 * The master-secret is generated randomly by the client. The server
 * responds with a DTLS Session-ID. These, done over the HTTPS
 * connection, are enough to 'resume' a DTLS session, bypassing all
 * the normal setup of a normal DTLS connection.
 *
41 42 43 44
 * Cisco use a version of the protocol which predates RFC4347, but
 * isn't quite the same as the pre-RFC version of the protocol which
 * was in OpenSSL 0.9.8e -- it includes backports of some later
 * OpenSSL patches.
45
 *
Nick Andrew's avatar
Nick Andrew committed
46 47
 * The openssl/ directory of this source tree should contain both a
 * small patch against OpenSSL 0.9.8e to make it support Cisco's
48 49
 * snapshot of the protocol, and a larger patch against newer OpenSSL
 * which gives us an option to use the old protocol again.
50
 *
51 52 53 54 55 56
 * Cisco's server also seems to respond to the official version of the
 * protocol, with a change in the ChangeCipherSpec packet which implies
 * that it does know the difference and isn't just repeating the version
 * number seen in the ClientHello. But although I can make the handshake
 * complete by hacking tls1_mac() to use the _old_ protocol version
 * number when calculating the MAC, the server still seems to be ignoring
57 58
 * my subsequent data packets. So we use the old protocol, which is what
 * their clients use anyway.
Nick Andrew's avatar
Nick Andrew committed
59
 */
David Woodhouse's avatar
David Woodhouse committed
60

61
#if defined(OPENCONNECT_OPENSSL)
62 63
#define DTLS_SEND SSL_write
#define DTLS_RECV SSL_read
64
#elif defined(OPENCONNECT_GNUTLS)
65 66 67 68
#define DTLS_SEND gnutls_record_send
#define DTLS_RECV gnutls_record_recv
#endif

69 70
char *openconnect_bin2hex(const char *prefix, const uint8_t *data, unsigned len)
{
71 72
	struct oc_text_buf *buf;
	char *p = NULL;
73

74
	buf = buf_alloc();
75 76
	if (prefix)
		buf_append(buf, "%s", prefix);
77 78 79 80 81
	buf_append_hex(buf, data, len);

	if (!buf_error(buf)) {
		p = buf->data;
		buf->data = NULL;
82
	}
83 84 85
	buf_free(buf);

	return p;
86 87
}

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
char *openconnect_bin2base64(const char *prefix, const uint8_t *data, unsigned len)
{
	struct oc_text_buf *buf;
	char *p = NULL;

	buf = buf_alloc();
	if (prefix)
		buf_append(buf, "%s", prefix);
	buf_append_base64(buf, data, len);

	if (!buf_error(buf)) {
		p = buf->data;
		buf->data = NULL;
	}
	buf_free(buf);

	return p;
}

107
static int connect_dtls_socket(struct openconnect_info *vpninfo)
108
{
109
	int dtls_fd, ret;
110

111 112 113 114 115 116 117
	/* Sanity check for the removal of new_dtls_{fd,ssl} */
	if (vpninfo->dtls_fd != -1) {
		vpn_progress(vpninfo, PRG_ERR, _("DTLS connection attempted with an existing fd\n"));
		vpninfo->dtls_attempt_period = 0;
		return -EINVAL;
	}

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
	if (!vpninfo->dtls_addr) {
		vpn_progress(vpninfo, PRG_ERR, _("No DTLS address\n"));
		vpninfo->dtls_attempt_period = 0;
		return -EINVAL;
	}

	if (!vpninfo->dtls_cipher) {
		/* We probably didn't offer it any ciphers it liked */
		vpn_progress(vpninfo, PRG_ERR, _("Server offered no DTLS cipher option\n"));
		vpninfo->dtls_attempt_period = 0;
		return -EINVAL;
	}

	if (vpninfo->proxy) {
		/* XXX: Theoretically, SOCKS5 proxies can do UDP too */
		vpn_progress(vpninfo, PRG_ERR, _("No DTLS when connected via proxy\n"));
		vpninfo->dtls_attempt_period = 0;
		return -EINVAL;
	}

138 139
	dtls_fd = udp_connect(vpninfo);
	if (dtls_fd < 0)
140 141 142 143 144
		return -EINVAL;


	ret = start_dtls_handshake(vpninfo, dtls_fd);
	if (ret) {
145
		closesocket(dtls_fd);
146 147 148
		return ret;
	}

149 150
	vpninfo->dtls_state = DTLS_CONNECTING;

151
	vpninfo->dtls_fd = dtls_fd;
152 153 154
	monitor_fd_new(vpninfo, dtls);
	monitor_read_fd(vpninfo, dtls);
	monitor_except_fd(vpninfo, dtls);
155 156 157 158 159 160

	time(&vpninfo->new_dtls_started);

	return dtls_try_handshake(vpninfo);
}

161
void dtls_close(struct openconnect_info *vpninfo)
162 163
{
	if (vpninfo->dtls_ssl) {
David Woodhouse's avatar
David Woodhouse committed
164
		dtls_ssl_free(vpninfo);
165
		closesocket(vpninfo->dtls_fd);
166 167 168
		unmonitor_read_fd(vpninfo, dtls);
		unmonitor_write_fd(vpninfo, dtls);
		unmonitor_except_fd(vpninfo, dtls);
169 170 171
		vpninfo->dtls_ssl = NULL;
		vpninfo->dtls_fd = -1;
	}
172
	vpninfo->dtls_state = DTLS_SLEEPING;
173
}
David Woodhouse's avatar
David Woodhouse committed
174

175
static int dtls_reconnect(struct openconnect_info *vpninfo)
176
{
177
	dtls_close(vpninfo);
178 179 180 181

	if (vpninfo->dtls_state == DTLS_DISABLED)
		return -EINVAL;

182
	vpninfo->dtls_state = DTLS_SLEEPING;
183
	return connect_dtls_socket(vpninfo);
184 185
}

186
int dtls_setup(struct openconnect_info *vpninfo, int dtls_attempt_period)
David Woodhouse's avatar
David Woodhouse committed
187
{
188
	struct oc_vpn_option *dtls_opt = vpninfo->dtls_options;
David Woodhouse's avatar
David Woodhouse committed
189
	int dtls_port = 0;
David Woodhouse's avatar
David Woodhouse committed
190

191 192 193
	if (vpninfo->dtls_state == DTLS_DISABLED)
		return -EINVAL;

194 195 196 197
	vpninfo->dtls_attempt_period = dtls_attempt_period;
	if (!dtls_attempt_period)
		return 0;

David Woodhouse's avatar
David Woodhouse committed
198
	while (dtls_opt) {
199
		vpn_progress(vpninfo, PRG_DEBUG,
200 201
			     _("DTLS option %s : %s\n"),
			     dtls_opt->option, dtls_opt->value);
David Woodhouse's avatar
David Woodhouse committed
202

203
		if (!strcmp(dtls_opt->option, "X-DTLS-Port")) {
David Woodhouse's avatar
David Woodhouse committed
204
			dtls_port = atol(dtls_opt->value);
205
		} else if (!strcmp(dtls_opt->option, "X-DTLS-Keepalive")) {
206
			vpninfo->dtls_times.keepalive = atol(dtls_opt->value);
207
		} else if (!strcmp(dtls_opt->option, "X-DTLS-DPD")) {
David Woodhouse's avatar
David Woodhouse committed
208 209 210
			int j = atol(dtls_opt->value);
			if (j && (!vpninfo->dtls_times.dpd || j < vpninfo->dtls_times.dpd))
				vpninfo->dtls_times.dpd = j;
211
		} else if (!strcmp(dtls_opt->option, "X-DTLS-Rekey-Method")) {
212 213 214 215 216 217
			if (!strcmp(dtls_opt->value, "new-tunnel"))
				vpninfo->dtls_times.rekey_method = REKEY_TUNNEL;
			else if (!strcmp(dtls_opt->value, "ssl"))
				vpninfo->dtls_times.rekey_method = REKEY_SSL;
			else
				vpninfo->dtls_times.rekey_method = REKEY_NONE;
218
		} else if (!strcmp(dtls_opt->option, "X-DTLS-Rekey-Time")) {
219
			vpninfo->dtls_times.rekey = atol(dtls_opt->value);
David Woodhouse's avatar
David Woodhouse committed
220
		}
Nick Andrew's avatar
Nick Andrew committed
221

David Woodhouse's avatar
David Woodhouse committed
222 223
		dtls_opt = dtls_opt->next;
	}
224
	if (!dtls_port) {
225
		vpninfo->dtls_attempt_period = 0;
David Woodhouse's avatar
David Woodhouse committed
226
		return -EINVAL;
227
	}
228 229
	if (vpninfo->dtls_times.rekey <= 0)
		vpninfo->dtls_times.rekey_method = REKEY_NONE;
230

231
	if (udp_sockaddr(vpninfo, dtls_port)) {
232
		vpninfo->dtls_attempt_period = 0;
233 234
		return -EINVAL;
	}
235 236
	if (connect_dtls_socket(vpninfo))
		return -EINVAL;
237

238
	vpn_progress(vpninfo, PRG_DEBUG,
239
		     _("DTLS initialised. DPD %d, Keepalive %d\n"),
240
		     vpninfo->dtls_times.dpd, vpninfo->dtls_times.keepalive);
241 242

	return 0;
David Woodhouse's avatar
David Woodhouse committed
243 244
}

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
int udp_tos_update(struct openconnect_info *vpninfo, struct pkt *pkt)
{
	int tos;

	/* Extract TOS field from IP header (IPv4 and IPv6 differ) */
	switch(pkt->data[0] >> 4) {
		case 4:
			tos = pkt->data[1];
			break;
		case 6:
			tos = (load_be16(pkt->data) >> 4) & 0xff;
			break;
		default:
			vpn_progress(vpninfo, PRG_ERR,
				     _("Unknown packet (len %d) received: %02x %02x %02x %02x...\n"),
				     pkt->len, pkt->data[0], pkt->data[1], pkt->data[2], pkt->data[3]);
			return -EINVAL;
	}

	/* set the actual value */
	if (tos != vpninfo->dtls_tos_current) {
		vpn_progress(vpninfo, PRG_DEBUG, _("TOS this: %d, TOS last: %d\n"),
			     tos, vpninfo->dtls_tos_current);
		if (setsockopt(vpninfo->dtls_fd, vpninfo->dtls_tos_proto,
			       vpninfo->dtls_tos_optname, (void *)&tos, sizeof(tos)))
			vpn_perror(vpninfo, _("UDP setsockopt"));
		else
			vpninfo->dtls_tos_current = tos;
	}
	return 0;
}

277
int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable)
278
{
279
	int work_done = 0;
280
	char magic_pkt;
281

282 283 284 285 286 287
	if (vpninfo->dtls_need_reconnect) {
		vpninfo->dtls_need_reconnect = 0;
		dtls_reconnect(vpninfo);
		return 1;
	}

288
	if (vpninfo->dtls_state == DTLS_CONNECTING) {
289
		dtls_try_handshake(vpninfo);
290
		return 0;
291
	}
292 293 294 295 296

	if (vpninfo->dtls_state == DTLS_SLEEPING) {
		int when = vpninfo->new_dtls_started + vpninfo->dtls_attempt_period - time(NULL);

		if (when <= 0) {
297
			vpn_progress(vpninfo, PRG_DEBUG, _("Attempt new DTLS connection\n"));
298 299
			if (connect_dtls_socket(vpninfo) < 0)
				*timeout = 1000;
300 301 302
		} else if ((when * 1000) < *timeout) {
			*timeout = when * 1000;
		}
303
		return 0;
304
	}
305

306
	while (readable) {
307
		int len = MAX(16384, vpninfo->ip_info.mtu);
308 309
		unsigned char *buf;

David Woodhouse's avatar
David Woodhouse committed
310 311 312
		if (!vpninfo->dtls_pkt) {
			vpninfo->dtls_pkt = malloc(sizeof(struct pkt) + len);
			if (!vpninfo->dtls_pkt) {
313
				vpn_progress(vpninfo, PRG_ERR, _("Allocation failed\n"));
314 315 316 317
				break;
			}
		}

David Woodhouse's avatar
David Woodhouse committed
318
		buf = vpninfo->dtls_pkt->data - 1;
319
		len = DTLS_RECV(vpninfo->dtls_ssl, buf, len + 1);
320 321
		if (len <= 0)
			break;
322

323
		vpn_progress(vpninfo, PRG_TRACE,
324 325
			     _("Received DTLS packet 0x%02x of %d bytes\n"),
			     buf[0], len);
326

327
		vpninfo->dtls_times.last_rx = time(NULL);
328

329
		switch (buf[0]) {
David Woodhouse's avatar
David Woodhouse committed
330
		case AC_PKT_DATA:
David Woodhouse's avatar
David Woodhouse committed
331 332 333
			vpninfo->dtls_pkt->len = len - 1;
			queue_packet(&vpninfo->incoming_queue, vpninfo->dtls_pkt);
			vpninfo->dtls_pkt = NULL;
334 335 336
			work_done = 1;
			break;

337
		case AC_PKT_DPD_OUT:
338
			vpn_progress(vpninfo, PRG_DEBUG, _("Got DTLS DPD request\n"));
339 340 341

			/* FIXME: What if the packet doesn't get through? */
			magic_pkt = AC_PKT_DPD_RESP;
342
			if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
343 344
				vpn_progress(vpninfo, PRG_ERR,
					     _("Failed to send DPD response. Expect disconnect\n"));
345 346
			continue;

David Woodhouse's avatar
David Woodhouse committed
347
		case AC_PKT_DPD_RESP:
348
			vpn_progress(vpninfo, PRG_DEBUG, _("Got DTLS DPD response\n"));
349 350
			break;

351
		case AC_PKT_KEEPALIVE:
352
			vpn_progress(vpninfo, PRG_DEBUG, _("Got DTLS Keepalive\n"));
353 354
			break;

355 356 357 358 359 360
		case AC_PKT_COMPRESSED:
			if (!vpninfo->dtls_compr) {
				vpn_progress(vpninfo, PRG_ERR,
					     _("Compressed DTLS packet received when compression not enabled\n"));
				goto unknown_pkt;
			}
361 362
			decompress_and_queue_packet(vpninfo, vpninfo->dtls_compr,
						    vpninfo->dtls_pkt->data, len - 1);
363
			break;
364
		default:
365
			vpn_progress(vpninfo, PRG_ERR,
366 367
				     _("Unknown DTLS packet type %02x, len %d\n"),
				     buf[0], len);
368 369
			if (1) {
				/* Some versions of OpenSSL have bugs with receiving out-of-order
Nick Andrew's avatar
Nick Andrew committed
370
				 * packets. Not only do they wrongly decide to drop packets if
371 372 373 374 375
				 * two packets get swapped in transit, but they also _fail_ to
				 * drop the packet in non-blocking mode; instead they return
				 * the appropriate length of garbage. So don't abort... for now. */
				break;
			} else {
376
			unknown_pkt:
377 378 379 380
				vpninfo->quit_reason = "Unknown packet received";
				return 1;
			}

381
		}
382
	}
383

384
	switch (keepalive_action(&vpninfo->dtls_times, timeout)) {
385 386 387
	case KA_REKEY: {
		int ret;

388
		vpn_progress(vpninfo, PRG_INFO, _("DTLS rekey due\n"));
389

390 391 392 393 394
		if (vpninfo->dtls_times.rekey_method == REKEY_SSL) {
			time(&vpninfo->new_dtls_started);
			vpninfo->dtls_state = DTLS_CONNECTING;
			ret = dtls_try_handshake(vpninfo);
			if (ret) {
395 396
				vpn_progress(vpninfo, PRG_ERR, _("DTLS Rehandshake failed; reconnecting.\n"));
				return connect_dtls_socket(vpninfo);
397
			}
398 399
		}

400
		return 1;
401
	}
402 403

	case KA_DPD_DEAD:
404
		vpn_progress(vpninfo, PRG_ERR, _("DTLS Dead Peer Detection detected dead peer!\n"));
405
		/* Fall back to SSL, and start a new DTLS connection */
406
		dtls_reconnect(vpninfo);
407
		return 1;
408 409

	case KA_DPD:
410
		vpn_progress(vpninfo, PRG_DEBUG, _("Send DTLS DPD\n"));
411

412
		magic_pkt = AC_PKT_DPD_OUT;
413 414 415 416
		if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
			vpn_progress(vpninfo, PRG_ERR,
				     _("Failed to send DPD request. Expect disconnect\n"));

417 418 419 420
		/* last_dpd will just have been set */
		vpninfo->dtls_times.last_tx = vpninfo->dtls_times.last_dpd;
		work_done = 1;
		break;
421

422 423 424
	case KA_KEEPALIVE:
		/* No need to send an explicit keepalive
		   if we have real data to send */
425
		if (vpninfo->outgoing_queue.head)
426
			break;
427

428
		vpn_progress(vpninfo, PRG_DEBUG, _("Send DTLS Keepalive\n"));
429

430
		magic_pkt = AC_PKT_KEEPALIVE;
431 432 433
		if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
			vpn_progress(vpninfo, PRG_ERR,
				     _("Failed to send keepalive request. Expect disconnect\n"));
434 435 436
		time(&vpninfo->dtls_times.last_tx);
		work_done = 1;
		break;
437

438 439
	case KA_NONE:
		;
440 441
	}

442
	/* Service outgoing packet queue */
443
	unmonitor_write_fd(vpninfo, dtls);
444 445
	while (vpninfo->outgoing_queue.head) {
		struct pkt *this = dequeue_packet(&vpninfo->outgoing_queue);
446
		struct pkt *send_pkt = this;
447
		int ret;
448

449 450
		/* If TOS optname is set, we want to copy the TOS/TCLASS header
		   to the outer UDP packet */
451 452
		if (vpninfo->dtls_tos_optname)
			udp_tos_update(vpninfo, this);
453

454
		/* One byte of header */
455
		this->cstp.hdr[7] = AC_PKT_DATA;
Nick Andrew's avatar
Nick Andrew committed
456

457 458 459 460 461 462
		/* We can compress into vpninfo->deflate_pkt unless CSTP
		 * currently has a compressed packet pending — which it
		 * shouldn't if DTLS is active. */
		if (vpninfo->dtls_compr &&
		    vpninfo->current_ssl_pkt != vpninfo->deflate_pkt &&
		    !compress_packet(vpninfo, vpninfo->dtls_compr, this)) {
463
				send_pkt = vpninfo->deflate_pkt;
464
				send_pkt->cstp.hdr[7] = AC_PKT_COMPRESSED;
465 466
		}

467
#ifdef OPENCONNECT_OPENSSL
468
		ret = SSL_write(vpninfo->dtls_ssl, &send_pkt->cstp.hdr[7], send_pkt->len + 1);
469 470 471
		if (ret <= 0) {
			ret = SSL_get_error(vpninfo->dtls_ssl, ret);

472
			if (ret == SSL_ERROR_WANT_WRITE) {
473
				monitor_write_fd(vpninfo, dtls);
474
				requeue_packet(&vpninfo->outgoing_queue, this);
475 476 477
			} else if (ret != SSL_ERROR_WANT_READ) {
				/* If it's a real error, kill the DTLS connection and
				   requeue the packet to be sent over SSL */
478
				vpn_progress(vpninfo, PRG_ERR,
479 480
					     _("DTLS got write error %d. Falling back to SSL\n"),
					     ret);
481
				openconnect_report_ssl_errors(vpninfo);
482
				dtls_reconnect(vpninfo);
483
				requeue_packet(&vpninfo->outgoing_queue, this);
484
				work_done = 1;
485
			}
486
			return work_done;
487
		}
488
#else /* GnuTLS */
489
		ret = gnutls_record_send(vpninfo->dtls_ssl, &send_pkt->cstp.hdr[7], send_pkt->len + 1);
490
		if (ret <= 0) {
491
			if (ret != GNUTLS_E_AGAIN && ret != GNUTLS_E_INTERRUPTED) {
492 493 494
				vpn_progress(vpninfo, PRG_ERR,
					     _("DTLS got write error: %s. Falling back to SSL\n"),
					     gnutls_strerror(ret));
495
				dtls_reconnect(vpninfo);
496
				work_done = 1;
497 498
			} else {
				/* Wake me up when it becomes writeable */
499
				monitor_write_fd(vpninfo, dtls);
500
			}
501

502
			requeue_packet(&vpninfo->outgoing_queue, this);
503
			return work_done;
504 505
		}
#endif
506
		time(&vpninfo->dtls_times.last_tx);
507
		vpn_progress(vpninfo, PRG_TRACE,
508
			     _("Sent DTLS packet of %d bytes; DTLS send returned %d\n"),
509
			     this->len, ret);
510
		free(this);
511
	}
512

513
	return work_done;
514
}
515

David Woodhouse's avatar
David Woodhouse committed
516 517 518 519 520 521 522
/* This symbol is missing in glibc < 2.22 (bug 18643). */
#if defined(__linux__) && !defined(HAVE_IPV6_PATHMTU)
# define HAVE_IPV6_PATHMTU 1
# define IPV6_PATHMTU 61
#endif

#define PKT_INTERVAL_MS 50
523

524 525 526 527 528
/* Performs a binary search to detect MTU.
 * @buf: is preallocated with MTU size
 *
 * Returns: new MTU or 0
 */
David Woodhouse's avatar
David Woodhouse committed
529
static int probe_mtu(struct openconnect_info *vpninfo, unsigned char *buf)
530
{
David Woodhouse's avatar
David Woodhouse committed
531
	int max, min, cur, ret, absolute_min, last;
532
	int tries = 0; /* Number of loops in bin search - includes resends */
David Woodhouse's avatar
David Woodhouse committed
533 534
	uint32_t id, id_len;
	struct timeval start_tv, now_tv, last_tv;
535

David Woodhouse's avatar
David Woodhouse committed
536 537 538
	absolute_min = 576;
	if (vpninfo->ip_info.addr6)
		absolute_min = 1280;
539

David Woodhouse's avatar
David Woodhouse committed
540 541 542
	/* We'll assume that it is at least functional, and permits the bare
	 * minimum MTU for the protocol(s) it transports. All else is mad. */
	min = absolute_min;
543

David Woodhouse's avatar
David Woodhouse committed
544 545 546
	/* First send a probe at the configured maximum. Most of the time, this
	   one will probably work. */
	last = cur = max = vpninfo->ip_info.mtu;
547

David Woodhouse's avatar
David Woodhouse committed
548 549
	if (max <= min)
		goto fail;
550

David Woodhouse's avatar
David Woodhouse committed
551 552 553
	/* Generate unique ID */
	if (openconnect_random(&id, sizeof(id)) < 0)
		goto fail;
554

David Woodhouse's avatar
David Woodhouse committed
555 556
	vpn_progress(vpninfo, PRG_DEBUG,
		     _("Initiating MTU detection (min=%d, max=%d)\n"), min, max);
557

David Woodhouse's avatar
David Woodhouse committed
558 559
	gettimeofday(&start_tv, NULL);
	last_tv = start_tv;
560

David Woodhouse's avatar
David Woodhouse committed
561 562
	while (1) {
		int wait_ms;
563

David Woodhouse's avatar
David Woodhouse committed
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
#ifdef HAVE_IPV6_PATHMTU
		if (vpninfo->peer_addr->sa_family == AF_INET6) {
			struct ip6_mtuinfo mtuinfo;
			socklen_t len = sizeof(mtuinfo);
			int newmax;

			if (getsockopt(vpninfo->dtls_fd, IPPROTO_IPV6, IPV6_PATHMTU, &mtuinfo, &len) >= 0) {
				newmax = mtuinfo.ip6m_mtu;
				if (newmax > 0) {
					newmax = dtls_set_mtu(vpninfo, newmax) - /*ipv6*/40 - /*udp*/20 - /*oc dtls*/1;
					if (absolute_min > newmax)
						goto fail;
					if (max > newmax)
						max = newmax;
					if (cur > newmax)
						cur = newmax;
				}
581
			}
582
		}
583 584
#endif

585
		buf[0] = AC_PKT_DPD_OUT;
David Woodhouse's avatar
David Woodhouse committed
586 587
		id_len = id + cur;
		memcpy(&buf[1], &id_len, sizeof(id_len));
588

589
		vpn_progress(vpninfo, PRG_TRACE,
David Woodhouse's avatar
David Woodhouse committed
590 591 592
			     _("Sending MTU DPD probe (%u bytes)\n"), cur);
		ret = openconnect_dtls_write(vpninfo, buf, cur + 1);
		if (ret != cur + 1) {
593
			vpn_progress(vpninfo, PRG_ERR,
David Woodhouse's avatar
David Woodhouse committed
594 595 596 597 598 599 600 601 602 603 604 605 606
				     _("Failed to send DPD request (%d %d)\n"), cur, ret);
			if (cur == max) {
				max = --cur;
				if (cur >= absolute_min)
					continue;
			}
			goto fail;
		}
		if (last == cur)
			tries++;
		else {
			tries = 0;
			last = cur;
607 608 609
		}

		memset(buf, 0, sizeof(id)+1);
David Woodhouse's avatar
David Woodhouse committed
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
	keep_waiting:
		gettimeofday(&now_tv, NULL);

		if (now_tv.tv_sec > start_tv.tv_sec + 10) {
                        if (absolute_min == min) {
                                /* Hm, we never got *anything* back successfully? */
                                vpn_progress(vpninfo, PRG_ERR,
                                             _("Too long time in MTU detect loop; assuming negotiated MTU.\n"));
                                goto fail;
                        } else {
                                vpn_progress(vpninfo, PRG_ERR,
                                             _("Too long time in MTU detect loop; MTU set to %d.\n"), min);
				ret = min;
				goto out;
                        }
625
		}
626

David Woodhouse's avatar
David Woodhouse committed
627 628 629 630 631 632 633 634 635 636

		wait_ms = PKT_INTERVAL_MS -
			((now_tv.tv_sec - last_tv.tv_sec) * 1000) -
			((now_tv.tv_usec - last_tv.tv_usec) / 1000);
		if (wait_ms < 0 || wait_ms > PKT_INTERVAL_MS)
			wait_ms = PKT_INTERVAL_MS;

		ret = openconnect_dtls_read(vpninfo, buf, max+1, wait_ms);
		if (ret > 0 && (buf[0] != AC_PKT_DPD_RESP || !memcpy(&id_len, &buf[1], sizeof(id_len)) ||
				id_len != id + ret - 1)) {
637
			vpn_progress(vpninfo, PRG_DEBUG,
David Woodhouse's avatar
David Woodhouse committed
638 639
				     _("Received unexpected packet (%.2x) in MTU detection; skipping.\n"), (unsigned)buf[0]);
			goto keep_waiting;
640 641
		}

David Woodhouse's avatar
David Woodhouse committed
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
		if (ret == -ETIMEDOUT) {
			if (tries >= 6) {
				vpn_progress(vpninfo, PRG_DEBUG,
					     _("No response to size %u after %d tries; declare MTU is %u\n"),
					     last, tries, min);
				ret = min;
				goto out;
			}
		} else if (ret < 0) {
			vpn_progress(vpninfo, PRG_ERR,
				     _("Failed to recv DPD request (%d)\n"), ret);
			goto fail;
		} else if (ret > 0) {
			vpn_progress(vpninfo, PRG_TRACE,
				     _("Received MTU DPD probe (%u bytes)\n"), ret - 1);
			ret--;
			tries = 0;
		}
660

David Woodhouse's avatar
David Woodhouse committed
661 662
		if (ret == max)
			goto out;
663

David Woodhouse's avatar
David Woodhouse committed
664 665 666 667 668 669
		if (ret > min) {
			min = ret;
			if (min >= last) {
				cur = (min + max + 1) / 2;
			} else {
				cur = (min + last + 1) / 2;
670
			}
David Woodhouse's avatar
David Woodhouse committed
671 672
		} else {
			cur = (min + last + 1) / 2;
673 674 675
		}
	}
 fail:
David Woodhouse's avatar
David Woodhouse committed
676 677 678
	ret = 0;
 out:
	return ret;
679
}
David Woodhouse's avatar
David Woodhouse committed
680 681


682

683
void dtls_detect_mtu(struct openconnect_info *vpninfo)
684 685 686 687 688
{
	int mtu = vpninfo->ip_info.mtu;
	int prev_mtu = vpninfo->ip_info.mtu;
	unsigned char *buf;

David Woodhouse's avatar
David Woodhouse committed
689
	if (vpninfo->ip_info.mtu < 1 + sizeof(uint32_t))
690 691 692 693 694 695 696 697 698
		return;

	/* detect MTU */
	buf = calloc(1, 1 + vpninfo->ip_info.mtu);
	if (!buf) {
		vpn_progress(vpninfo, PRG_ERR, _("Allocation failed\n"));
		return;
	}

David Woodhouse's avatar
David Woodhouse committed
699 700 701
	mtu = probe_mtu(vpninfo, buf);
	if (mtu == 0)
		goto skip_mtu;
702 703 704 705 706 707 708 709 710 711 712 713 714 715

	vpninfo->ip_info.mtu = mtu;
	if (prev_mtu != vpninfo->ip_info.mtu) {
		vpn_progress(vpninfo, PRG_INFO,
		     _("Detected MTU of %d bytes (was %d)\n"), vpninfo->ip_info.mtu, prev_mtu);
	} else {
		vpn_progress(vpninfo, PRG_DEBUG,
		     _("No change in MTU after detection (was %d)\n"), prev_mtu);
	}

 skip_mtu:
	free(buf);
}