resetting manifest requested domain to floor
[platform/upstream/openconnect.git] / dtls.c
diff --git a/dtls.c b/dtls.c
index 905605e..9c21a3f 100644 (file)
--- a/dtls.c
+++ b/dtls.c
@@ -1,14 +1,15 @@
 /*
- * Open AnyConnect (SSL + DTLS) client
+ * OpenConnect (SSL + DTLS) VPN client
  *
- * © 2008 David Woodhouse <dwmw2@infradead.org>
+ * Copyright © 2008-2012 Intel Corporation.
  *
- * This library is free software; you can redistribute it and/or
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
+ * version 2.1, as published by the Free Software Foundation.
  *
- * This library is distributed in the hope that it will be useful, but
+ * This program 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
  * Lesser General Public License for more details.
 #include <sys/socket.h>
 #include <netdb.h>
 #include <unistd.h>
-#include <openssl/err.h>
+#include <netinet/in.h>
 #include <fcntl.h>
 #include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "openconnect-internal.h"
+
+static unsigned char nybble(unsigned char n)
+{
+       if      (n >= '0' && n <= '9') return n - '0';
+       else if (n >= 'A' && n <= 'F') return n - ('A' - 10);
+       else if (n >= 'a' && n <= 'f') return n - ('a' - 10);
+       return 0;
+}
 
-#include "openconnect.h"
+unsigned char unhex(const char *data)
+{
+       return (nybble(data[0]) << 4) | nybble(data[1]);
+}
+
+#ifdef HAVE_DTLS
 
 #if 0
 /*
@@ -49,7 +68,7 @@ int RAND_pseudo_bytes(char *buf, int len)
        memset(buf, 0x5a, len);
        printf("FAKE PSEUDO RANDOM!\n");
        return 1;
-       
+
 }
 int RAND_bytes(char *buf, int len)
 {
@@ -71,8 +90,8 @@ int RAND_bytes(char *buf, int len)
  * was in OpenSSL 0.9.8e -- it includes backports of some later
  * OpenSSL patches.
  *
- * The openssl/ directory of this source tree should contain both a 
- * small patch against OpenSSL 0.9.8e to make it support Cisco's 
+ * The openssl/ directory of this source tree should contain both a
+ * small patch against OpenSSL 0.9.8e to make it support Cisco's
  * snapshot of the protocol, and a larger patch against newer OpenSSL
  * which gives us an option to use the old protocol again.
  *
@@ -84,120 +103,114 @@ int RAND_bytes(char *buf, int len)
  * number when calculating the MAC, the server still seems to be ignoring
  * my subsequent data packets. So we use the old protocol, which is what
  * their clients use anyway.
- */   
+ */
 
-static unsigned char nybble(unsigned char n)
-{
-       if      (n >= '0' && n <= '9') return n - '0';
-       else if (n >= 'A' && n <= 'F') return n - ('A' - 10);
-       else if (n >= 'a' && n <= 'f') return n - ('a' - 10);
-       return 0;
-}
+#if defined (DTLS_OPENSSL)
+#define DTLS_SEND SSL_write
+#define DTLS_RECV SSL_read
 
-static unsigned char hex(const char *data)
-{
-       return (nybble(data[0]) << 4) | nybble(data[1]);
-}
+#ifdef HAVE_DTLS1_STOP_TIMER
+/* OpenSSL doesn't deliberately export this, but we need it to
+   workaround a DTLS bug in versions < 1.0.0e */
+extern void dtls1_stop_timer (SSL *);
+#endif
 
-int connect_dtls_socket(struct openconnect_info *vpninfo)
+static int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
 {
-       SSL_METHOD *dtls_method;
-       SSL_CIPHER *https_cipher;
+       STACK_OF(SSL_CIPHER) *ciphers;
+       method_const SSL_METHOD *dtls_method;
+       SSL_CIPHER *dtls_cipher;
        SSL *dtls_ssl;
        BIO *dtls_bio;
-       int dtls_fd;
-
-       dtls_fd = socket(vpninfo->peer_addr->sa_family, SOCK_DGRAM, IPPROTO_UDP);
-       if (dtls_fd < 0) {
-               perror("Open UDP socket for DTLS:");
-               return -EINVAL;
-       }
-       
-       if (connect(dtls_fd, vpninfo->peer_addr, vpninfo->peer_addrlen)) {
-               perror("UDP (DTLS) connect:\n");
-               close(dtls_fd);
-               return -EINVAL;
-       }
-
-       fcntl(dtls_fd, F_SETFD, FD_CLOEXEC);
-       
-       https_cipher = SSL_get_current_cipher(vpninfo->https_ssl);
 
        if (!vpninfo->dtls_ctx) {
                dtls_method = DTLSv1_client_method();
                vpninfo->dtls_ctx = SSL_CTX_new(dtls_method);
                if (!vpninfo->dtls_ctx) {
-                       vpninfo->progress(vpninfo, PRG_ERR, "Initialise DTLSv1 CTX failed\n");
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Initialise DTLSv1 CTX failed\n"));
+                       openconnect_report_ssl_errors(vpninfo);
+                       vpninfo->dtls_attempt_period = 0;
                        return -EINVAL;
                }
 
                /* If we don't readahead, then we do short reads and throw
                   away the tail of data packets. */
                SSL_CTX_set_read_ahead(vpninfo->dtls_ctx, 1);
+
+               if (!SSL_CTX_set_cipher_list(vpninfo->dtls_ctx, vpninfo->dtls_cipher)) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Set DTLS cipher list failed\n"));
+                       SSL_CTX_free(vpninfo->dtls_ctx);
+                       vpninfo->dtls_ctx = NULL;
+                       vpninfo->dtls_attempt_period = 0;
+                       return -EINVAL;
+               }
        }
 
        if (!vpninfo->dtls_session) {
                /* We're going to "resume" a session which never existed. Fake it... */
                vpninfo->dtls_session = SSL_SESSION_new();
                if (!vpninfo->dtls_session) {
-                       vpninfo->progress(vpninfo, PRG_ERR, "Initialise DTLSv1 session failed\n");
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Initialise DTLSv1 session failed\n"));
+                       vpninfo->dtls_attempt_period = 0;
                        return -EINVAL;
-               }                       
-               vpninfo->dtls_session->ssl_version = 0x0100; // DTLS1_BAD_VER
+               }
+               vpninfo->dtls_session->ssl_version = 0x0100; /* DTLS1_BAD_VER */
+       }
 
-               vpninfo->dtls_session->master_key_length = sizeof(vpninfo->dtls_secret);
-               memcpy(vpninfo->dtls_session->master_key, vpninfo->dtls_secret,
-                      sizeof(vpninfo->dtls_secret));
+       /* Do this every time; it may have changed due to a rekey */
+       vpninfo->dtls_session->master_key_length = sizeof(vpninfo->dtls_secret);
+       memcpy(vpninfo->dtls_session->master_key, vpninfo->dtls_secret,
+              sizeof(vpninfo->dtls_secret));
 
-               vpninfo->dtls_session->session_id_length = sizeof(vpninfo->dtls_session_id);
-               memcpy(vpninfo->dtls_session->session_id, vpninfo->dtls_session_id,
-                      sizeof(vpninfo->dtls_session_id));
-
-               vpninfo->dtls_session->cipher = https_cipher;
-               vpninfo->dtls_session->cipher_id = https_cipher->id;
-       }
+       vpninfo->dtls_session->session_id_length = sizeof(vpninfo->dtls_session_id);
+       memcpy(vpninfo->dtls_session->session_id, vpninfo->dtls_session_id,
+              sizeof(vpninfo->dtls_session_id));
 
        dtls_ssl = SSL_new(vpninfo->dtls_ctx);
        SSL_set_connect_state(dtls_ssl);
-       SSL_set_cipher_list(dtls_ssl, SSL_CIPHER_get_name(https_cipher));
+
+       ciphers = SSL_get_ciphers(dtls_ssl);
+       if (sk_SSL_CIPHER_num(ciphers) != 1) {
+               vpn_progress(vpninfo, PRG_ERR, _("Not precisely one DTLS cipher\n"));
+               SSL_CTX_free(vpninfo->dtls_ctx);
+               SSL_free(dtls_ssl);
+               SSL_SESSION_free(vpninfo->dtls_session);
+               vpninfo->dtls_ctx = NULL;
+               vpninfo->dtls_session = NULL;
+               vpninfo->dtls_attempt_period = 0;
+               return -EINVAL;
+       }
+       dtls_cipher = sk_SSL_CIPHER_value(ciphers, 0);
+
+       /* Set the appropriate cipher on our session to be resumed */
+       vpninfo->dtls_session->cipher = dtls_cipher;
+       vpninfo->dtls_session->cipher_id = dtls_cipher->id;
 
        /* Add the generated session to the SSL */
        if (!SSL_set_session(dtls_ssl, vpninfo->dtls_session)) {
-               vpninfo->progress(vpninfo, PRG_ERR,
-                                 "SSL_set_session() failed with old protocol version 0x%x\n"
-                                 "Your OpenSSL may lack Cisco compatibility support\n"
-                                 "See http://rt.openssl.org/Ticket/Display.html?id=1751\n"
-                                 "Use the --no-dtls command line option to avoid this message\n",
-                                 vpninfo->dtls_session->ssl_version);
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("SSL_set_session() failed with old protocol version 0x%x\n"
+                              "Are you using a version of OpenSSL older than 0.9.8m?\n"
+                              "See http://rt.openssl.org/Ticket/Display.html?id=1751\n"
+                              "Use the --no-dtls command line option to avoid this message\n"),
+                            vpninfo->dtls_session->ssl_version);
+               vpninfo->dtls_attempt_period = 0;
                return -EINVAL;
        }
 
-       /* Go Go Go! */
        dtls_bio = BIO_new_socket(dtls_fd, BIO_NOCLOSE);
+       /* Set non-blocking */
+       BIO_set_nbio(dtls_bio, 1);
        SSL_set_bio(dtls_ssl, dtls_bio, dtls_bio);
 
-#ifndef SSL_OP_CISCO_ANYCONNECT
-#define SSL_OP_CISCO_ANYCONNECT 0x8000
-#endif
        SSL_set_options(dtls_ssl, SSL_OP_CISCO_ANYCONNECT);
 
-       /* Set non-blocking */
-       BIO_set_nbio(SSL_get_rbio(dtls_ssl),1);
-       BIO_set_nbio(SSL_get_wbio(dtls_ssl),1);
-
-       fcntl(dtls_fd, F_SETFL, fcntl(dtls_fd, F_GETFL) | O_NONBLOCK);
-
-       vpninfo->new_dtls_fd = dtls_fd;
        vpninfo->new_dtls_ssl = dtls_ssl;
-       
-       if (vpninfo->select_nfds <= dtls_fd)
-               vpninfo->select_nfds = dtls_fd + 1;
-
-       FD_SET(dtls_fd, &vpninfo->select_rfds);
-       FD_SET(dtls_fd, &vpninfo->select_efds);
 
-       time(&vpninfo->new_dtls_started);
-       return dtls_try_handshake(vpninfo);
+       return 0;
 }
 
 int dtls_try_handshake(struct openconnect_info *vpninfo)
@@ -205,7 +218,7 @@ int dtls_try_handshake(struct openconnect_info *vpninfo)
        int ret = SSL_do_handshake(vpninfo->new_dtls_ssl);
 
        if (ret == 1) {
-               vpninfo->progress(vpninfo, PRG_INFO, "Established DTLS connection\n");
+               vpn_progress(vpninfo, PRG_INFO, _("Established DTLS connection (using OpenSSL)\n"));
 
                if (vpninfo->dtls_ssl) {
                        /* We are replacing an old connection */
@@ -221,9 +234,65 @@ int dtls_try_handshake(struct openconnect_info *vpninfo)
                vpninfo->new_dtls_ssl = NULL;
                vpninfo->new_dtls_fd = -1;
 
-               vpninfo->dtls_times.last_rekey = vpninfo->dtls_times.last_rx =
-                       vpninfo->dtls_times.last_tx = time(NULL);
-
+               vpninfo->dtls_times.last_rx = vpninfo->dtls_times.last_tx = time(NULL);
+
+               /* From about 8.4.1(11) onwards, the ASA seems to get
+                  very unhappy if we resend ChangeCipherSpec messages
+                  after the initial setup. This was "fixed" in OpenSSL
+                  1.0.0e for RT#2505, but it's not clear if that was
+                  the right fix. What happens if the original packet
+                  *does* get lost? Surely we *wanted* the retransmits,
+                  because without them the server will never be able
+                  to decrypt anything we send?
+                  Oh well, our retransmitted packets upset the server
+                  because we don't get the Cisco-compatibility right
+                  (this is one of the areas in which Cisco's DTLS differs
+                  from the RFC4347 spec), and DPD should help us notice
+                  if *nothing* is getting through. */
+#if OPENSSL_VERSION_NUMBER >= 0x1000005fL
+               /* OpenSSL 1.0.0e or above doesn't resend anyway; do nothing.
+                  However, if we were *built* against 1.0.0e or newer, but at
+                  runtime we find that we are being run against an older 
+                  version, warn about it. */
+               if (SSLeay() < 0x1000005fL) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Your OpenSSL is older than the one you built against, so DTLS may fail!"));
+               }
+#elif defined (HAVE_DTLS1_STOP_TIMER)
+               /*
+                * This works for any normal OpenSSL that supports
+                * Cisco DTLS compatibility (0.9.8m to 1.0.0d inclusive,
+                * and even later versions although it isn't needed there.
+                */
+               dtls1_stop_timer(vpninfo->dtls_ssl);
+#elif defined (BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT)
+               /*
+                * Debian restricts visibility of dtls1_stop_timer()
+                * so do it manually. This version also works on all
+                * sane versions of OpenSSL:
+                */
+               memset (&(vpninfo->dtls_ssl->d1->next_timeout), 0,
+                       sizeof((vpninfo->dtls_ssl->d1->next_timeout)));
+               vpninfo->dtls_ssl->d1->timeout_duration = 1;
+               BIO_ctrl(SSL_get_rbio(vpninfo->dtls_ssl),
+                        BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT, 0,
+                        &(vpninfo->dtls_ssl->d1->next_timeout));
+#elif defined (BIO_CTRL_DGRAM_SET_TIMEOUT)
+               /*
+                * OK, here it gets more fun... this shoul handle the case
+                * of older OpenSSL which has the Cisco DTLS compatibility
+                * backported, but *not* the fix for RT#1922.
+                */
+               BIO_ctrl(SSL_get_rbio(vpninfo->dtls_ssl),
+                        BIO_CTRL_DGRAM_SET_TIMEOUT, 0, NULL);
+#else
+               /*
+                * And if they don't have any of the above, they probably
+                * don't have RT#1829 fixed either, but that's OK because
+                * that's the "fix" that *introduces* the timeout we're
+                * trying to disable. So do nothing...
+                */
+#endif
                return 0;
        }
 
@@ -231,11 +300,11 @@ int dtls_try_handshake(struct openconnect_info *vpninfo)
        if (ret == SSL_ERROR_WANT_WRITE || ret == SSL_ERROR_WANT_READ) {
                if (time(NULL) < vpninfo->new_dtls_started + 5)
                        return 0;
-               vpninfo->progress(vpninfo, PRG_TRACE, "DTLS handshake timed out\n");
+               vpn_progress(vpninfo, PRG_TRACE, _("DTLS handshake timed out\n"));
        }
 
-       vpninfo->progress(vpninfo, PRG_ERR, "DTLS handshake failed: %d\n", ret);
-       ERR_print_errors_fp(stderr);
+       vpn_progress(vpninfo, PRG_ERR, _("DTLS handshake failed: %d\n"), ret);
+       openconnect_report_ssl_errors(vpninfo);
 
        /* Kill the new (failed) connection... */
        SSL_free(vpninfo->new_dtls_ssl);
@@ -262,10 +331,265 @@ int dtls_try_handshake(struct openconnect_info *vpninfo)
        return -EINVAL;
 }
 
+#elif defined (DTLS_GNUTLS)
+#include <gnutls/dtls.h>
+
+struct {
+       const char *name;
+       gnutls_cipher_algorithm_t cipher;
+       gnutls_mac_algorithm_t mac;
+       const char *prio;
+} gnutls_dtls_ciphers[] = {
+       { "AES128-SHA", GNUTLS_CIPHER_AES_128_CBC, GNUTLS_MAC_SHA1,
+         "NONE:+VERS-DTLS0.9:+COMP-NULL:+AES-128-CBC:+SHA1:+RSA:%COMPAT:%DISABLE_SAFE_RENEGOTIATION" },
+       { "DES-CBC3-SHA", GNUTLS_CIPHER_3DES_CBC, GNUTLS_MAC_SHA1,
+         "NONE:+VERS-DTLS0.9:+COMP-NULL:+3DES-CBC:+SHA1:+RSA:%COMPAT:%DISABLE_SAFE_RENEGOTIATION" },
+};
+
+#define DTLS_SEND gnutls_record_send
+#define DTLS_RECV gnutls_record_recv
+static int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
+{
+       gnutls_session_t dtls_ssl;
+       gnutls_datum_t master_secret, session_id;
+       int err;
+       int cipher;
+
+       for (cipher = 0; cipher < sizeof(gnutls_dtls_ciphers)/sizeof(gnutls_dtls_ciphers[0]); cipher++) {
+               if (!strcmp(vpninfo->dtls_cipher, gnutls_dtls_ciphers[cipher].name))
+                       goto found_cipher;
+       }
+       vpn_progress(vpninfo, PRG_ERR, _("Unknown DTLS parameters for requested CipherSuite '%s'\n"),
+                    vpninfo->dtls_cipher);
+       vpninfo->dtls_attempt_period = 0;
+
+       return -EINVAL;
+
+ found_cipher:
+       gnutls_init(&dtls_ssl, GNUTLS_CLIENT|GNUTLS_DATAGRAM|GNUTLS_NONBLOCK);
+       err = gnutls_priority_set_direct(dtls_ssl,
+                                        gnutls_dtls_ciphers[cipher].prio,
+                                        NULL);
+       if (err) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to set DTLS priority: %s\n"),
+                            gnutls_strerror(err));
+               gnutls_deinit(dtls_ssl);
+               vpninfo->dtls_attempt_period = 0;
+               return -EINVAL;
+       }
+       gnutls_transport_set_ptr(dtls_ssl,
+                                (gnutls_transport_ptr_t)(long) dtls_fd);
+       gnutls_record_disable_padding(dtls_ssl);
+       master_secret.data = vpninfo->dtls_secret;
+       master_secret.size = sizeof(vpninfo->dtls_secret);
+       session_id.data = vpninfo->dtls_session_id;
+       session_id.size = sizeof(vpninfo->dtls_session_id);
+       err = gnutls_session_set_premaster(dtls_ssl, GNUTLS_CLIENT, GNUTLS_DTLS0_9,
+                                          GNUTLS_KX_RSA, gnutls_dtls_ciphers[cipher].cipher,
+                                          gnutls_dtls_ciphers[cipher].mac, GNUTLS_COMP_NULL,
+                                          &master_secret, &session_id);
+       if (err) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to set DTLS session parameters: %s\n"),
+                            gnutls_strerror(err));
+               gnutls_deinit(dtls_ssl);
+               vpninfo->dtls_attempt_period = 0;
+               return -EINVAL;
+       }
+
+       vpninfo->new_dtls_ssl = dtls_ssl;
+       return 0;
+}
+
+int dtls_try_handshake(struct openconnect_info *vpninfo)
+{
+       int err = gnutls_handshake(vpninfo->new_dtls_ssl);
+
+       if (!err) {
+#ifdef HAVE_GNUTLS_DTLS_SET_DATA_MTU
+               /* Make sure GnuTLS's idea of the MTU is sufficient to take
+                  a full VPN MTU (with 1-byte header) in a data record. */
+               err = gnutls_dtls_set_data_mtu(vpninfo->new_dtls_ssl, vpninfo->actual_mtu + 1);
+               if (err) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to set DTLS MTU: %s\n"),
+                                    gnutls_strerror(err));
+                       goto error;
+               }
+#else
+               /* If we don't have gnutls_dtls_set_data_mtu() then make sure
+                  we leave enough headroom by adding the worst-case overhead.
+                  We only support AES128-CBC and DES-CBC3-SHA anyway, so
+                  working out the worst case isn't hard. */
+               gnutls_dtls_set_mtu(vpninfo->new_dtls_ssl,
+                                   vpninfo->actual_mtu + 1 /* packet + header */
+                                   + 13 /* DTLS header */
+                                   + 20 /* biggest supported MAC (SHA1) */
+                                   + 16 /* biggest supported IV (AES-128) */
+                                   + 16 /* max padding */);
+#endif
+
+               vpn_progress(vpninfo, PRG_INFO, _("Established DTLS connection (using GnuTLS)\n"));
+
+               if (vpninfo->dtls_ssl) {
+                       /* We are replacing an old connection */
+                       gnutls_deinit(vpninfo->dtls_ssl);
+                       close(vpninfo->dtls_fd);
+                       FD_CLR(vpninfo->dtls_fd, &vpninfo->select_rfds);
+                       FD_CLR(vpninfo->dtls_fd, &vpninfo->select_wfds);
+                       FD_CLR(vpninfo->dtls_fd, &vpninfo->select_efds);
+               }
+               vpninfo->dtls_ssl = vpninfo->new_dtls_ssl;
+               vpninfo->dtls_fd = vpninfo->new_dtls_fd;
+
+               vpninfo->new_dtls_ssl = NULL;
+               vpninfo->new_dtls_fd = -1;
+
+               vpninfo->dtls_times.last_rx = vpninfo->dtls_times.last_tx = time(NULL);
+
+               /* XXX: For OpenSSL we explicitly prevent retransmits here. */
+               return 0;
+       }
+
+       if (err == GNUTLS_E_AGAIN) {
+               if (time(NULL) < vpninfo->new_dtls_started + 5)
+                       return 0;
+               vpn_progress(vpninfo, PRG_TRACE, _("DTLS handshake timed out\n"));
+       }
+
+       vpn_progress(vpninfo, PRG_ERR, _("DTLS handshake failed: %s\n"),
+                    gnutls_strerror(err));
+
+       goto error;
+ error:
+       /* Kill the new (failed) connection... */
+       gnutls_deinit(vpninfo->new_dtls_ssl);
+       FD_CLR(vpninfo->new_dtls_fd, &vpninfo->select_rfds);
+       FD_CLR(vpninfo->new_dtls_fd, &vpninfo->select_efds);
+       close(vpninfo->new_dtls_fd);
+       vpninfo->new_dtls_ssl = NULL;
+       vpninfo->new_dtls_fd = -1;
+
+       /* ... and kill the old one too. The only time there'll be a valid
+          existing session is when it was a rekey, and in that case it's
+          time for the old one to die. */
+       if (vpninfo->dtls_ssl) {
+               gnutls_deinit(vpninfo->dtls_ssl);
+               close(vpninfo->dtls_fd);
+               FD_CLR(vpninfo->dtls_fd, &vpninfo->select_rfds);
+               FD_CLR(vpninfo->dtls_fd, &vpninfo->select_wfds);
+               FD_CLR(vpninfo->dtls_fd, &vpninfo->select_efds);
+               vpninfo->dtls_ssl = NULL;
+               vpninfo->dtls_fd = -1;
+       }
+
+       time(&vpninfo->new_dtls_started);
+       return -EINVAL;
+}
+#endif
+
+int connect_dtls_socket(struct openconnect_info *vpninfo)
+{
+       int dtls_fd, ret;
+
+       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;
+       }
+
+       dtls_fd = socket(vpninfo->peer_addr->sa_family, SOCK_DGRAM, IPPROTO_UDP);
+       if (dtls_fd < 0) {
+               perror(_("Open UDP socket for DTLS:"));
+               return -EINVAL;
+       }
+
+       if (vpninfo->dtls_local_port) {
+               union {
+                       struct sockaddr_in in;
+                       struct sockaddr_in6 in6;
+               } dtls_bind_addr;
+               int dtls_bind_addrlen;
+               memset(&dtls_bind_addr, 0, sizeof(dtls_bind_addr));
+
+               if (vpninfo->peer_addr->sa_family == AF_INET) {
+                       struct sockaddr_in *addr = &dtls_bind_addr.in;
+                       dtls_bind_addrlen = sizeof(*addr);
+                       addr->sin_family = AF_INET;
+                       addr->sin_addr.s_addr = INADDR_ANY;
+                       addr->sin_port = htons(vpninfo->dtls_local_port);
+               } else if (vpninfo->peer_addr->sa_family == AF_INET6) {
+                       struct sockaddr_in6 *addr = &dtls_bind_addr.in6;
+                       dtls_bind_addrlen = sizeof(*addr);
+                       addr->sin6_family = AF_INET6;
+                       addr->sin6_addr = in6addr_any;
+                       addr->sin6_port = htons(vpninfo->dtls_local_port);
+               } else {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Unknown protocol family %d. Cannot do DTLS\n"),
+                                    vpninfo->peer_addr->sa_family);
+                       vpninfo->dtls_attempt_period = 0;
+                       close(dtls_fd);
+                       return -EINVAL;
+               }
+
+               if (bind(dtls_fd, (struct sockaddr *)&dtls_bind_addr, dtls_bind_addrlen)) {
+                       perror(_("Bind UDP socket for DTLS"));
+                       close(dtls_fd);
+                       return -EINVAL;
+               }
+       }
+
+       if (connect(dtls_fd, vpninfo->dtls_addr, vpninfo->peer_addrlen)) {
+               perror(_("UDP (DTLS) connect:\n"));
+               close(dtls_fd);
+               return -EINVAL;
+       }
+
+       fcntl(dtls_fd, F_SETFD, FD_CLOEXEC);
+       fcntl(dtls_fd, F_SETFL, fcntl(dtls_fd, F_GETFL) | O_NONBLOCK);
+
+       ret = start_dtls_handshake(vpninfo, dtls_fd);
+       if (ret) {
+               close(dtls_fd);
+               return ret;
+       }
+
+       vpninfo->new_dtls_fd = dtls_fd;
+       if (vpninfo->select_nfds <= dtls_fd)
+               vpninfo->select_nfds = dtls_fd + 1;
+
+       FD_SET(dtls_fd, &vpninfo->select_rfds);
+       FD_SET(dtls_fd, &vpninfo->select_efds);
+
+       time(&vpninfo->new_dtls_started);
+
+       return dtls_try_handshake(vpninfo);
+}
+
 static int dtls_restart(struct openconnect_info *vpninfo)
 {
        if (vpninfo->dtls_ssl) {
+#if defined (DTLS_OPENSSL)
                SSL_free(vpninfo->dtls_ssl);
+#elif defined (DTLS_GNUTLS)
+               gnutls_deinit(vpninfo->dtls_ssl);
+#endif
                close(vpninfo->dtls_fd);
                FD_CLR(vpninfo->dtls_fd, &vpninfo->select_rfds);
                FD_CLR(vpninfo->dtls_fd, &vpninfo->select_wfds);
@@ -281,106 +605,137 @@ static int dtls_restart(struct openconnect_info *vpninfo)
 int setup_dtls(struct openconnect_info *vpninfo)
 {
        struct vpn_option *dtls_opt = vpninfo->dtls_options;
-       int sessid_found = 0;
        int dtls_port = 0;
-       int i;
+
+#if defined (OPENCONNECT_GNUTLS) && defined (DTLS_OPENSSL)
+       /* If we're using GnuTLS for authentication but OpenSSL for DTLS,
+          we'll need to initialise OpenSSL now... */
+       SSL_library_init ();
+       ERR_clear_error ();
+       SSL_load_error_strings ();
+       OpenSSL_add_all_algorithms ();
+#endif
 
        while (dtls_opt) {
-               vpninfo->progress(vpninfo, PRG_TRACE,
-                                 "DTLS option %s : %s\n",
-                                 dtls_opt->option, dtls_opt->value);
-
-               if (!strcmp(dtls_opt->option, "X-DTLS-Session-ID")) {
-                       if (strlen(dtls_opt->value) != 64) {
-                               vpninfo->progress(vpninfo, PRG_ERR, "X-DTLS-Session-ID not 64 characters\n");
-                               vpninfo->progress(vpninfo, PRG_ERR, "Is: %s\n", dtls_opt->value);
-                               return -EINVAL;
-                       }
-                       for (i = 0; i < 64; i += 2)
-                               vpninfo->dtls_session_id[i/2] = hex(dtls_opt->value + i);
-                       sessid_found = 1;
-               } else if (!strcmp(dtls_opt->option + 7, "Port")) {
+               vpn_progress(vpninfo, PRG_TRACE,
+                            _("DTLS option %s : %s\n"),
+                            dtls_opt->option, dtls_opt->value);
+
+               if (!strcmp(dtls_opt->option + 7, "Port")) {
                        dtls_port = atol(dtls_opt->value);
                } else if (!strcmp(dtls_opt->option + 7, "Keepalive")) {
                        vpninfo->dtls_times.keepalive = atol(dtls_opt->value);
                } else if (!strcmp(dtls_opt->option + 7, "DPD")) {
-                       vpninfo->dtls_times.dpd = atol(dtls_opt->value);
+                       int j = atol(dtls_opt->value);
+                       if (j && (!vpninfo->dtls_times.dpd || j < vpninfo->dtls_times.dpd))
+                               vpninfo->dtls_times.dpd = j;
                } else if (!strcmp(dtls_opt->option + 7, "Rekey-Time")) {
                        vpninfo->dtls_times.rekey = atol(dtls_opt->value);
+               } else if (!strcmp(dtls_opt->option + 7, "CipherSuite")) {
+                       vpninfo->dtls_cipher = strdup(dtls_opt->value);
                }
-                       
+
                dtls_opt = dtls_opt->next;
        }
-       if (!sessid_found || !dtls_port)
+       if (!dtls_port) {
+               vpninfo->dtls_attempt_period = 0;
                return -EINVAL;
+       }
+
+       vpninfo->dtls_addr = malloc(vpninfo->peer_addrlen);
+       if (!vpninfo->dtls_addr) {
+               vpninfo->dtls_attempt_period = 0;
+               return -ENOMEM;
+       }
+       memcpy(vpninfo->dtls_addr, vpninfo->peer_addr, vpninfo->peer_addrlen);
 
        if (vpninfo->peer_addr->sa_family == AF_INET) {
-               struct sockaddr_in *sin = (void *)vpninfo->peer_addr;
+               struct sockaddr_in *sin = (void *)vpninfo->dtls_addr;
                sin->sin_port = htons(dtls_port);
        } else if (vpninfo->peer_addr->sa_family == AF_INET6) {
-               struct sockaddr_in6 *sin = (void *)vpninfo->peer_addr;
+               struct sockaddr_in6 *sin = (void *)vpninfo->dtls_addr;
                sin->sin6_port = htons(dtls_port);
        } else {
-               vpninfo->progress(vpninfo, PRG_ERR, "Unknown protocol family %d. Cannot do DTLS\n",
-                       vpninfo->peer_addr->sa_family);
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unknown protocol family %d. Cannot do DTLS\n"),
+                            vpninfo->peer_addr->sa_family);
+               vpninfo->dtls_attempt_period = 0;
                return -EINVAL;
        }
 
-       
        if (connect_dtls_socket(vpninfo))
                return -EINVAL;
 
-       vpninfo->progress(vpninfo, PRG_TRACE,
-                         "DTLS connected. DPD %d, Keepalive %d\n",
-                         vpninfo->dtls_times.dpd, vpninfo->dtls_times.keepalive);
+       vpn_progress(vpninfo, PRG_TRACE,
+                    _("DTLS connected. DPD %d, Keepalive %d\n"),
+                    vpninfo->dtls_times.dpd, vpninfo->dtls_times.keepalive);
 
        return 0;
 }
 
+static struct pkt *dtls_pkt;
+
 int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
 {
-       unsigned char buf[2000];
-       int len;
        int work_done = 0;
        char magic_pkt;
 
-       while ( (len = SSL_read(vpninfo->dtls_ssl, buf, sizeof(buf))) > 0 ) {
+       while (1) {
+               int len = vpninfo->actual_mtu;
+               unsigned char *buf;
+
+               if (!dtls_pkt) {
+                       dtls_pkt = malloc(sizeof(struct pkt) + len);
+                       if (!dtls_pkt) {
+                               vpn_progress(vpninfo, PRG_ERR, "Allocation failed\n");
+                               break;
+                       }
+               }
+
+               buf = dtls_pkt->data - 1;
+               len = DTLS_RECV(vpninfo->dtls_ssl, buf, len + 1);
+               if (len <= 0)
+                       break;
 
-               vpninfo->progress(vpninfo, PRG_TRACE,
-                                 "Received DTLS packet 0x%02x of %d bytes\n",
-                                 buf[0], len);
+               vpn_progress(vpninfo, PRG_TRACE,
+                            _("Received DTLS packet 0x%02x of %d bytes\n"),
+                            buf[0], len);
 
                vpninfo->dtls_times.last_rx = time(NULL);
 
                switch(buf[0]) {
                case AC_PKT_DATA:
-                       queue_new_packet(&vpninfo->incoming_queue, AF_INET, buf+1, len-1);
+                       dtls_pkt->len = len - 1;
+                       queue_packet(&vpninfo->incoming_queue, dtls_pkt);
+                       dtls_pkt = NULL;
                        work_done = 1;
                        break;
 
                case AC_PKT_DPD_OUT:
-                       vpninfo->progress(vpninfo, PRG_TRACE, "Got DTLS DPD request\n");
+                       vpn_progress(vpninfo, PRG_TRACE, _("Got DTLS DPD request\n"));
 
                        /* FIXME: What if the packet doesn't get through? */
                        magic_pkt = AC_PKT_DPD_RESP;
-                       if (SSL_write(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
-                               vpninfo->progress(vpninfo, PRG_ERR, "Failed to send DPD response. Expect disconnect\n");
+                       if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
+                               vpn_progress(vpninfo, PRG_ERR,
+                                            _("Failed to send DPD response. Expect disconnect\n"));
                        continue;
 
                case AC_PKT_DPD_RESP:
-                       vpninfo->progress(vpninfo, PRG_TRACE, "Got DTLS DPD response\n");
+                       vpn_progress(vpninfo, PRG_TRACE, _("Got DTLS DPD response\n"));
                        break;
 
                case AC_PKT_KEEPALIVE:
-                       vpninfo->progress(vpninfo, PRG_TRACE, "Got DTLS Keepalive\n");
+                       vpn_progress(vpninfo, PRG_TRACE, _("Got DTLS Keepalive\n"));
                        break;
 
                default:
-                       vpninfo->progress(vpninfo, PRG_ERR,
-                                         "Unknown DTLS packet type %02x, len %d\n", buf[0], len);
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Unknown DTLS packet type %02x, len %d\n"),
+                                    buf[0], len);
                        if (1) {
                                /* Some versions of OpenSSL have bugs with receiving out-of-order
-                                * packets. Not only do they wrongly decide to drop packets if 
+                                * packets. Not only do they wrongly decide to drop packets if
                                 * 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. */
@@ -395,10 +750,18 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
 
        switch (keepalive_action(&vpninfo->dtls_times, timeout)) {
        case KA_REKEY:
-               time(&vpninfo->dtls_times.last_rekey);
-               vpninfo->progress(vpninfo, PRG_TRACE, "DTLS rekey due\n");
-               if (connect_dtls_socket(vpninfo)) {
-                       vpninfo->progress(vpninfo, PRG_ERR, "DTLS rekey failed\n");
+               vpn_progress(vpninfo, PRG_INFO, _("DTLS rekey due\n"));
+
+               /* There ought to be a method of rekeying DTLS without tearing down
+                  the CSTP session and restarting, but we don't (yet) know it */
+               if (cstp_reconnect(vpninfo)) {
+                       vpn_progress(vpninfo, PRG_ERR, _("Reconnect failed\n"));
+                       vpninfo->quit_reason = "CSTP reconnect failed";
+                       return 1;
+               }
+
+               if (dtls_restart(vpninfo)) {
+                       vpn_progress(vpninfo, PRG_ERR, _("DTLS rekey failed\n"));
                        return 1;
                }
                work_done = 1;
@@ -406,16 +769,19 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
 
 
        case KA_DPD_DEAD:
-               vpninfo->progress(vpninfo, PRG_ERR, "DTLS Dead Peer Detection detected dead peer!\n");
+               vpn_progress(vpninfo, PRG_ERR, _("DTLS Dead Peer Detection detected dead peer!\n"));
                /* Fall back to SSL, and start a new DTLS connection */
                dtls_restart(vpninfo);
                return 1;
 
        case KA_DPD:
-               vpninfo->progress(vpninfo, PRG_TRACE, "Send DTLS DPD\n");
+               vpn_progress(vpninfo, PRG_TRACE, _("Send DTLS DPD\n"));
 
                magic_pkt = AC_PKT_DPD_OUT;
-               SSL_write(vpninfo->dtls_ssl, &magic_pkt, 1);
+               if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to send DPD request. Expect disconnect\n"));
+
                /* last_dpd will just have been set */
                vpninfo->dtls_times.last_tx = vpninfo->dtls_times.last_dpd;
                work_done = 1;
@@ -427,10 +793,12 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
                if (vpninfo->outgoing_queue)
                        break;
 
-               vpninfo->progress(vpninfo, PRG_TRACE, "Send DTLS Keepalive\n");
+               vpn_progress(vpninfo, PRG_TRACE, _("Send DTLS Keepalive\n"));
 
                magic_pkt = AC_PKT_KEEPALIVE;
-               SSL_write(vpninfo->dtls_ssl, &magic_pkt, 1);
+               if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to send keepalive request. Expect disconnect\n"));
                time(&vpninfo->dtls_times.last_tx);
                work_done = 1;
                break;
@@ -449,7 +817,8 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
 
                /* One byte of header */
                this->hdr[7] = AC_PKT_DATA;
-               
+
+#if defined(DTLS_OPENSSL)
                ret = SSL_write(vpninfo->dtls_ssl, &this->hdr[7], this->len + 1);
                if (ret <= 0) {
                        ret = SSL_get_error(vpninfo->dtls_ssl, ret);
@@ -457,22 +826,46 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
                        /* If it's a real error, kill the DTLS connection and
                           requeue the packet to be sent over SSL */
                        if (ret != SSL_ERROR_WANT_READ && ret != SSL_ERROR_WANT_WRITE) {
-                               vpninfo->progress(vpninfo, PRG_ERR, 
-                                                 "DTLS got write error %d. Falling back to SSL\n", ret);
-                               ERR_print_errors_fp(stderr);
+                               vpn_progress(vpninfo, PRG_ERR,
+                                            _("DTLS got write error %d. Falling back to SSL\n"),
+                                            ret);
+                               openconnect_report_ssl_errors(vpninfo);
                                dtls_restart(vpninfo);
                                vpninfo->outgoing_queue = this;
                                vpninfo->outgoing_qlen++;
                        }
                        return 1;
                }
+#elif defined (DTLS_GNUTLS)
+               ret = gnutls_record_send(vpninfo->dtls_ssl, &this->hdr[7], this->len + 1);
+               if (ret <= 0) {
+                       if (ret != GNUTLS_E_AGAIN) {
+                               vpn_progress(vpninfo, PRG_ERR,
+                                            _("DTLS got write error: %s. Falling back to SSL\n"),
+                                            gnutls_strerror(ret));
+                               dtls_restart(vpninfo);
+                               vpninfo->outgoing_queue = this;
+                               vpninfo->outgoing_qlen++;
+                       }
+                       return 1;
+               }
+#endif
                time(&vpninfo->dtls_times.last_tx);
-               vpninfo->progress(vpninfo, PRG_TRACE,
-                                 "Sent DTLS packet of %d bytes; SSL_write() returned %d\n",
-                                 this->len, ret);
+               vpn_progress(vpninfo, PRG_TRACE,
+                            _("Sent DTLS packet of %d bytes; DTLS send returned %d\n"),
+                            this->len, ret);
+               free(this);
        }
 
        return work_done;
 }
-
+#else /* !HAVE_DTLS */
+#warning Your SSL library does not seem to support Cisco DTLS compatibility
+ int setup_dtls(struct openconnect_info *vpninfo)
+{
+       vpn_progress(vpninfo, PRG_ERR,
+                    _("Built against SSL library with no Cisco DTLS support\n"));
+       return -EINVAL;
+}
+#endif