summaryrefslogtreecommitdiffstats
path: root/lib/ext/heartbeat.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/ext/heartbeat.c574
1 files changed, 574 insertions, 0 deletions
diff --git a/lib/ext/heartbeat.c b/lib/ext/heartbeat.c
new file mode 100644
index 0000000..5d9e9f4
--- /dev/null
+++ b/lib/ext/heartbeat.c
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2012,2013 Free Software Foundation, Inc.
+ * Copyright (C) 2013 Nikos Mavrogiannopoulos
+ *
+ * Author: Nikos Mavrogiannopoulos
+ *
+ * This file is part of GnuTLS.
+ *
+ * The GnuTLS 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.
+ *
+ * This library 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 program. If not, see <https://www.gnu.org/licenses/>
+ *
+ */
+
+/* This file implements the TLS heartbeat extension.
+ */
+
+#include "errors.h"
+#include "gnutls_int.h"
+#include <dtls.h>
+#include <record.h>
+#include <ext/heartbeat.h>
+#include <hello_ext.h>
+#include <random.h>
+
+#ifdef ENABLE_HEARTBEAT
+/**
+ * gnutls_heartbeat_enable:
+ * @session: is a #gnutls_session_t type.
+ * @type: one of the GNUTLS_HB_* flags
+ *
+ * If this function is called with the %GNUTLS_HB_PEER_ALLOWED_TO_SEND
+ * @type, GnuTLS will allow heartbeat messages to be received. Moreover it also
+ * request the peer to accept heartbeat messages. This function
+ * must be called prior to TLS handshake.
+ *
+ * If the @type used is %GNUTLS_HB_LOCAL_ALLOWED_TO_SEND, then the peer
+ * will be asked to accept heartbeat messages but not send ones.
+ *
+ * The function gnutls_heartbeat_allowed() can be used to test Whether
+ * locally generated heartbeat messages can be accepted by the peer.
+ *
+ * Since: 3.1.2
+ **/
+void gnutls_heartbeat_enable(gnutls_session_t session, unsigned int type)
+{
+ gnutls_ext_priv_data_t epriv;
+
+ epriv = (void*)(intptr_t)type;
+ _gnutls_hello_ext_set_priv(session, GNUTLS_EXTENSION_HEARTBEAT,
+ epriv);
+}
+
+/**
+ * gnutls_heartbeat_allowed:
+ * @session: is a #gnutls_session_t type.
+ * @type: one of %GNUTLS_HB_LOCAL_ALLOWED_TO_SEND and %GNUTLS_HB_PEER_ALLOWED_TO_SEND
+ *
+ * This function will check whether heartbeats are allowed
+ * to be sent or received in this session.
+ *
+ * Returns: Non zero if heartbeats are allowed.
+ *
+ * Since: 3.1.2
+ **/
+unsigned gnutls_heartbeat_allowed(gnutls_session_t session, unsigned int type)
+{
+ gnutls_ext_priv_data_t epriv;
+
+ if (session->internals.handshake_in_progress != 0)
+ return 0; /* not allowed */
+
+ if (_gnutls_hello_ext_get_priv
+ (session, GNUTLS_EXTENSION_HEARTBEAT, &epriv) < 0)
+ return 0; /* Not enabled */
+
+ if (type == GNUTLS_HB_LOCAL_ALLOWED_TO_SEND) {
+ if (((intptr_t)epriv) & LOCAL_ALLOWED_TO_SEND)
+ return 1;
+ } else if (((intptr_t)epriv) & GNUTLS_HB_PEER_ALLOWED_TO_SEND)
+ return 1;
+
+ return 0;
+}
+
+#define DEFAULT_PADDING_SIZE 16
+
+/*
+ * Sends heartbeat data.
+ */
+static int
+heartbeat_send_data(gnutls_session_t session, const void *data,
+ size_t data_size, uint8_t type)
+{
+ int ret, pos;
+ uint8_t *response;
+
+ response = gnutls_malloc(1 + 2 + data_size + DEFAULT_PADDING_SIZE);
+ if (response == NULL)
+ return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);
+
+ pos = 0;
+ response[pos++] = type;
+
+ _gnutls_write_uint16(data_size, &response[pos]);
+ pos += 2;
+
+ memcpy(&response[pos], data, data_size);
+ pos += data_size;
+
+ ret =
+ gnutls_rnd(GNUTLS_RND_NONCE, &response[pos],
+ DEFAULT_PADDING_SIZE);
+ if (ret < 0) {
+ gnutls_assert();
+ goto cleanup;
+ }
+ pos += DEFAULT_PADDING_SIZE;
+
+ ret =
+ _gnutls_send_int(session, GNUTLS_HEARTBEAT, -1,
+ EPOCH_WRITE_CURRENT, response, pos,
+ MBUFFER_FLUSH);
+
+ cleanup:
+ gnutls_free(response);
+ return ret;
+}
+
+/**
+ * gnutls_heartbeat_ping:
+ * @session: is a #gnutls_session_t type.
+ * @data_size: is the length of the ping payload.
+ * @max_tries: if flags is %GNUTLS_HEARTBEAT_WAIT then this sets the number of retransmissions. Use zero for indefinite (until timeout).
+ * @flags: if %GNUTLS_HEARTBEAT_WAIT then wait for pong or timeout instead of returning immediately.
+ *
+ * This function sends a ping to the peer. If the @flags is set
+ * to %GNUTLS_HEARTBEAT_WAIT then it waits for a reply from the peer.
+ *
+ * Note that it is highly recommended to use this function with the
+ * flag %GNUTLS_HEARTBEAT_WAIT, or you need to handle retransmissions
+ * and timeouts manually.
+ *
+ * The total TLS data transmitted as part of the ping message are given by
+ * the following formula: MAX(16, @data_size)+gnutls_record_overhead_size()+3.
+ *
+ * Returns: %GNUTLS_E_SUCCESS on success, otherwise a negative error code.
+ *
+ * Since: 3.1.2
+ **/
+int
+gnutls_heartbeat_ping(gnutls_session_t session, size_t data_size,
+ unsigned int max_tries, unsigned int flags)
+{
+ int ret;
+ unsigned int retries = 1, diff;
+ struct timespec now;
+
+ if (data_size > MAX_HEARTBEAT_LENGTH)
+ return
+ gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
+
+ if (gnutls_heartbeat_allowed
+ (session, GNUTLS_HB_LOCAL_ALLOWED_TO_SEND) == 0)
+ return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);
+
+ /* resume previous call if interrupted */
+ if (session->internals.record_send_buffer.byte_length > 0 &&
+ session->internals.record_send_buffer.head != NULL &&
+ session->internals.record_send_buffer.head->type ==
+ GNUTLS_HEARTBEAT)
+ return _gnutls_io_write_flush(session);
+
+ switch (session->internals.hb_state) {
+ case SHB_SEND1:
+ if (data_size > DEFAULT_PADDING_SIZE)
+ data_size -= DEFAULT_PADDING_SIZE;
+ else
+ data_size = 0;
+
+ _gnutls_buffer_reset(&session->internals.hb_local_data);
+
+ ret =
+ _gnutls_buffer_resize(&session->internals.
+ hb_local_data, data_size);
+ if (ret < 0)
+ return gnutls_assert_val(ret);
+
+ ret =
+ gnutls_rnd(GNUTLS_RND_NONCE,
+ session->internals.hb_local_data.data,
+ data_size);
+ if (ret < 0)
+ return gnutls_assert_val(ret);
+
+ gnutls_gettime(&session->internals.hb_ping_start);
+ session->internals.hb_local_data.length = data_size;
+ session->internals.hb_state = SHB_SEND2;
+
+ FALLTHROUGH;
+ case SHB_SEND2:
+ session->internals.hb_actual_retrans_timeout_ms =
+ session->internals.hb_retrans_timeout_ms;
+ retry:
+ ret =
+ heartbeat_send_data(session,
+ session->internals.hb_local_data.
+ data,
+ session->internals.hb_local_data.
+ length, HEARTBEAT_REQUEST);
+ if (ret < 0)
+ return gnutls_assert_val(ret);
+
+ gnutls_gettime(&session->internals.hb_ping_sent);
+
+ if (!(flags & GNUTLS_HEARTBEAT_WAIT)) {
+ session->internals.hb_state = SHB_SEND1;
+ break;
+ }
+
+ session->internals.hb_state = SHB_RECV;
+ FALLTHROUGH;
+
+ case SHB_RECV:
+ ret =
+ _gnutls_recv_int(session, GNUTLS_HEARTBEAT,
+ NULL, 0, NULL,
+ session->internals.
+ hb_actual_retrans_timeout_ms);
+ if (ret == GNUTLS_E_HEARTBEAT_PONG_RECEIVED) {
+ session->internals.hb_state = SHB_SEND1;
+ break;
+ } else if (ret == GNUTLS_E_TIMEDOUT) {
+ retries++;
+ if (max_tries > 0 && retries > max_tries) {
+ session->internals.hb_state = SHB_SEND1;
+ return gnutls_assert_val(ret);
+ }
+
+ gnutls_gettime(&now);
+ diff =
+ timespec_sub_ms(&now,
+ &session->internals.
+ hb_ping_start);
+ if (diff > session->internals.hb_total_timeout_ms) {
+ session->internals.hb_state = SHB_SEND1;
+ return
+ gnutls_assert_val(GNUTLS_E_TIMEDOUT);
+ }
+
+ session->internals.hb_actual_retrans_timeout_ms *=
+ 2;
+ session->internals.hb_actual_retrans_timeout_ms %=
+ MAX_DTLS_TIMEOUT;
+
+ session->internals.hb_state = SHB_SEND2;
+ goto retry;
+ } else if (ret < 0) {
+ session->internals.hb_state = SHB_SEND1;
+ return gnutls_assert_val(ret);
+ }
+ }
+
+ return 0;
+}
+
+/**
+ * gnutls_heartbeat_pong:
+ * @session: is a #gnutls_session_t type.
+ * @flags: should be zero
+ *
+ * This function replies to a ping by sending a pong to the peer.
+ *
+ * Returns: %GNUTLS_E_SUCCESS on success, otherwise a negative error code.
+ *
+ * Since: 3.1.2
+ **/
+int gnutls_heartbeat_pong(gnutls_session_t session, unsigned int flags)
+{
+ int ret;
+
+ if (session->internals.record_send_buffer.byte_length > 0 &&
+ session->internals.record_send_buffer.head != NULL &&
+ session->internals.record_send_buffer.head->type ==
+ GNUTLS_HEARTBEAT)
+ return _gnutls_io_write_flush(session);
+
+ if (session->internals.hb_remote_data.length == 0)
+ return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);
+
+ ret =
+ heartbeat_send_data(session,
+ session->internals.hb_remote_data.data,
+ session->internals.hb_remote_data.length,
+ HEARTBEAT_RESPONSE);
+
+ _gnutls_buffer_reset(&session->internals.hb_remote_data);
+
+ if (ret < 0)
+ return gnutls_assert_val(ret);
+
+ return 0;
+}
+
+/*
+ * Processes a heartbeat message.
+ */
+int _gnutls_heartbeat_handle(gnutls_session_t session, mbuffer_st * bufel)
+{
+ int ret;
+ unsigned type;
+ unsigned pos;
+ uint8_t *msg = _mbuffer_get_udata_ptr(bufel);
+ size_t hb_len, len = _mbuffer_get_udata_size(bufel);
+
+ if (gnutls_heartbeat_allowed
+ (session, GNUTLS_HB_PEER_ALLOWED_TO_SEND) == 0)
+ return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);
+
+ if (len < 3 + DEFAULT_PADDING_SIZE)
+ return
+ gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
+
+ pos = 0;
+ type = msg[pos++];
+
+ hb_len = _gnutls_read_uint16(&msg[pos]);
+ if (hb_len > len - 3 - DEFAULT_PADDING_SIZE)
+ return
+ gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
+
+ pos += 2;
+
+ switch (type) {
+ case HEARTBEAT_REQUEST:
+ _gnutls_buffer_reset(&session->internals.hb_remote_data);
+
+ ret =
+ _gnutls_buffer_resize(&session->internals.
+ hb_remote_data, hb_len);
+ if (ret < 0)
+ return gnutls_assert_val(ret);
+
+ if (hb_len > 0)
+ memcpy(session->internals.hb_remote_data.data,
+ &msg[pos], hb_len);
+ session->internals.hb_remote_data.length = hb_len;
+
+ return gnutls_assert_val(GNUTLS_E_HEARTBEAT_PING_RECEIVED);
+
+ case HEARTBEAT_RESPONSE:
+
+ if (hb_len != session->internals.hb_local_data.length)
+ return
+ gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);
+
+ if (hb_len > 0 &&
+ memcmp(&msg[pos],
+ session->internals.hb_local_data.data,
+ hb_len) != 0) {
+ if (IS_DTLS(session))
+ return gnutls_assert_val(GNUTLS_E_AGAIN); /* ignore it */
+ else
+ return
+ gnutls_assert_val
+ (GNUTLS_E_UNEXPECTED_PACKET);
+ }
+
+ _gnutls_buffer_reset(&session->internals.hb_local_data);
+
+ return gnutls_assert_val(GNUTLS_E_HEARTBEAT_PONG_RECEIVED);
+ default:
+ _gnutls_record_log
+ ("REC[%p]: HB: received unknown type %u\n", session,
+ type);
+ return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);
+ }
+}
+
+/**
+ * gnutls_heartbeat_get_timeout:
+ * @session: is a #gnutls_session_t type.
+ *
+ * This function will return the milliseconds remaining
+ * for a retransmission of the previously sent ping
+ * message. This function is useful when ping is used in
+ * non-blocking mode, to estimate when to call gnutls_heartbeat_ping()
+ * if no packets have been received.
+ *
+ * Returns: the remaining time in milliseconds.
+ *
+ * Since: 3.1.2
+ **/
+unsigned int gnutls_heartbeat_get_timeout(gnutls_session_t session)
+{
+ struct timespec now;
+ unsigned int diff;
+
+ gnutls_gettime(&now);
+ diff = timespec_sub_ms(&now, &session->internals.hb_ping_sent);
+ if (diff >= session->internals.hb_actual_retrans_timeout_ms)
+ return 0;
+ else
+ return session->internals.hb_actual_retrans_timeout_ms -
+ diff;
+}
+
+/**
+ * gnutls_heartbeat_set_timeouts:
+ * @session: is a #gnutls_session_t type.
+ * @retrans_timeout: The time at which a retransmission will occur in milliseconds
+ * @total_timeout: The time at which the connection will be aborted, in milliseconds.
+ *
+ * This function will override the timeouts for the DTLS heartbeat
+ * protocol. The retransmission timeout is the time after which a
+ * message from the peer is not received, the previous request will
+ * be retransmitted. The total timeout is the time after which the
+ * handshake will be aborted with %GNUTLS_E_TIMEDOUT.
+ *
+ * Since: 3.1.2
+ **/
+void gnutls_heartbeat_set_timeouts(gnutls_session_t session,
+ unsigned int retrans_timeout,
+ unsigned int total_timeout)
+{
+ session->internals.hb_retrans_timeout_ms = retrans_timeout;
+ session->internals.hb_total_timeout_ms = total_timeout;
+}
+
+
+static int
+_gnutls_heartbeat_recv_params(gnutls_session_t session,
+ const uint8_t * data, size_t _data_size)
+{
+ unsigned policy;
+ gnutls_ext_priv_data_t epriv;
+
+ if (_gnutls_hello_ext_get_priv
+ (session, GNUTLS_EXTENSION_HEARTBEAT, &epriv) < 0) {
+ if (session->security_parameters.entity == GNUTLS_CLIENT)
+ return
+ gnutls_assert_val
+ (GNUTLS_E_RECEIVED_ILLEGAL_PARAMETER);
+ return 0; /* Not enabled */
+ }
+
+ if (_data_size == 0)
+ return GNUTLS_E_UNEXPECTED_PACKET_LENGTH;
+
+ policy = (intptr_t)epriv;
+
+ if (data[0] == 1)
+ policy |= LOCAL_ALLOWED_TO_SEND;
+ else if (data[0] == 2)
+ policy |= LOCAL_NOT_ALLOWED_TO_SEND;
+ else
+ return
+ gnutls_assert_val(GNUTLS_E_RECEIVED_ILLEGAL_PARAMETER);
+
+ epriv = (void*)(intptr_t)policy;
+ _gnutls_hello_ext_set_priv(session, GNUTLS_EXTENSION_HEARTBEAT,
+ epriv);
+
+ return 0;
+}
+
+static int
+_gnutls_heartbeat_send_params(gnutls_session_t session,
+ gnutls_buffer_st * extdata)
+{
+ gnutls_ext_priv_data_t epriv;
+ uint8_t p;
+
+ if (_gnutls_hello_ext_get_priv
+ (session, GNUTLS_EXTENSION_HEARTBEAT, &epriv) < 0)
+ return 0; /* nothing to send - not enabled */
+
+ if (((intptr_t)epriv) & GNUTLS_HB_PEER_ALLOWED_TO_SEND)
+ p = 1;
+ else /*if (epriv.num & GNUTLS_HB_PEER_NOT_ALLOWED_TO_SEND) */
+ p = 2;
+
+ if (_gnutls_buffer_append_data(extdata, &p, 1) < 0)
+ return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);
+
+ return 1;
+}
+
+static int
+_gnutls_heartbeat_pack(gnutls_ext_priv_data_t epriv, gnutls_buffer_st * ps)
+{
+ int ret;
+
+ BUFFER_APPEND_NUM(ps, (intptr_t)epriv);
+
+ return 0;
+
+}
+
+static int
+_gnutls_heartbeat_unpack(gnutls_buffer_st * ps,
+ gnutls_ext_priv_data_t * _priv)
+{
+ gnutls_ext_priv_data_t epriv;
+ int ret;
+
+ BUFFER_POP_CAST_NUM(ps, epriv);
+
+ *_priv = epriv;
+
+ ret = 0;
+ error:
+ return ret;
+}
+
+const hello_ext_entry_st ext_mod_heartbeat = {
+ .name = "Heartbeat",
+ .tls_id = 15,
+ .gid = GNUTLS_EXTENSION_HEARTBEAT,
+ .client_parse_point = GNUTLS_EXT_TLS,
+ .server_parse_point = GNUTLS_EXT_TLS,
+ .validity = GNUTLS_EXT_FLAG_TLS | GNUTLS_EXT_FLAG_DTLS | GNUTLS_EXT_FLAG_CLIENT_HELLO |
+ GNUTLS_EXT_FLAG_EE | GNUTLS_EXT_FLAG_TLS12_SERVER_HELLO,
+ .recv_func = _gnutls_heartbeat_recv_params,
+ .send_func = _gnutls_heartbeat_send_params,
+ .pack_func = _gnutls_heartbeat_pack,
+ .unpack_func = _gnutls_heartbeat_unpack,
+ .deinit_func = NULL,
+ .cannot_be_overriden = 1
+};
+
+#else
+void gnutls_heartbeat_enable(gnutls_session_t session, unsigned int type)
+{
+}
+
+unsigned gnutls_heartbeat_allowed(gnutls_session_t session, unsigned int type)
+{
+ return 0;
+}
+
+int
+gnutls_heartbeat_ping(gnutls_session_t session, size_t data_size,
+ unsigned int max_tries, unsigned int flags)
+{
+ return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);
+}
+
+int gnutls_heartbeat_pong(gnutls_session_t session, unsigned int flags)
+{
+ return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);
+}
+
+unsigned int gnutls_heartbeat_get_timeout(gnutls_session_t session)
+{
+ return 0;
+}
+
+void gnutls_heartbeat_set_timeouts(gnutls_session_t session,
+ unsigned int retrans_timeout,
+ unsigned int total_timeout)
+{
+ return;
+}
+#endif