/*
- * Open AnyConnect (SSL + DTLS) client
+ * OpenConnect (SSL + DTLS) VPN client
*
- * © 2008 David Woodhouse <dwmw2@infradead.org>
+ * Copyright © 2008-2012 Intel Corporation.
*
- * Permission to use, copy, modify, and/or distribute this software
- * for any purpose with or without fee is hereby granted, provided
- * that the above copyright notice and this permission notice appear
- * in all copies.
+ * Author: David Woodhouse <dwmw2@infradead.org>
*
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
- * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
- * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
- * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
- * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
- * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
- * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to:
+ *
+ * Free Software Foundation, Inc.
+ * 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA
*/
#include <errno.h>
#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 "anyconnect.h"
-
-/*
- * 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.
- *
- * This code works when run against Cisco's own libssl.so.0.9.8, but
- * fails (Bad Record MAC on receipt of the Server Hello) when run
- * against my own build of OpenSSL-0.9.8f.
- *
- * It lookslike they've reverted _some_ of the changes beween 0.9.8e
- * and 0.9.8f, but not all of them. In particular, they use
- * DTLS1_BAD_VER for the protocol version.
- *
- * Using OpenSSL-0.9.8e, which was the last release of OpenSSL to use
- * DTLS1_BAD_VER, also fails similarly.
- *
- * Hopefully they're just using something equivalent to a snapshot
- * between 0.9.8e and 0.9.8f, and they don't have their own "special"
- * changes on top.
- */
+#include "openconnect-internal.h"
static unsigned char nybble(unsigned char n)
{
return 0;
}
-static unsigned char hex(const char *data)
+unsigned char unhex(const char *data)
{
return (nybble(data[0]) << 4) | nybble(data[1]);
}
-static int connect_dtls_socket(struct anyconnect_info *vpninfo, int dtls_port)
+#ifdef HAVE_DTLS
+
+#if 0
+/*
+ * Useful for catching test cases, where we want everything to be
+ * reproducible. *NEVER* do this in the wild.
+ */
+time_t time(time_t *t)
+{
+ time_t x = 0x3ab2d948;
+ if (t) *t = x;
+ return x;
+}
+
+int RAND_pseudo_bytes(char *buf, int len)
{
- SSL_METHOD *dtls_method;
- SSL_CTX *dtls_ctx;
- SSL_SESSION *dtls_session;
- SSL_CIPHER *https_cipher;
+ memset(buf, 0x5a, len);
+ printf("FAKE PSEUDO RANDOM!\n");
+ return 1;
+
+}
+int RAND_bytes(char *buf, int len)
+{
+ static int foo = 0x5b;
+ printf("FAKE RANDOM!\n");
+ memset(buf, foo, len);
+ return 1;
+}
+#endif
+
+/*
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+ * my subsequent data packets. So we use the old protocol, which is what
+ * their clients use anyway.
+ */
+
+#if defined (DTLS_OPENSSL)
+#define DTLS_SEND SSL_write
+#define DTLS_RECV SSL_read
+
+#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
+
+static int start_dtls_handshake(struct openconnect_info *vpninfo, int dtls_fd)
+{
+ STACK_OF(SSL_CIPHER) *ciphers;
+ method_const SSL_METHOD *dtls_method;
+ SSL_CIPHER *dtls_cipher;
SSL *dtls_ssl;
BIO *dtls_bio;
- int dtls_fd;
- int ret;
- if (vpninfo->peer_addr->sa_family == AF_INET) {
- struct sockaddr_in *sin = (void *)vpninfo->peer_addr;
- sin->sin_port = htons(dtls_port);
- } else if (vpninfo->peer_addr->sa_family == AF_INET6) {
- struct sockaddr_in6 *sin = (void *)vpninfo->peer_addr;
- sin->sin6_port = htons(dtls_port);
- } else {
- fprintf(stderr, "Unknown protocol family %d. Cannot do DTLS\n",
- vpninfo->peer_addr->sa_family);
+ if (!vpninfo->dtls_ctx) {
+ dtls_method = DTLSv1_client_method();
+ vpninfo->dtls_ctx = SSL_CTX_new(dtls_method);
+ if (!vpninfo->dtls_ctx) {
+ 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) {
+ 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 */
+ }
+
+ /* 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));
+
+ dtls_ssl = SSL_new(vpninfo->dtls_ctx);
+ SSL_set_connect_state(dtls_ssl);
+
+ 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)) {
+ 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;
}
- dtls_fd = socket(vpninfo->peer_addr->sa_family, SOCK_DGRAM, IPPROTO_UDP);
- if (dtls_fd < 0) {
- perror("Open UDP socket for DTLS:");
+ 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);
+
+ SSL_set_options(dtls_ssl, SSL_OP_CISCO_ANYCONNECT);
+
+ vpninfo->new_dtls_ssl = dtls_ssl;
+
+ return 0;
+}
+
+int dtls_try_handshake(struct openconnect_info *vpninfo)
+{
+ int ret = SSL_do_handshake(vpninfo->new_dtls_ssl);
+
+ if (ret == 1) {
+ vpn_progress(vpninfo, PRG_INFO, _("Established DTLS connection (using OpenSSL)\n"));
+
+ if (vpninfo->dtls_ssl) {
+ /* We are replacing an old connection */
+ SSL_free(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);
+
+ /* 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;
+ }
+
+ ret = SSL_get_error(vpninfo->new_dtls_ssl, ret);
+ if (ret == SSL_ERROR_WANT_WRITE || ret == SSL_ERROR_WANT_READ) {
+ 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: %d\n"), ret);
+ openconnect_report_ssl_errors(vpninfo);
+
+ /* Kill the new (failed) connection... */
+ SSL_free(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) {
+ SSL_free(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;
+}
+
+#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;
}
-
- if (connect(dtls_fd, vpninfo->peer_addr, vpninfo->peer_addrlen)) {
- perror("UDP (DTLS) connect:\n");
- close(dtls_fd);
+ 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;
}
- dtls_method = DTLSv1_client_method();
- dtls_ctx = SSL_CTX_new(dtls_method);
- SSL_CTX_set_read_ahead(dtls_ctx, 1);
- https_cipher = SSL_get_current_cipher(vpninfo->https_ssl);
+ vpninfo->new_dtls_ssl = dtls_ssl;
+ return 0;
+}
- dtls_ssl = SSL_new(dtls_ctx);
- SSL_set_connect_state(dtls_ssl);
- SSL_set_cipher_list(dtls_ssl, SSL_CIPHER_get_name(https_cipher));
- printf("SSL_SESSION is %d bytes\n", sizeof(*dtls_session));
- /* We're going to "resume" a session which never existed. Fake it... */
- dtls_session = SSL_SESSION_new();
+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;
- dtls_session->ssl_version = DTLS1_BAD_VER;
+ vpninfo->new_dtls_ssl = NULL;
+ vpninfo->new_dtls_fd = -1;
- dtls_session->master_key_length = sizeof(vpninfo->dtls_secret);
- memcpy(dtls_session->master_key, vpninfo->dtls_secret,
- sizeof(vpninfo->dtls_secret));
+ vpninfo->dtls_times.last_rx = vpninfo->dtls_times.last_tx = time(NULL);
- dtls_session->session_id_length = sizeof(vpninfo->dtls_session_id);
- memcpy(dtls_session->session_id, vpninfo->dtls_session_id,
- sizeof(vpninfo->dtls_session_id));
+ /* XXX: For OpenSSL we explicitly prevent retransmits here. */
+ return 0;
+ }
- dtls_session->cipher = https_cipher;
- dtls_session->cipher_id = https_cipher->id;
+ 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;
+ }
- /* Having faked a session, add it to the CTX and the SSL */
- if (!SSL_set_session(dtls_ssl, dtls_session)) {
- printf("SSL_set_session() failed with old protocol version 0x%x\n", dtls_session->ssl_version);
- printf("Trying the official version %x\n", DTLS1_VERSION);
- dtls_session->ssl_version = DTLS1_VERSION;
- if (!SSL_set_session(dtls_ssl, dtls_session)) {
- printf("SSL_set_session() failed still. Is your build ABI-compatible with your libssl?\n");
+ 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 (!SSL_CTX_add_session(dtls_ctx, dtls_session))
- printf("SSL_CTX_add_session() failed\n");
+ if (connect(dtls_fd, vpninfo->dtls_addr, vpninfo->peer_addrlen)) {
+ perror(_("UDP (DTLS) connect:\n"));
+ close(dtls_fd);
+ return -EINVAL;
+ }
- /* Go Go Go! */
- dtls_bio = BIO_new_socket(dtls_fd, BIO_NOCLOSE);
- SSL_set_bio(dtls_ssl, dtls_bio, dtls_bio);
+ fcntl(dtls_fd, F_SETFD, FD_CLOEXEC);
+ fcntl(dtls_fd, F_SETFL, fcntl(dtls_fd, F_GETFL) | O_NONBLOCK);
- ret = SSL_do_handshake(dtls_ssl);
-
- if (ret != 1) {
- fprintf(stderr, "DTLS connection returned %d\n", ret);
- if (ret < 0)
- fprintf(stderr, "DTLS handshake error: %d\n", SSL_get_error(dtls_ssl, ret));
- ERR_print_errors_fp(stderr);
- SSL_free(dtls_ssl);
- SSL_CTX_free(dtls_ctx);
+ ret = start_dtls_handshake(vpninfo, dtls_fd);
+ if (ret) {
close(dtls_fd);
- return -EINVAL;
+ return ret;
}
- vpninfo->dtls_fd = dtls_fd;
- vpninfo->dtls_ssl = dtls_ssl;
+ vpninfo->new_dtls_fd = dtls_fd;
+ if (vpninfo->select_nfds <= dtls_fd)
+ vpninfo->select_nfds = dtls_fd + 1;
- return 0;
+ FD_SET(dtls_fd, &vpninfo->select_rfds);
+ FD_SET(dtls_fd, &vpninfo->select_efds);
+
+ time(&vpninfo->new_dtls_started);
+
+ return dtls_try_handshake(vpninfo);
}
-int setup_dtls(struct anyconnect_info *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);
+ FD_CLR(vpninfo->dtls_fd, &vpninfo->select_efds);
+ vpninfo->dtls_ssl = NULL;
+ vpninfo->dtls_fd = -1;
+ }
+
+ return connect_dtls_socket(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) {
- if (verbose)
- printf("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) {
- fprintf(stderr, "X-DTLS-Session-ID not 64 characters\n");
- fprintf(stderr, "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_keepalive = atol(dtls_opt->value);
+ vpninfo->dtls_times.keepalive = atol(dtls_opt->value);
} else if (!strcmp(dtls_opt->option + 7, "DPD")) {
- vpninfo->dtls_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)
- return -EINVAL;
-
- if (connect_dtls_socket(vpninfo, dtls_port))
+ if (!dtls_port) {
+ vpninfo->dtls_attempt_period = 0;
return -EINVAL;
+ }
- BIO_set_nbio(SSL_get_rbio(vpninfo->dtls_ssl),1);
- BIO_set_nbio(SSL_get_wbio(vpninfo->dtls_ssl),1);
+ 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);
- fcntl(vpninfo->dtls_fd, F_SETFL, fcntl(vpninfo->dtls_fd, F_GETFL) | O_NONBLOCK);
+ if (vpninfo->peer_addr->sa_family == AF_INET) {
+ 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->dtls_addr;
+ sin->sin6_port = htons(dtls_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;
+ return -EINVAL;
+ }
- vpn_add_pollfd(vpninfo, vpninfo->ssl_fd, POLLIN|POLLHUP|POLLERR);
- vpninfo->last_dtls_rx = vpninfo->last_dtls_tx = time(NULL);
+ if (connect_dtls_socket(vpninfo))
+ return -EINVAL;
- if (verbose)
- printf("DTLS connected. DPD %d, Keepalive %d\n",
- vpninfo->dtls_dpd, vpninfo->dtls_keepalive);
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("DTLS connected. DPD %d, Keepalive %d\n"),
+ vpninfo->dtls_times.dpd, vpninfo->dtls_times.keepalive);
return 0;
}
-int dtls_mainloop(struct anyconnect_info *vpninfo, int *timeout)
+static struct pkt *dtls_pkt;
+
+int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)
{
- char buf[2000];
- int len;
int work_done = 0;
+ char magic_pkt;
+
+ while (1) {
+ int len = vpninfo->actual_mtu;
+ unsigned char *buf;
- while ( (len = SSL_read(vpninfo->dtls_ssl, buf, sizeof(buf))) > 0 ) {
- if (verbose) {
- printf("Received DTLS packet of %d bytes\n", len);
- printf("Packet starts %02x %02x %02x %02x %02x %02x %02x %02x\n",
- buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]);
+ if (!dtls_pkt) {
+ dtls_pkt = malloc(sizeof(struct pkt) + len);
+ if (!dtls_pkt) {
+ vpn_progress(vpninfo, PRG_ERR, "Allocation failed\n");
+ break;
+ }
}
- vpninfo->last_dtls_rx = time(NULL);
+
+ buf = dtls_pkt->data - 1;
+ len = DTLS_RECV(vpninfo->dtls_ssl, buf, len + 1);
+ if (len <= 0)
+ break;
+
+ 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 0:
- queue_new_packet(&vpninfo->incoming_queue, AF_INET, buf+1, len-1);
+ case AC_PKT_DATA:
+ dtls_pkt->len = len - 1;
+ queue_packet(&vpninfo->incoming_queue, dtls_pkt);
+ dtls_pkt = NULL;
work_done = 1;
break;
- case 4: /* DPD response */
- if (verbose)
- printf("Got DTLS DPD response\n");
+ case AC_PKT_DPD_OUT:
+ 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 (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:
+ vpn_progress(vpninfo, PRG_TRACE, _("Got DTLS DPD response\n"));
+ break;
+
+ case AC_PKT_KEEPALIVE:
+ vpn_progress(vpninfo, PRG_TRACE, _("Got DTLS Keepalive\n"));
break;
default:
- fprintf(stderr, "Unknown DTLS packet type %02x\n", buf[0]);
- vpninfo->quit_reason = "Unknown packet received";
- return 1;
+ 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
+ * 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 {
+ vpninfo->quit_reason = "Unknown packet received";
+ return 1;
+ }
+
}
}
- while (vpninfo->outgoing_queue) {
- struct pkt *this = vpninfo->outgoing_queue;
- int ret;
- vpninfo->outgoing_queue = this->next;
+ switch (keepalive_action(&vpninfo->dtls_times, timeout)) {
+ case KA_REKEY:
+ vpn_progress(vpninfo, PRG_INFO, _("DTLS rekey due\n"));
- buf[0] = 0;
- memcpy(buf + 1, this->data, this->len);
-
- ret = SSL_write(vpninfo->dtls_ssl, buf, this->len + 1);
- vpninfo->last_dtls_tx = time(NULL);
- if (verbose) {
- printf("Sent DTLS packet of %d bytes; SSL_write() returned %d\n",
- this->len, ret);
+ /* 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;
}
- }
- /* DPD is bidirectional -- PKT 3 out, PKT 4 back */
- if (vpninfo->dtls_dpd) {
- time_t now = time(NULL);
- time_t due = vpninfo->last_dtls_rx + vpninfo->dtls_dpd;
- time_t overdue = vpninfo->last_dtls_rx + (5 * vpninfo->dtls_dpd);
-
- /* If we already have DPD outstanding, don't flood */
- if (vpninfo->last_dtls_dpd > vpninfo->last_dtls_rx)
- due = vpninfo->last_dtls_dpd + vpninfo->dtls_dpd;
-
- if (now > overdue) {
- fprintf(stderr, "DTLS Dead Peer Detection detected dead peer!\n");
- /* FIXME: Can we call fack to SSL instead? */
- SSL_free(vpninfo->dtls_ssl);
- close(vpninfo->dtls_fd);
- vpninfo->dtls_ssl = NULL;
- vpninfo->dtls_fd = -1;
+ if (dtls_restart(vpninfo)) {
+ vpn_progress(vpninfo, PRG_ERR, _("DTLS rekey failed\n"));
return 1;
}
- if (now >= due) {
- static unsigned char dtls_dpd_pkt[1] = { 3 };
- /* Haven't heard anything from the other end for a while.
- Check if it's still there */
- /* FIXME: If isn't, we should act on that */
- SSL_write(vpninfo->dtls_ssl, dtls_dpd_pkt, 1);
- vpninfo->last_dtls_tx = now;
-
- due = now + vpninfo->dtls_dpd;
- if (verbose)
- printf("Sent DTLS DPD\n");
- }
+ work_done = 1;
+ break;
- if (verbose)
- printf("Next DTLS DPD due in %ld seconds\n", (due - now));
- if (*timeout > (due - now) * 1000)
- *timeout = (due - now) * 1000;
- }
- /* Keepalive is just client -> server */
- if (vpninfo->dtls_keepalive) {
- time_t now = time(NULL);
- time_t due = vpninfo->last_dtls_tx + vpninfo->dtls_keepalive;
+ case KA_DPD_DEAD:
+ 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;
- if (now >= due) {
- static unsigned char dtls_keepalive_pkt[1] = { 7 };
+ case KA_DPD:
+ vpn_progress(vpninfo, PRG_TRACE, _("Send DTLS DPD\n"));
- /* Send something (which is discarded), to keep
- the connection alive. */
- SSL_write(vpninfo->dtls_ssl, dtls_keepalive_pkt, 1);
- vpninfo->last_dtls_tx = now;
+ magic_pkt = AC_PKT_DPD_OUT;
+ if (DTLS_SEND(vpninfo->dtls_ssl, &magic_pkt, 1) != 1)
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Failed to send DPD request. Expect disconnect\n"));
- due = now + vpninfo->dtls_keepalive;
- if (verbose)
- printf("Sent DTLS Keepalive\n");
- }
+ /* last_dpd will just have been set */
+ vpninfo->dtls_times.last_tx = vpninfo->dtls_times.last_dpd;
+ work_done = 1;
+ break;
- if (verbose)
- printf("Next DTLS Keepalive due in %ld seconds\n", (due - now));
- if (*timeout > (due - now) * 1000)
- *timeout = (due - now) * 1000;
+ case KA_KEEPALIVE:
+ /* No need to send an explicit keepalive
+ if we have real data to send */
+ if (vpninfo->outgoing_queue)
+ break;
+
+ vpn_progress(vpninfo, PRG_TRACE, _("Send DTLS Keepalive\n"));
+
+ magic_pkt = AC_PKT_KEEPALIVE;
+ 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;
+
+ case KA_NONE:
+ ;
}
+ /* Service outgoing packet queue */
+ while (vpninfo->outgoing_queue) {
+ struct pkt *this = vpninfo->outgoing_queue;
+ int ret;
+
+ vpninfo->outgoing_queue = this->next;
+ vpninfo->outgoing_qlen--;
+
+ /* 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);
+
+ /* 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) {
+ 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);
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("Sent DTLS packet of %d bytes; DTLS send returned %d\n"),
+ this->len, ret);
+ free(this);
+ }
- /* FIXME: Keepalive */
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