blob: 669c6cf73c763fd1532ec51b51767cbafcea93c1 [file] [log] [blame]
/**
* @file openssl/tls_udp.c DTLS backend using OpenSSL
*
* Copyright (C) 2010 Creytiv.com
*/
#include <sys/time.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <re_types.h>
#include <re_fmt.h>
#include <re_mem.h>
#include <re_mbuf.h>
#include <re_list.h>
#include <re_hash.h>
#include <re_sa.h>
#include <re_srtp.h>
#include <re_udp.h>
#include <re_tmr.h>
#include <re_tls.h>
#include "tls.h"
#define DEBUG_MODULE "dtls"
#define DEBUG_LEVEL 5
#include <re_dbg.h>
enum {
MTU_DEFAULT = 1400,
MTU_FALLBACK = 548,
HEADROOM_DEFAULT = 4,
};
struct dtls_sock {
struct sa peer;
struct udp_helper *uh;
struct udp_sock *us;
struct hash *ht;
struct mbuf *mb;
dtls_conn_h *connh;
dtls_send_h *sendh;
dtls_mtu_h *mtuh;
void *arg;
size_t mtu;
size_t headroom;
};
/* NOTE: shadow struct defined in tls_*.c */
struct tls_conn {
SSL *ssl; /* inheritance */
#ifdef TLS_BIO_OPAQUE
BIO_METHOD *biomet;
#endif
BIO *sbio_out;
BIO *sbio_in;
struct tmr tmr;
struct sa peer;
struct le he;
struct dtls_sock *sock;
dtls_estab_h *estabh;
dtls_recv_h *recvh;
dtls_close_h *closeh;
void *arg;
bool active;
bool up;
};
static int bio_create(BIO *b)
{
#ifdef TLS_BIO_OPAQUE
BIO_set_init(b, 1);
BIO_set_data(b, NULL);
BIO_set_flags(b, 0);
#else
b->init = 1;
b->num = 0;
b->ptr = NULL;
b->flags = 0;
#endif
return 1;
}
static int bio_destroy(BIO *b)
{
if (!b)
return 0;
#ifdef TLS_BIO_OPAQUE
BIO_set_init(b, 0);
BIO_set_data(b, NULL);
BIO_set_flags(b, 0);
#else
b->ptr = NULL;
b->init = 0;
b->flags = 0;
#endif
return 1;
}
static int bio_write(BIO *b, const char *buf, int len)
{
#ifdef TLS_BIO_OPAQUE
struct tls_conn *tc = BIO_get_data(b);
#else
struct tls_conn *tc = b->ptr;
#endif
struct mbuf *mb;
int err;
mb = mbuf_alloc(tc->sock->headroom + len);
if (!mb)
return -1;
mb->pos = tc->sock->headroom;
(void)mbuf_write_mem(mb, (void *)buf, len);
mb->pos = tc->sock->headroom;
err = tc->sock->sendh(tc, &tc->peer, mb, tc->arg);
mem_deref(mb);
return err ? -1 : len;
}
static long bio_ctrl(BIO *b, int cmd, long num, void *ptr)
{
#ifdef TLS_BIO_OPAQUE
struct tls_conn *tc = BIO_get_data(b);
#else
struct tls_conn *tc = b->ptr;
#endif
(void)num;
(void)ptr;
switch (cmd) {
case BIO_CTRL_FLUSH:
/* The OpenSSL library needs this */
return 1;
#if defined (BIO_CTRL_DGRAM_QUERY_MTU)
case BIO_CTRL_DGRAM_QUERY_MTU:
if (tc) {
if (tc->sock->mtuh) {
return tc->sock->mtuh(tc, tc->arg);
}
else {
return tc->sock->mtu;
}
}
else {
return MTU_DEFAULT;
}
#endif
#if defined (BIO_CTRL_DGRAM_GET_FALLBACK_MTU)
case BIO_CTRL_DGRAM_GET_FALLBACK_MTU:
return MTU_FALLBACK;
#endif
}
return 0;
}
#ifdef TLS_BIO_OPAQUE
static BIO_METHOD *bio_method_udp(void)
{
BIO_METHOD *method;
method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "udp_send");
if (!method) {
DEBUG_WARNING("alloc: BIO_meth_new() failed\n");
ERR_clear_error();
return NULL;
}
BIO_meth_set_write(method, bio_write);
BIO_meth_set_ctrl(method, bio_ctrl);
BIO_meth_set_create(method, bio_create);
BIO_meth_set_destroy(method, bio_destroy);
return method;
}
#else
static struct bio_method_st bio_udp_send = {
BIO_TYPE_SOURCE_SINK,
"udp_send",
bio_write,
0,
0,
0,
bio_ctrl,
bio_create,
bio_destroy,
0
};
#endif
static void tls_close(struct tls_conn *tc)
{
int r;
if (!tc->ssl)
return;
r = SSL_shutdown(tc->ssl);
if (r <= 0)
ERR_clear_error();
SSL_free(tc->ssl);
tc->ssl = NULL;
}
static void conn_destructor(void *arg)
{
struct tls_conn *tc = arg;
hash_unlink(&tc->he);
tmr_cancel(&tc->tmr);
tls_close(tc);
#ifdef TLS_BIO_OPAQUE
if (tc->biomet)
BIO_meth_free(tc->biomet);
#endif
mem_deref(tc->sock);
}
static void conn_close(struct tls_conn *tc, int err)
{
tmr_cancel(&tc->tmr);
tls_close(tc);
tc->up = false;
if (tc->closeh)
tc->closeh(err, tc->arg);
}
#if defined (DTLS_CTRL_HANDLE_TIMEOUT) && defined(DTLS_CTRL_GET_TIMEOUT)
static void check_timer(struct tls_conn *tc);
static void timeout(void *arg)
{
struct tls_conn *tc = arg;
DEBUG_INFO("timeout\n");
if (0 <= DTLSv1_handle_timeout(tc->ssl)) {
check_timer(tc);
}
else {
ERR_clear_error();
conn_close(tc, ETIMEDOUT);
}
}
static void check_timer(struct tls_conn *tc)
{
struct timeval tv = {0, 0};
if (1 == DTLSv1_get_timeout(tc->ssl, &tv)) {
tmr_start(&tc->tmr, tv.tv_sec * 1000 + tv.tv_usec / 1000,
timeout, tc);
}
else {
tmr_cancel(&tc->tmr);
}
}
#else
static void check_timer(struct tls_conn *tc)
{
(void)tc;
}
#endif
static int tls_connect(struct tls_conn *tc)
{
int r;
ERR_clear_error();
r = SSL_connect(tc->ssl);
if (r <= 0) {
const int ssl_err = SSL_get_error(tc->ssl, r);
tls_flush_error();
switch (ssl_err) {
case SSL_ERROR_WANT_READ:
break;
default:
DEBUG_WARNING("connect error: %i\n", ssl_err);
return EPROTO;
}
}
check_timer(tc);
return 0;
}
static int tls_accept(struct tls_conn *tc)
{
int r;
ERR_clear_error();
r = SSL_accept(tc->ssl);
if (r <= 0) {
const int ssl_err = SSL_get_error(tc->ssl, r);
tls_flush_error();
switch (ssl_err) {
case SSL_ERROR_WANT_READ:
break;
default:
DEBUG_WARNING("accept error: %i\n", ssl_err);
return EPROTO;
}
}
check_timer(tc);
return 0;
}
static void conn_recv(struct tls_conn *tc, struct mbuf *mb)
{
int err, r;
if (!tc->ssl)
return;
/* feed SSL data to the BIO */
r = BIO_write(tc->sbio_in, mbuf_buf(mb), (int)mbuf_get_left(mb));
if (r <= 0) {
DEBUG_WARNING("receive bio write error: %i\n", r);
ERR_clear_error();
conn_close(tc, ENOMEM);
return;
}
if (SSL_state(tc->ssl) != SSL_ST_OK) {
if (tc->up) {
conn_close(tc, EPROTO);
return;
}
if (tc->active) {
err = tls_connect(tc);
}
else {
err = tls_accept(tc);
}
if (err) {
conn_close(tc, err);
return;
}
DEBUG_INFO("%s: state=0x%04x\n",
tc->active ? "client" : "server",
SSL_state(tc->ssl));
/* TLS connection is established */
if (SSL_state(tc->ssl) != SSL_ST_OK)
return;
tc->up = true;
if (tc->estabh) {
uint32_t nrefs;
mem_ref(tc);
tc->estabh(tc->arg);
nrefs = mem_nrefs(tc);
mem_deref(tc);
/* check if connection was deref'd from handler */
if (nrefs == 1)
return;
}
}
mbuf_set_pos(mb, 0);
for (;;) {
int n;
if (mbuf_get_space(mb) < 4096) {
err = mbuf_resize(mb, mb->size + 8192);
if (err) {
conn_close(tc, err);
return;
}
}
ERR_clear_error();
n = SSL_read(tc->ssl, mbuf_buf(mb), (int)mbuf_get_space(mb));
if (n <= 0) {
const int ssl_err = SSL_get_error(tc->ssl, n);
ERR_clear_error();
switch (ssl_err) {
case SSL_ERROR_WANT_READ:
break;
case SSL_ERROR_ZERO_RETURN:
conn_close(tc, ECONNRESET);
return;
default:
DEBUG_WARNING("read error: %i\n", ssl_err);
conn_close(tc, EPROTO);
return;
}
break;
}
mb->pos += n;
}
mbuf_set_end(mb, mb->pos);
mbuf_set_pos(mb, 0);
if (tc->recvh && mbuf_get_left(mb) > 0)
tc->recvh(mb, tc->arg);
return;
}
static int conn_alloc(struct tls_conn **ptc, struct tls *tls,
struct dtls_sock *sock, const struct sa *peer,
dtls_estab_h *estabh, dtls_recv_h *recvh,
dtls_close_h *closeh, void *arg)
{
struct tls_conn *tc;
int err = 0;
tc = mem_zalloc(sizeof(*tc), conn_destructor);
if (!tc)
return ENOMEM;
hash_append(sock->ht, sa_hash(peer, SA_ALL), &tc->he, tc);
tc->sock = mem_ref(sock);
tc->peer = *peer;
tc->estabh = estabh;
tc->recvh = recvh;
tc->closeh = closeh;
tc->arg = arg;
#ifdef TLS_BIO_OPAQUE
tc->biomet = bio_method_udp();
if (!tc->biomet) {
err = ENOMEM;
goto out;
}
#endif
/* Connect the SSL socket */
tc->ssl = SSL_new(tls->ctx);
if (!tc->ssl) {
DEBUG_WARNING("ssl new failed: %i\n",
ERR_GET_REASON(ERR_get_error()));
ERR_clear_error();
err = ENOMEM;
goto out;
}
tc->sbio_in = BIO_new(BIO_s_mem());
if (!tc->sbio_in) {
ERR_clear_error();
err = ENOMEM;
goto out;
}
#ifdef TLS_BIO_OPAQUE
tc->sbio_out = BIO_new(tc->biomet);
#else
tc->sbio_out = BIO_new(&bio_udp_send);
#endif
if (!tc->sbio_out) {
ERR_clear_error();
BIO_free(tc->sbio_in);
err = ENOMEM;
goto out;
}
#ifdef TLS_BIO_OPAQUE
BIO_set_data(tc->sbio_out, tc);
#else
tc->sbio_out->ptr = tc;
#endif
SSL_set_bio(tc->ssl, tc->sbio_in, tc->sbio_out);
SSL_set_read_ahead(tc->ssl, 1);
out:
if (err)
mem_deref(tc);
else
*ptc = tc;
return err;
}
/**
* DTLS Connect
*
* @param ptc Pointer to allocated DTLS connection
* @param tls TLS Context
* @param sock DTLS Socket
* @param peer Peer address
* @param estabh Establish handler
* @param recvh Receive handler
* @param closeh Close handler
* @param arg Handler argument
*
* @return 0 if success, otherwise errorcode
*/
int dtls_connect(struct tls_conn **ptc, struct tls *tls,
struct dtls_sock *sock, const struct sa *peer,
dtls_estab_h *estabh, dtls_recv_h *recvh,
dtls_close_h *closeh, void *arg)
{
struct tls_conn *tc;
int err;
if (!ptc || !tls || !sock || !peer)
return EINVAL;
err = conn_alloc(&tc, tls, sock, peer, estabh, recvh, closeh, arg);
if (err)
return err;
tc->active = true;
err = tls_connect(tc);
if (err)
goto out;
out:
if (err)
mem_deref(tc);
else
*ptc = tc;
return err;
}
/**
* DTLS Accept
*
* @param ptc Pointer to allocated DTLS connection
* @param tls TLS Context
* @param sock DTLS Socket
* @param estabh Establish handler
* @param recvh Receive handler
* @param closeh Close handler
* @param arg Handler argument
*
* @return 0 if success, otherwise errorcode
*/
int dtls_accept(struct tls_conn **ptc, struct tls *tls,
struct dtls_sock *sock,
dtls_estab_h *estabh, dtls_recv_h *recvh,
dtls_close_h *closeh, void *arg)
{
struct tls_conn *tc;
int err, r;
if (!ptc || !tls || !sock || !sock->mb)
return EINVAL;
err = conn_alloc(&tc, tls, sock, &sock->peer, estabh, recvh, closeh,
arg);
if (err)
return err;
tc->active = false;
r = BIO_write(tc->sbio_in, mbuf_buf(sock->mb),
(int)mbuf_get_left(sock->mb));
if (r <= 0) {
DEBUG_WARNING("accept bio write error: %i\n", r);
ERR_clear_error();
err = ENOMEM;
goto out;
}
err = tls_accept(tc);
if (err)
goto out;
sock->mb = mem_deref(sock->mb);
out:
if (err)
mem_deref(tc);
else
*ptc = tc;
return err;
}
/**
* Send data on a DTLS connection
*
* @param tc DTLS connection
* @param mb Buffer to send
*
* @return 0 if success, otherwise errorcode
*/
int dtls_send(struct tls_conn *tc, struct mbuf *mb)
{
int r;
if (!tc || !mb)
return EINVAL;
if (!tc->up || !tc->ssl)
return ENOTCONN;
ERR_clear_error();
r = SSL_write(tc->ssl, mbuf_buf(mb), (int)mbuf_get_left(mb));
if (r <= 0) {
DEBUG_WARNING("write error: %i\n", SSL_get_error(tc->ssl, r));
ERR_clear_error();
return EPROTO;
}
return 0;
}
/**
* Set handlers on a DTLS Connection
*
* @param tc DTLS Connection
* @param estabh DTLS Connection Established handler
* @param recvh DTLS Connection Receive data handler
* @param closeh DTLS Connection Close handler
* @param arg Handler argument
*/
void dtls_set_handlers(struct tls_conn *tc, dtls_estab_h *estabh,
dtls_recv_h *recvh, dtls_close_h *closeh, void *arg)
{
if (!tc)
return;
tc->estabh = estabh;
tc->recvh = recvh;
tc->closeh = closeh;
tc->arg = arg;
}
/**
* Get the remote peer of a DTLS Connection
*
* @param tc DTLS Connection
*
* @return Remote peer
*/
const struct sa *dtls_peer(const struct tls_conn *tc)
{
return tc ? &tc->peer : NULL;
}
/**
* Set the remote peer of a DTLS Connection
*
* @param tc DTLS Connection
* @param peer Peer address
*/
void dtls_set_peer(struct tls_conn *tc, const struct sa *peer)
{
if (!tc || !peer)
return;
hash_unlink(&tc->he);
hash_append(tc->sock->ht, sa_hash(peer, SA_ALL), &tc->he, tc);
tc->peer = *peer;
}
static void sock_destructor(void *arg)
{
struct dtls_sock *sock = arg;
hash_clear(sock->ht);
mem_deref(sock->uh);
mem_deref(sock->us);
mem_deref(sock->ht);
mem_deref(sock->mb);
}
static bool cmp_handler(struct le *le, void *arg)
{
struct tls_conn *tc = le->data;
return sa_cmp(&tc->peer, arg, SA_ALL);
}
static struct tls_conn *conn_lookup(struct dtls_sock *sock,
const struct sa *peer)
{
return list_ledata(hash_lookup(sock->ht, sa_hash(peer, SA_ALL),
cmp_handler, (void *)peer));
}
static int send_handler(struct tls_conn *tc, const struct sa *dst,
struct mbuf *mb, void *arg) {
(void)arg;
return udp_send_helper(tc->sock->us, dst, mb, tc->sock->uh);
}
static bool recv_handler(struct sa *src, struct mbuf *mb, void *arg)
{
struct dtls_sock *sock = arg;
struct tls_conn *tc;
uint8_t b;
if (mbuf_get_left(mb) < 1)
return false;
b = mb->buf[mb->pos];
if (b < 20 || b > 63)
return false;
tc = conn_lookup(sock, src);
if (tc) {
conn_recv(tc, mb);
return true;
}
if (sock->connh) {
mem_deref(sock->mb);
sock->mb = mem_ref(mb);
sock->peer = *src;
sock->connh(src, sock->arg);
}
return true;
}
/**
* Feed data to a DTLS socket
*
* @param sock DTLS socket
* @param src Source address
* @param mb Buffer to receive
*
* @return whether the packet has been handled.
*/
bool dtls_receive(struct dtls_sock *sock, struct sa *src, struct mbuf *mb) {
return recv_handler(src, mb, sock);
}
/**
* Create DTLS Socket
*
* @param sockp Pointer to returned DTLS Socket
* @param laddr Local listen address (optional)
* @param us External UDP socket (optional)
* @param htsize Connection hash table size
* @param layer UDP protocol layer
* @param connh Connect handler
* @param arg Handler argument
*
* @return 0 if success, otherwise errorcode
*/
int dtls_listen(struct dtls_sock **sockp, const struct sa *laddr,
struct udp_sock *us, uint32_t htsize, int layer,
dtls_conn_h *connh, void *arg)
{
struct dtls_sock *sock;
int err;
if (!sockp)
return EINVAL;
sock = mem_zalloc(sizeof(*sock), sock_destructor);
if (!sock)
return ENOMEM;
if (us) {
sock->us = mem_ref(us);
}
else {
err = udp_listen(&sock->us, laddr, NULL, NULL);
if (err)
goto out;
}
err = udp_register_helper(&sock->uh, sock->us, layer,
NULL, recv_handler, sock);
if (err)
goto out;
err = hash_alloc(&sock->ht, hash_valid_size(htsize));
if (err)
goto out;
sock->mtu = MTU_DEFAULT;
sock->headroom = HEADROOM_DEFAULT;
sock->connh = connh;
sock->sendh = send_handler;
sock->arg = arg;
out:
if (err)
mem_deref(sock);
else
*sockp = sock;
return err;
}
/**
* Create a DTLS Socket without using an UDP socket underneath
*
* @param sockp Pointer to returned DTLS Socket
* @param htsize Connection hash table size. Set to 0 if one DTLS session shall
* be used for all peers.
* @param connh Connect handler
* @param sendh Send handler
* @param mtuh MTU handler
* @param arg Handler argument
*
* @return 0 if success, otherwise errorcode
*/
int dtls_socketless(struct dtls_sock **sockp, uint32_t htsize,
dtls_conn_h *connh, dtls_send_h *sendh, dtls_mtu_h *mtuh,
void *arg)
{
struct dtls_sock *sock;
int err;
if (!sockp || !sendh)
return EINVAL;
sock = mem_zalloc(sizeof(*sock), sock_destructor);
if (!sock)
return ENOMEM;
err = hash_alloc(&sock->ht, hash_valid_size(htsize));
if (err)
goto out;
sock->mtu = MTU_DEFAULT;
sock->connh = connh;
sock->sendh = sendh;
sock->mtuh = mtuh;
sock->arg = arg;
out:
if (err)
mem_deref(sock);
else
*sockp = sock;
return err;
}
/**
* Get the underlying UDP socket of a DTLS Socket
*
* @param sock DTLS Socket
*
* @return UDP Socket
*/
struct udp_sock *dtls_udp_sock(struct dtls_sock *sock)
{
return sock ? sock->us : NULL;
}
/**
* Set MTU on a DTLS Socket
*
* @param sock DTLS Socket
* @param mtu MTU value
*/
void dtls_set_mtu(struct dtls_sock *sock, size_t mtu)
{
if (!sock)
return;
sock->mtu = mtu;
}
/*
* Get headroom of a DTLS Socket
*
* @param sock DTLS Socket
*
* @return Headroom value.
*/
size_t dtls_headroom(struct dtls_sock *sock)
{
return sock ? sock->headroom : 0;
}
/**
* Set headroom on a DTLS Socket
*
* @param sock DTLS Socket
* @param headroom Headroom value
*/
void dtls_set_headroom(struct dtls_sock *sock, size_t headroom)
{
if (!sock)
return;
sock->headroom = headroom;
}
void dtls_recv_packet(struct dtls_sock *sock, const struct sa *src,
struct mbuf *mb)
{
struct sa addr;
if (!sock || !src || !mb)
return;
addr = *src;
recv_handler(&addr, mb, sock);
}