From ec9b7bf37bce3466391545ce42af5c95d643921b Mon Sep 17 00:00:00 2001 From: Nikos Mavrogiannopoulos Date: Thu, 8 Sep 2016 16:46:30 +0200 Subject: [PATCH] Extended MTU discovery to work even when compiled with openssl Signed-off-by: Nikos Mavrogiannopoulos Signed-off-by: David Woodhouse --- dtls.c | 180 +++++++++++++++++++---------------------- gnutls.c | 83 ++++++++++++++----- openconnect-internal.h | 3 + openssl.c | 60 ++++++++++---- 4 files changed, 198 insertions(+), 128 deletions(-) diff --git a/dtls.c b/dtls.c index c6ea52da..9f376d04 100644 --- a/dtls.c +++ b/dtls.c @@ -34,6 +34,8 @@ #ifdef HAVE_DTLS +static void detect_mtu(struct openconnect_info *vpninfo); + #if 0 /* * Useful for catching test cases, where we want everything to be @@ -108,6 +110,18 @@ int RAND_bytes(char *buf, int len) extern void dtls1_stop_timer(SSL *); #endif +/* sets the DTLS MTU and returns the actual tunnel MTU */ +static unsigned dtls_set_mtu(struct openconnect_info *vpninfo, unsigned mtu) +{ +#ifdef DTLS_set_link_mtu + DTLS_set_link_mtu(vpninfo->dtls_ssl, mtu); +#else + /* not sure if this is equivalent */ + SSL_set_mtu(vpninfo->dtls_ssl, LINK_TO_TUNNEL_MTU(mtu)); +#endif + return LINK_TO_TUNNEL_MTU(mtu); +} + #if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) /* Since OpenSSL 1.1, the SSL_SESSION structure is opaque and we can't * just fill it in directly. So we have to generate the OpenSSL ASN.1 @@ -429,6 +443,7 @@ static int dtls_try_handshake(struct openconnect_info *vpninfo) * trying to disable. So do nothing... */ #endif + detect_mtu(vpninfo); return 0; } @@ -485,7 +500,12 @@ void append_dtls_ciphers(struct openconnect_info *vpninfo, struct oc_text_buf *b # define GNUTLS_CIPHER_CHACHA20_POLY1305 23 #endif -static void detect_mtu(struct openconnect_info *vpninfo); +/* sets the DTLS MTU and returns the actual tunnel MTU */ +static unsigned dtls_set_mtu(struct openconnect_info *vpninfo, unsigned mtu) +{ + gnutls_dtls_set_mtu(vpninfo->dtls_ssl, mtu); + return gnutls_dtls_get_data_mtu(vpninfo->dtls_ssl); +} struct { const char *name; @@ -1114,34 +1134,6 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout) return work_done; } -#if defined(DTLS_GNUTLS) - -static int is_cancelled(struct openconnect_info *vpninfo) -{ - fd_set rd_set; - int maxfd = 0; - FD_ZERO(&rd_set); - cmd_fd_set(vpninfo, &rd_set, &maxfd); - - if (is_cancel_pending(vpninfo, &rd_set)) { - vpn_progress(vpninfo, PRG_ERR, _("SSL operation cancelled\n")); - return -EINTR; - } - - return 0; -} - -static -void ms_sleep(unsigned ms) -{ - struct timespec tv; - - tv.tv_sec = ms/1000; - tv.tv_nsec = (ms%1000) * 1000 * 1000; - - nanosleep(&tv, NULL); -} - #define MTU_ID_SIZE 4 #define MTU_FAIL_CONT { \ cur--; \ @@ -1149,6 +1141,8 @@ void ms_sleep(unsigned ms) continue; \ } +#define MTU_TIMEOUT_MS 2400 + /* Performs a binary search to detect MTU. * @buf: is preallocated with MTU size * @id: a unique ID for our DPD exchange @@ -1157,73 +1151,79 @@ void ms_sleep(unsigned ms) */ static int detect_mtu_ipv4(struct openconnect_info *vpninfo, unsigned char *buf) { - int max, min, cur, ret; - int max_tries = 100; /* maximum number of loops in bin search */ - int max_recv_loops = 40; /* 4 secs max */ + int max, min, cur, ret, orig_min; + int max_tries = 10; /* maximum number of loops in bin search - includes resends */ char id[MTU_ID_SIZE]; + unsigned re_use_rnd_val = 0; cur = max = vpninfo->ip_info.mtu; - min = vpninfo->ip_info.mtu/2; + orig_min = min = vpninfo->ip_info.mtu/2; vpn_progress(vpninfo, PRG_DEBUG, _("Initiating IPv4 MTU detection (min=%d, max=%d)\n"), min, max); while (max > min) { if (max_tries-- <= 0) { - vpn_progress(vpninfo, PRG_ERR, - _("Too long time in MTU detect loop; bailing out.\n")); - goto fail; + if (orig_min == cur) { + 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"), cur); + return cur; + } } /* generate unique ID */ - if (gnutls_rnd(GNUTLS_RND_NONCE, id, sizeof(id)) < 0) - goto fail; + if (!re_use_rnd_val) { + if (openconnect_random(id, sizeof(id)) < 0) + goto fail; + } else { + re_use_rnd_val = 0; + } cur = (min + max + 1) / 2; buf[0] = AC_PKT_DPD_OUT; memcpy(&buf[1], id, sizeof(id)); - do { - if (is_cancelled(vpninfo)) - goto fail; - - ret = DTLS_SEND(vpninfo->dtls_ssl, buf, cur+1); - if (ret < 0) - ms_sleep(100); - } while(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED); - + vpn_progress(vpninfo, PRG_TRACE, + _("Sending MTU DPD probe (%u bytes)\n"), cur); + ret = openconnect_dtls_write(vpninfo, buf, cur+1); if (ret != cur+1) { vpn_progress(vpninfo, PRG_ERR, - _("Failed to send DPD request (%d): %s\n"), cur, gnutls_strerror(ret)); + _("Failed to send DPD request (%d)\n"), cur); MTU_FAIL_CONT; } reread: memset(buf, 0, sizeof(id)+1); - do { - if (is_cancelled(vpninfo)) - goto fail; - - ret = DTLS_RECV(vpninfo->dtls_ssl, buf, cur + 1); - if (ret < 0) - ms_sleep(100); - - if (max_recv_loops-- <= 0) - goto fail; - } while(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED); + ret = openconnect_dtls_read(vpninfo, buf, cur+1, MTU_TIMEOUT_MS); if (ret > 0 && (buf[0] != AC_PKT_DPD_RESP || memcmp(&buf[1], id, sizeof(id)) != 0)) { vpn_progress(vpninfo, PRG_DEBUG, _("Received unexpected packet (%.2x) in MTU detection; skipping.\n"), (unsigned)buf[0]); - goto reread; /* resend */ + goto reread; + } + + /* timeout, probably our original request was lost, + * let's resend the DPD */ + if (ret == -ETIMEDOUT) { + vpn_progress(vpninfo, PRG_DEBUG, + _("Timeout while waiting for DPD response; resending probe.\n")); + re_use_rnd_val = 1; + continue; } if (ret <= 0) { vpn_progress(vpninfo, PRG_ERR, - _("Failed to recv DPD request (%d): %s\n"), cur, gnutls_strerror(ret)); + _("Failed to recv DPD request (%d)\n"), cur); MTU_FAIL_CONT; } + vpn_progress(vpninfo, PRG_TRACE, + _("Received MTU DPD probe (%u bytes)\n"), cur); + /* If we reached the max, success */ if (cur == max) { break; @@ -1254,54 +1254,47 @@ static int detect_mtu_ipv6(struct openconnect_info *vpninfo, unsigned char *buf) { int max, cur, ret; int max_resends = 5; /* maximum number of resends */ - int max_recv_loops = 40; /* 4 secs max */ char id[MTU_ID_SIZE]; + unsigned re_use_rnd_val = 0; cur = max = vpninfo->ip_info.mtu; vpn_progress(vpninfo, PRG_DEBUG, _("Initiating IPv6 MTU detection\n")); - do { + while(max_resends-- > 0) { /* generate unique ID */ - if (gnutls_rnd(GNUTLS_RND_NONCE, id, sizeof(id)) < 0) - goto fail; + if (!re_use_rnd_val) { + if (openconnect_random(id, sizeof(id)) < 0) + goto fail; + } else { + re_use_rnd_val = 0; + } buf[0] = AC_PKT_DPD_OUT; memcpy(&buf[1], id, sizeof(id)); - do { - if (is_cancelled(vpninfo)) - goto fail; - - ret = DTLS_SEND(vpninfo->dtls_ssl, buf, cur+1); - if (ret < 0) - ms_sleep(100); - } while(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED); + vpn_progress(vpninfo, PRG_TRACE, + _("Sending MTU DPD probe (%u bytes)\n"), cur); + ret = openconnect_dtls_write(vpninfo, buf, cur+1); if (ret != cur+1) { vpn_progress(vpninfo, PRG_ERR, - _("Failed to send DPD request (%d): %s\n"), cur, gnutls_strerror(ret)); + _("Failed to send DPD request (%d)\n"), cur); goto mtu6_fail; } reread: memset(buf, 0, sizeof(id)+1); - do { - if (is_cancelled(vpninfo)) - goto fail; - - ret = DTLS_RECV(vpninfo->dtls_ssl, buf, cur + 1); - if (ret < 0) - ms_sleep(100); - - if (max_recv_loops-- <= 0) - goto fail; - } while(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED); + ret = openconnect_dtls_read(vpninfo, buf, cur+1, MTU_TIMEOUT_MS); /* timeout, probably our original request was lost, * let's resend the DPD */ - if (ret == GNUTLS_E_TIMEDOUT) + if (ret == -ETIMEDOUT) { + vpn_progress(vpninfo, PRG_DEBUG, + _("Timeout while waiting for DPD response; resending probe.\n")); + re_use_rnd_val = 1; continue; + } /* something unexpected was received, let's ignore it */ if (ret > 0 && (buf[0] != AC_PKT_DPD_RESP || memcmp(&buf[1], id, sizeof(id)) != 0)) { @@ -1310,9 +1303,12 @@ static int detect_mtu_ipv6(struct openconnect_info *vpninfo, unsigned char *buf) goto reread; } + vpn_progress(vpninfo, PRG_TRACE, + _("Received MTU DPD probe (%u bytes)\n"), cur); + /* we received what we expected, move on */ break; - } while(max_resends-- > 0); + } #ifdef HAVE_IPV6_PATHMTU /* If we received back our DPD packet, do nothing; otherwise, @@ -1322,13 +1318,12 @@ static int detect_mtu_ipv6(struct openconnect_info *vpninfo, unsigned char *buf) socklen_t len = sizeof(mtuinfo); max = 0; vpn_progress(vpninfo, PRG_ERR, - _("Failed to recv DPD request (%d): %s\n"), cur, gnutls_strerror(ret)); + _("Failed to recv DPD request (%d)\n"), cur); mtu6_fail: if (getsockopt(vpninfo->dtls_fd, IPPROTO_IPV6, IPV6_PATHMTU, &mtuinfo, &len) >= 0) { max = mtuinfo.ip6m_mtu; if (max >= 0 && max < cur) { - gnutls_dtls_set_mtu(vpninfo->dtls_ssl, max); - cur = gnutls_dtls_get_data_mtu(vpninfo->dtls_ssl) - /*ipv6*/40 - /*udp*/20 - /*oc dtls*/1; + cur = dtls_set_mtu(vpninfo, max) - /*ipv6*/40 - /*udp*/20 - /*oc dtls*/1; } } } @@ -1358,8 +1353,6 @@ static void detect_mtu(struct openconnect_info *vpninfo) return; } - /* enforce a timeout in receiving */ - gnutls_record_set_timeout(vpninfo->dtls_ssl, 2400); if (vpninfo->peer_addr->sa_family == AF_INET) { /* IPv4 */ mtu = detect_mtu_ipv4(vpninfo, buf); if (mtu == 0) @@ -1382,12 +1375,9 @@ static void detect_mtu(struct openconnect_info *vpninfo) } skip_mtu: - gnutls_record_set_timeout(vpninfo->dtls_ssl, 0); free(buf); } -#endif - #else /* !HAVE_DTLS */ #warning Your SSL library does not seem to support Cisco DTLS compatibility #endif diff --git a/gnutls.c b/gnutls.c index 2f4c8938..01379307 100644 --- a/gnutls.c +++ b/gnutls.c @@ -99,26 +99,26 @@ static P11KitPin *p11kit_pin_callback(const char *pin_source, P11KitUri *pin_uri gnutls_check_version(#a "." #b "." #c))) /* Helper functions for reading/writing lines over SSL. */ -static int openconnect_gnutls_write(struct openconnect_info *vpninfo, char *buf, size_t len) +static int _openconnect_gnutls_write(gnutls_session_t ses, int fd, struct openconnect_info *vpninfo, char *buf, size_t len) { size_t orig_len = len; while (len) { - int done = gnutls_record_send(vpninfo->https_sess, buf, len); + int done = gnutls_record_send(ses, buf, len); if (done > 0) len -= done; else if (done == GNUTLS_E_AGAIN || done == GNUTLS_E_INTERRUPTED) { /* Wait for something to happen on the socket, or on cmd_fd */ fd_set wr_set, rd_set; - int maxfd = vpninfo->ssl_fd; + int maxfd = fd; FD_ZERO(&wr_set); FD_ZERO(&rd_set); - if (gnutls_record_get_direction(vpninfo->https_sess)) - FD_SET(vpninfo->ssl_fd, &wr_set); + if (gnutls_record_get_direction(ses)) + FD_SET(fd, &wr_set); else - FD_SET(vpninfo->ssl_fd, &rd_set); + FD_SET(fd, &rd_set); cmd_fd_set(vpninfo, &rd_set, &maxfd); select(maxfd + 1, &rd_set, &wr_set, NULL, NULL); @@ -135,48 +135,93 @@ static int openconnect_gnutls_write(struct openconnect_info *vpninfo, char *buf, return orig_len; } -static int openconnect_gnutls_read(struct openconnect_info *vpninfo, char *buf, size_t len) +static int openconnect_gnutls_write(struct openconnect_info *vpninfo, char *buf, size_t len) { - int done; + return _openconnect_gnutls_write(vpninfo->https_sess, vpninfo->ssl_fd, vpninfo, buf, len); +} - while ((done = gnutls_record_recv(vpninfo->https_sess, buf, len)) < 0) { +int openconnect_dtls_write(struct openconnect_info *vpninfo, void *buf, size_t len) +{ + return _openconnect_gnutls_write(vpninfo->dtls_ssl, vpninfo->dtls_fd, vpninfo, buf, len); +} + +static int _openconnect_gnutls_read(gnutls_session_t ses, int fd, struct openconnect_info *vpninfo, char *buf, size_t len, unsigned ms) +{ + int done, ret; + struct timeval timeout, *tv = NULL; + + if (ms) { + timeout.tv_sec = ms/1000; + timeout.tv_usec = (ms%1000)*1000; + tv = &timeout; + } + + while ((done = gnutls_record_recv(ses, buf, len)) < 0) { if (done == GNUTLS_E_AGAIN || done == GNUTLS_E_INTERRUPTED) { /* Wait for something to happen on the socket, or on cmd_fd */ fd_set wr_set, rd_set; - int maxfd = vpninfo->ssl_fd; + int maxfd = fd; FD_ZERO(&wr_set); FD_ZERO(&rd_set); - if (gnutls_record_get_direction(vpninfo->https_sess)) - FD_SET(vpninfo->ssl_fd, &wr_set); + if (gnutls_record_get_direction(ses)) + FD_SET(fd, &wr_set); else - FD_SET(vpninfo->ssl_fd, &rd_set); + FD_SET(fd, &rd_set); cmd_fd_set(vpninfo, &rd_set, &maxfd); - select(maxfd + 1, &rd_set, &wr_set, NULL, NULL); + ret = select(maxfd + 1, &rd_set, &wr_set, NULL, tv); if (is_cancel_pending(vpninfo, &rd_set)) { vpn_progress(vpninfo, PRG_ERR, _("SSL read cancelled\n")); - return -EINTR; + done = -EINTR; + goto cleanup; + } + + if (ret == 0) { + done = -ETIMEDOUT; + goto cleanup; } } else if (done == GNUTLS_E_PREMATURE_TERMINATION) { /* We've seen this with HTTP 1.0 responses followed by abrupt socket closure and no clean SSL shutdown. https://bugs.launchpad.net/bugs/1225276 */ vpn_progress(vpninfo, PRG_DEBUG, _("SSL socket closed uncleanly\n")); - return 0; + done = 0; + goto cleanup; } else if (done == GNUTLS_E_REHANDSHAKE) { int ret = cstp_handshake(vpninfo, 0); - if (ret) - return ret; + if (ret) { + done = ret; + goto cleanup; + } } else { vpn_progress(vpninfo, PRG_ERR, _("Failed to read from SSL socket: %s\n"), gnutls_strerror(done)); - return -EIO; + if (done == GNUTLS_E_TIMEDOUT) { + done = -ETIMEDOUT; + goto cleanup; + } else { + done = -EIO; + goto cleanup; + } } } + + cleanup: return done; + +} + +static int openconnect_gnutls_read(struct openconnect_info *vpninfo, char *buf, size_t len) +{ + return _openconnect_gnutls_read(vpninfo->https_sess, vpninfo->ssl_fd, vpninfo, buf, len, 0); +} + +int openconnect_dtls_read(struct openconnect_info *vpninfo, void *buf, size_t len, unsigned ms) +{ + return _openconnect_gnutls_read(vpninfo->dtls_ssl, vpninfo->dtls_fd, vpninfo, buf, len, ms); } static int openconnect_gnutls_gets(struct openconnect_info *vpninfo, char *buf, size_t len) diff --git a/openconnect-internal.h b/openconnect-internal.h index 16a6b735..a87d9def 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -807,6 +807,9 @@ void dtls_close(struct openconnect_info *vpninfo); void dtls_shutdown(struct openconnect_info *vpninfo); void append_dtls_ciphers(struct openconnect_info *vpninfo, struct oc_text_buf *buf); +int openconnect_dtls_read(struct openconnect_info *vpninfo, void *buf, size_t len, unsigned ms); +int openconnect_dtls_write(struct openconnect_info *vpninfo, void *buf, size_t len); + /* cstp.c */ void cstp_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf); int cstp_connect(struct openconnect_info *vpninfo); diff --git a/openssl.c b/openssl.c index 7b574d75..efb77e45 100644 --- a/openssl.c +++ b/openssl.c @@ -109,27 +109,27 @@ int openconnect_random(void *bytes, int len) /* Helper functions for reading/writing lines over SSL. We could use cURL for the HTTP stuff, but it's overkill */ -static int openconnect_openssl_write(struct openconnect_info *vpninfo, char *buf, size_t len) +static int _openconnect_openssl_write(SSL *ssl, int fd, struct openconnect_info *vpninfo, char *buf, size_t len) { size_t orig_len = len; while (len) { - int done = SSL_write(vpninfo->https_ssl, buf, len); + int done = SSL_write(ssl, buf, len); if (done > 0) len -= done; else { - int err = SSL_get_error(vpninfo->https_ssl, done); + int err = SSL_get_error(ssl, done); fd_set wr_set, rd_set; - int maxfd = vpninfo->ssl_fd; + int maxfd = fd; FD_ZERO(&wr_set); FD_ZERO(&rd_set); if (err == SSL_ERROR_WANT_READ) - FD_SET(vpninfo->ssl_fd, &rd_set); + FD_SET(fd, &rd_set); else if (err == SSL_ERROR_WANT_WRITE) - FD_SET(vpninfo->ssl_fd, &wr_set); + FD_SET(fd, &wr_set); else { vpn_progress(vpninfo, PRG_ERR, _("Failed to write to SSL socket\n")); openconnect_report_ssl_errors(vpninfo); @@ -146,37 +146,69 @@ static int openconnect_openssl_write(struct openconnect_info *vpninfo, char *buf return orig_len; } -static int openconnect_openssl_read(struct openconnect_info *vpninfo, char *buf, size_t len) +static int openconnect_openssl_write(struct openconnect_info *vpninfo, char *buf, size_t len) +{ + return _openconnect_openssl_write(vpninfo->https_ssl, vpninfo->ssl_fd, vpninfo, buf, len); +} + +int openconnect_dtls_write(struct openconnect_info *vpninfo, void *buf, size_t len) +{ + return _openconnect_openssl_write(vpninfo->dtls_ssl, vpninfo->dtls_fd, vpninfo, buf, len); +} + +/* set ms to zero for no timeout */ +static int _openconnect_openssl_read(SSL *ssl, int fd, struct openconnect_info *vpninfo, char *buf, size_t len, unsigned ms) { - int done; + int done, ret; + struct timeval timeout, *tv = NULL; - while ((done = SSL_read(vpninfo->https_ssl, buf, len)) == -1) { - int err = SSL_get_error(vpninfo->https_ssl, done); + if (ms) { + timeout.tv_sec = ms/1000; + timeout.tv_usec = (ms%1000)*1000; + tv = &timeout; + } + + while ((done = SSL_read(ssl, buf, len)) == -1) { + int err = SSL_get_error(ssl, done); fd_set wr_set, rd_set; - int maxfd = vpninfo->ssl_fd; + int maxfd = fd; FD_ZERO(&wr_set); FD_ZERO(&rd_set); if (err == SSL_ERROR_WANT_READ) - FD_SET(vpninfo->ssl_fd, &rd_set); + FD_SET(fd, &rd_set); else if (err == SSL_ERROR_WANT_WRITE) - FD_SET(vpninfo->ssl_fd, &wr_set); + FD_SET(fd, &wr_set); else { vpn_progress(vpninfo, PRG_ERR, _("Failed to read from SSL socket\n")); openconnect_report_ssl_errors(vpninfo); return -EIO; } cmd_fd_set(vpninfo, &rd_set, &maxfd); - select(maxfd + 1, &rd_set, &wr_set, NULL, NULL); + ret = select(maxfd + 1, &rd_set, &wr_set, NULL, tv); if (is_cancel_pending(vpninfo, &rd_set)) { vpn_progress(vpninfo, PRG_ERR, _("SSL read cancelled\n")); return -EINTR; } + + if (ret == 0) { + return -ETIMEDOUT; + } } return done; } +static int openconnect_openssl_read(struct openconnect_info *vpninfo, char *buf, size_t len) +{ + return _openconnect_openssl_read(vpninfo->https_ssl, vpninfo->ssl_fd, vpninfo, buf, len, 0); +} + +int openconnect_dtls_read(struct openconnect_info *vpninfo, void *buf, size_t len, unsigned ms) +{ + return _openconnect_openssl_read(vpninfo->dtls_ssl, vpninfo->dtls_fd, vpninfo, buf, len, ms); +} + static int openconnect_openssl_gets(struct openconnect_info *vpninfo, char *buf, size_t len) { int i = 0;