Skip to content

Commit

Permalink
Added MTU detection after DTLS channel establishment
Browse files Browse the repository at this point in the history
This uses binary search for IPv4 MTU detection and the usage of
the MTU field in the ICMP6 packet too big message.

Signed-off-by: Nikos Mavrogiannopoulos <nmav@redhat.com>
  • Loading branch information
Nikos Mavrogiannopoulos authored and nmav committed Dec 5, 2015
1 parent 8b137a2 commit c00609a
Showing 1 changed file with 268 additions and 0 deletions.
268 changes: 268 additions & 0 deletions dtls.c
Expand Up @@ -466,6 +466,8 @@ 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);

struct {
const char *name;
gnutls_protocol_t version;
Expand Down Expand Up @@ -652,6 +654,7 @@ static int dtls_try_handshake(struct openconnect_info *vpninfo)
vpninfo->dtls_times.last_rekey = vpninfo->dtls_times.last_rx =
vpninfo->dtls_times.last_tx = time(NULL);

detect_mtu(vpninfo);
/* XXX: For OpenSSL we explicitly prevent retransmits here. */
return 0;
}
Expand Down Expand Up @@ -1057,6 +1060,271 @@ int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout)

return work_done;
}

/* Old glibc doesn't define that */
#if defined(__linux__) && !defined(IPV6_PATHMTU)
# define IPV6_PATHMTU 61
#endif

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--; \
max = cur; \
continue; \
}

/* Performs a binary search to detect MTU.
* @buf: is preallocated with MTU size
* @id: a unique ID for our DPD exchange
*
* Returns: new MTU or 0
*/
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 */
char id[MTU_ID_SIZE];

cur = max = vpninfo->ip_info.mtu;
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;
}

/* generate unique ID */
if (gnutls_rnd(GNUTLS_RND_NONCE, id, sizeof(id)) < 0)
goto fail;

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);

if (ret != cur+1) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to send DPD request (%d): %s\n"), cur, gnutls_strerror(ret));
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);

if (ret > 0 && (buf[0] != AC_PKT_DPD_RESP || memcmp(&buf[1], id, sizeof(buf) != 0))) {
vpn_progress(vpninfo, PRG_DEBUG,
_("Received unexpected packet (%.2x) in MTU detection; skipping.\n"), (unsigned)buf[0]);
goto reread; /* resend */
}

if (ret <= 0) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to recv DPD request (%d): %s\n"), cur, gnutls_strerror(ret));
MTU_FAIL_CONT;
}

/* If we reached the max, success */
if (cur == max) {
break;
}
min = cur;
}

return cur;
fail:
return 0;
}

#if defined(IPPROTO_IPV6)
/* Verifies whether current MTU is ok, or detects new MTU using IPv6's ICMP6 messages
* @buf: is preallocated with MTU size
* @id: a unique ID for our DPD exchange
*
* Returns: new MTU or 0
*/
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];

cur = max = vpninfo->ip_info.mtu;

vpn_progress(vpninfo, PRG_DEBUG,
_("Initiating IPv6 MTU detection\n"));

do {
/* generate unique ID */
if (gnutls_rnd(GNUTLS_RND_NONCE, id, sizeof(id)) < 0)
goto fail;

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);

if (ret != cur+1) {
vpn_progress(vpninfo, PRG_ERR,
_("Failed to send DPD request (%d): %s\n"), cur, gnutls_strerror(ret));
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);

/* timeout, probably our original request was lost,
* let's resend the DPD */
if (ret == GNUTLS_E_TIMEDOUT)
continue;

/* something unexpected was received, let's ignore it */
if (ret > 0 && (buf[0] != AC_PKT_DPD_RESP || memcmp(&buf[1], id, sizeof(buf) != 0))) {
vpn_progress(vpninfo, PRG_DEBUG,
_("Received unexpected packet (%.2x) in MTU detection; skipping.\n"), (unsigned)buf[0]);
goto reread;
}

/* we received what we expected, move on */
break;
} while(max_resends-- > 0);

/* If we received back our DPD packet, do nothing; otherwise,
* attempt to get MTU from the ICMP6 packet we received */
if (ret <= 0) {
struct ip6_mtuinfo mtuinfo;
socklen_t len = sizeof(mtuinfo);
max = 0;
vpn_progress(vpninfo, PRG_ERR,
_("Failed to recv DPD request (%d): %s\n"), cur, gnutls_strerror(ret));
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;
}
}
}

return cur;
fail:
return 0;
}
#endif

static void detect_mtu(struct openconnect_info *vpninfo)
{
int mtu = vpninfo->ip_info.mtu;
int prev_mtu = vpninfo->ip_info.mtu;
unsigned char *buf;

if (vpninfo->ip_info.mtu < 1+MTU_ID_SIZE)
return;

/* detect MTU */
buf = calloc(1, 1 + vpninfo->ip_info.mtu);
if (!buf) {
vpn_progress(vpninfo, PRG_ERR, _("Allocation failed\n"));
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)
goto skip_mtu;
#if defined(IPPROTO_IPV6)
} else if (vpninfo->peer_addr->sa_family == AF_INET6) { /* IPv6 */
mtu = detect_mtu_ipv6(vpninfo, buf);
if (mtu == 0)
goto skip_mtu;
#endif
}

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

skip_mtu:
gnutls_record_set_timeout(vpninfo->dtls_ssl, 0);
free(buf);
}

#else /* !HAVE_DTLS */
#warning Your SSL library does not seem to support Cisco DTLS compatibility
#endif

0 comments on commit c00609a

Please sign in to comment.