668 lines
12 KiB
C
668 lines
12 KiB
C
/**
|
|
* @file turnc.c TURN Client implementation
|
|
*
|
|
* Copyright (C) 2010 Creytiv.com
|
|
*/
|
|
#include <re_types.h>
|
|
#include <re_fmt.h>
|
|
#include <re_mem.h>
|
|
#include <re_mbuf.h>
|
|
#include <re_md5.h>
|
|
#include <re_list.h>
|
|
#include <re_hash.h>
|
|
#include <re_tmr.h>
|
|
#include <re_sa.h>
|
|
#include <re_udp.h>
|
|
#include <re_tcp.h>
|
|
#include <re_stun.h>
|
|
#include <re_turn.h>
|
|
#include "turnc.h"
|
|
|
|
|
|
#define DEBUG_MODULE "turnc"
|
|
#define DEBUG_LEVEL 5
|
|
#include <re_dbg.h>
|
|
|
|
|
|
enum {
|
|
PERM_HASH_SIZE = 16,
|
|
CHAN_HASH_SIZE = 16,
|
|
FAILC_MAX = 16, /**< Maximum number of request errors for loopcheck. */
|
|
STUN_ATTR_ADDR4_SIZE = 8,
|
|
STUN_ATTR_ADDR6_SIZE = 20,
|
|
};
|
|
|
|
|
|
static const uint8_t sendind_tid[STUN_TID_SIZE];
|
|
|
|
static int allocate_request(struct turnc *t);
|
|
static int refresh_request(struct turnc *t, uint32_t lifetime, bool reset_ls,
|
|
stun_resp_h *resph, void *arg);
|
|
static void refresh_resp_handler(int err, uint16_t scode, const char *reason,
|
|
const struct stun_msg *msg, void *arg);
|
|
|
|
|
|
static void destructor(void *arg)
|
|
{
|
|
struct turnc *turnc = arg;
|
|
|
|
if (turnc->allocated)
|
|
(void)refresh_request(turnc, 0, true, NULL, NULL);
|
|
|
|
tmr_cancel(&turnc->tmr);
|
|
mem_deref(turnc->ct);
|
|
|
|
hash_flush(turnc->perms);
|
|
mem_deref(turnc->perms);
|
|
mem_deref(turnc->chans);
|
|
mem_deref(turnc->username);
|
|
mem_deref(turnc->password);
|
|
mem_deref(turnc->nonce);
|
|
mem_deref(turnc->realm);
|
|
mem_deref(turnc->stun);
|
|
mem_deref(turnc->uh);
|
|
mem_deref(turnc->sock);
|
|
}
|
|
|
|
|
|
static void timeout(void *arg)
|
|
{
|
|
struct turnc *turnc = arg;
|
|
int err;
|
|
|
|
err = refresh_request(turnc, turnc->lifetime, true,
|
|
refresh_resp_handler, turnc);
|
|
if (err)
|
|
turnc->th(err, 0, NULL, NULL, NULL, turnc->arg);
|
|
}
|
|
|
|
|
|
static void refresh_timer(struct turnc *turnc)
|
|
{
|
|
const uint32_t t = turnc->lifetime*1000*3/4;
|
|
|
|
DEBUG_INFO("Start refresh timer.. %u seconds\n", t/1000);
|
|
|
|
tmr_start(&turnc->tmr, t, timeout, turnc);
|
|
}
|
|
|
|
|
|
static void allocate_resp_handler(int err, uint16_t scode, const char *reason,
|
|
const struct stun_msg *msg, void *arg)
|
|
{
|
|
struct stun_attr *map = NULL, *rel = NULL, *ltm, *alt;
|
|
struct turnc *turnc = arg;
|
|
|
|
if (err || turnc_request_loops(&turnc->ls, scode))
|
|
goto out;
|
|
|
|
switch (scode) {
|
|
|
|
case 0:
|
|
map = stun_msg_attr(msg, STUN_ATTR_XOR_MAPPED_ADDR);
|
|
rel = stun_msg_attr(msg, STUN_ATTR_XOR_RELAY_ADDR);
|
|
ltm = stun_msg_attr(msg, STUN_ATTR_LIFETIME);
|
|
if (!rel || !map) {
|
|
DEBUG_WARNING("xor_mapped/relay addr attr missing\n");
|
|
err = EPROTO;
|
|
break;
|
|
}
|
|
|
|
if (ltm)
|
|
turnc->lifetime = ltm->v.lifetime;
|
|
|
|
turnc->allocated = true;
|
|
refresh_timer(turnc);
|
|
break;
|
|
|
|
case 300:
|
|
alt = stun_msg_attr(msg, STUN_ATTR_ALT_SERVER);
|
|
if (!alt)
|
|
break;
|
|
|
|
turnc->psrv = turnc->srv;
|
|
turnc->srv = alt->v.alt_server;
|
|
|
|
err = allocate_request(turnc);
|
|
if (err)
|
|
break;
|
|
|
|
return;
|
|
|
|
case 401:
|
|
case 438:
|
|
err = turnc_keygen(turnc, msg);
|
|
if (err)
|
|
break;
|
|
|
|
err = allocate_request(turnc);
|
|
if (err)
|
|
break;
|
|
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
out:
|
|
turnc->th(err, scode, reason,
|
|
rel ? &rel->v.xor_relay_addr : NULL,
|
|
map ? &map->v.xor_mapped_addr : NULL,
|
|
turnc->arg);
|
|
}
|
|
|
|
|
|
static int allocate_request(struct turnc *t)
|
|
{
|
|
const int proto = IPPROTO_UDP;
|
|
|
|
return stun_request(&t->ct, t->stun, t->proto, t->sock, &t->srv, 0,
|
|
STUN_METHOD_ALLOCATE,
|
|
t->realm ? t->md5_hash : NULL, sizeof(t->md5_hash),
|
|
false, allocate_resp_handler, t, 6,
|
|
STUN_ATTR_LIFETIME, &t->lifetime,
|
|
STUN_ATTR_REQ_TRANSPORT, &proto,
|
|
STUN_ATTR_USERNAME, t->realm ? t->username : NULL,
|
|
STUN_ATTR_REALM, t->realm,
|
|
STUN_ATTR_NONCE, t->nonce,
|
|
STUN_ATTR_SOFTWARE, stun_software);
|
|
}
|
|
|
|
|
|
static void refresh_resp_handler(int err, uint16_t scode, const char *reason,
|
|
const struct stun_msg *msg, void *arg)
|
|
{
|
|
struct turnc *turnc = arg;
|
|
struct stun_attr *ltm;
|
|
|
|
if (err || turnc_request_loops(&turnc->ls, scode))
|
|
goto out;
|
|
|
|
switch (scode) {
|
|
|
|
case 0:
|
|
ltm = stun_msg_attr(msg, STUN_ATTR_LIFETIME);
|
|
if (ltm)
|
|
turnc->lifetime = ltm->v.lifetime;
|
|
refresh_timer(turnc);
|
|
return;
|
|
|
|
case 401:
|
|
case 438:
|
|
err = turnc_keygen(turnc, msg);
|
|
if (err)
|
|
break;
|
|
|
|
err = refresh_request(turnc, turnc->lifetime, false,
|
|
refresh_resp_handler, turnc);
|
|
if (err)
|
|
break;
|
|
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
out:
|
|
turnc->th(err, scode, reason, NULL, NULL, turnc->arg);
|
|
}
|
|
|
|
|
|
static int refresh_request(struct turnc *t, uint32_t lifetime, bool reset_ls,
|
|
stun_resp_h *resph, void *arg)
|
|
{
|
|
if (!t)
|
|
return EINVAL;
|
|
|
|
if (reset_ls)
|
|
turnc_loopstate_reset(&t->ls);
|
|
|
|
if (t->ct)
|
|
t->ct = mem_deref(t->ct);
|
|
|
|
return stun_request(&t->ct, t->stun, t->proto, t->sock, &t->srv, 0,
|
|
STUN_METHOD_REFRESH,
|
|
t->realm ? t->md5_hash : NULL, sizeof(t->md5_hash),
|
|
false, resph, arg, 5,
|
|
STUN_ATTR_LIFETIME, &lifetime,
|
|
STUN_ATTR_USERNAME, t->realm ? t->username : NULL,
|
|
STUN_ATTR_REALM, t->realm,
|
|
STUN_ATTR_NONCE, t->nonce,
|
|
STUN_ATTR_SOFTWARE, stun_software);
|
|
}
|
|
|
|
|
|
static inline size_t stun_indlen(const struct sa *sa)
|
|
{
|
|
size_t len = STUN_HEADER_SIZE + STUN_ATTR_HEADER_SIZE * 2;
|
|
|
|
switch (sa_af(sa)) {
|
|
|
|
case AF_INET:
|
|
len += STUN_ATTR_ADDR4_SIZE;
|
|
break;
|
|
|
|
#ifdef HAVE_INET6
|
|
case AF_INET6:
|
|
len += STUN_ATTR_ADDR6_SIZE;
|
|
break;
|
|
#endif
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
static bool udp_send_handler(int *err, struct sa *dst, struct mbuf *mb,
|
|
void *arg)
|
|
{
|
|
struct turnc *turnc = arg;
|
|
size_t pos, indlen;
|
|
struct chan *chan;
|
|
|
|
if (mb->pos < CHAN_HDR_SIZE)
|
|
return false;
|
|
|
|
chan = turnc_chan_find_peer(turnc, dst);
|
|
if (chan) {
|
|
struct chan_hdr hdr;
|
|
|
|
hdr.nr = turnc_chan_numb(chan);
|
|
hdr.len = mbuf_get_left(mb);
|
|
|
|
mb->pos -= CHAN_HDR_SIZE;
|
|
*err = turnc_chan_hdr_encode(&hdr, mb);
|
|
mb->pos -= CHAN_HDR_SIZE;
|
|
|
|
*dst = turnc->srv;
|
|
|
|
return false;
|
|
}
|
|
|
|
indlen = stun_indlen(dst);
|
|
|
|
if (mb->pos < indlen)
|
|
return false;
|
|
|
|
mb->pos -= indlen;
|
|
pos = mb->pos;
|
|
*err = stun_msg_encode(mb, STUN_METHOD_SEND, STUN_CLASS_INDICATION,
|
|
sendind_tid, NULL, NULL, 0, false, 0x00, 2,
|
|
STUN_ATTR_XOR_PEER_ADDR, dst,
|
|
STUN_ATTR_DATA, mb);
|
|
mb->pos = pos;
|
|
|
|
*dst = turnc->srv;
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool udp_recv_handler(struct sa *src, struct mbuf *mb, void *arg)
|
|
{
|
|
struct stun_attr *peer, *data;
|
|
struct stun_unknown_attr ua;
|
|
struct turnc *turnc = arg;
|
|
struct stun_msg *msg;
|
|
bool hdld = true;
|
|
|
|
if (!sa_cmp(&turnc->srv, src, SA_ALL) &&
|
|
!sa_cmp(&turnc->psrv, src, SA_ALL))
|
|
return false;
|
|
|
|
if (stun_msg_decode(&msg, mb, &ua)) {
|
|
|
|
struct chan_hdr hdr;
|
|
struct chan *chan;
|
|
|
|
if (turnc_chan_hdr_decode(&hdr, mb))
|
|
return true;
|
|
|
|
if (mbuf_get_left(mb) < hdr.len)
|
|
return true;
|
|
|
|
chan = turnc_chan_find_numb(turnc, hdr.nr);
|
|
if (!chan)
|
|
return true;
|
|
|
|
*src = *turnc_chan_peer(chan);
|
|
|
|
return false;
|
|
}
|
|
|
|
switch (stun_msg_class(msg)) {
|
|
|
|
case STUN_CLASS_INDICATION:
|
|
if (ua.typec > 0)
|
|
break;
|
|
|
|
if (stun_msg_method(msg) != STUN_METHOD_DATA)
|
|
break;
|
|
|
|
peer = stun_msg_attr(msg, STUN_ATTR_XOR_PEER_ADDR);
|
|
data = stun_msg_attr(msg, STUN_ATTR_DATA);
|
|
if (!peer || !data)
|
|
break;
|
|
|
|
*src = peer->v.xor_peer_addr;
|
|
|
|
mb->pos = data->v.data.pos;
|
|
mb->end = data->v.data.end;
|
|
|
|
hdld = false;
|
|
break;
|
|
|
|
case STUN_CLASS_ERROR_RESP:
|
|
case STUN_CLASS_SUCCESS_RESP:
|
|
(void)stun_ctrans_recv(turnc->stun, msg, &ua);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
mem_deref(msg);
|
|
|
|
return hdld;
|
|
}
|
|
|
|
|
|
/**
|
|
* Allocate a TURN Client
|
|
*
|
|
* @param turncp Pointer to allocated TURN Client
|
|
* @param conf Optional STUN Configuration
|
|
* @param proto Transport Protocol
|
|
* @param sock Transport socket
|
|
* @param layer Transport layer
|
|
* @param srv TURN Server IP-address
|
|
* @param username Authentication username
|
|
* @param password Authentication password
|
|
* @param lifetime Allocate lifetime in [seconds]
|
|
* @param th TURN handler
|
|
* @param arg Handler argument
|
|
*
|
|
* @return 0 if success, otherwise errorcode
|
|
*/
|
|
int turnc_alloc(struct turnc **turncp, const struct stun_conf *conf, int proto,
|
|
void *sock, int layer, const struct sa *srv,
|
|
const char *username, const char *password,
|
|
uint32_t lifetime, turnc_h *th, void *arg)
|
|
{
|
|
struct turnc *turnc;
|
|
int err;
|
|
|
|
if (!turncp || !sock || !srv || !username || !password || !th)
|
|
return EINVAL;
|
|
|
|
turnc = mem_zalloc(sizeof(*turnc), destructor);
|
|
if (!turnc)
|
|
return ENOMEM;
|
|
|
|
err = stun_alloc(&turnc->stun, conf, NULL, NULL);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = str_dup(&turnc->username, username);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = str_dup(&turnc->password, password);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = turnc_perm_hash_alloc(&turnc->perms, PERM_HASH_SIZE);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = turnc_chan_hash_alloc(&turnc->chans, CHAN_HASH_SIZE);
|
|
if (err)
|
|
goto out;
|
|
|
|
tmr_init(&turnc->tmr);
|
|
turnc->proto = proto;
|
|
turnc->sock = mem_ref(sock);
|
|
turnc->psrv = *srv;
|
|
turnc->srv = *srv;
|
|
turnc->lifetime = lifetime;
|
|
turnc->th = th;
|
|
turnc->arg = arg;
|
|
|
|
switch (proto) {
|
|
|
|
case IPPROTO_UDP:
|
|
err = udp_register_helper(&turnc->uh, sock, layer,
|
|
udp_send_handler, udp_recv_handler,
|
|
turnc);
|
|
break;
|
|
|
|
default:
|
|
err = 0;
|
|
break;
|
|
}
|
|
|
|
if (err)
|
|
goto out;
|
|
|
|
err = allocate_request(turnc);
|
|
if (err)
|
|
goto out;
|
|
|
|
out:
|
|
if (err)
|
|
mem_deref(turnc);
|
|
else
|
|
*turncp = turnc;
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
int turnc_send(struct turnc *turnc, const struct sa *dst, struct mbuf *mb)
|
|
{
|
|
size_t pos, indlen;
|
|
struct chan *chan;
|
|
int err;
|
|
|
|
if (!turnc || !dst || !mb)
|
|
return EINVAL;
|
|
|
|
chan = turnc_chan_find_peer(turnc, dst);
|
|
if (chan) {
|
|
struct chan_hdr hdr;
|
|
|
|
if (mb->pos < CHAN_HDR_SIZE)
|
|
return EINVAL;
|
|
|
|
hdr.nr = turnc_chan_numb(chan);
|
|
hdr.len = mbuf_get_left(mb);
|
|
|
|
mb->pos -= CHAN_HDR_SIZE;
|
|
pos = mb->pos;
|
|
|
|
err = turnc_chan_hdr_encode(&hdr, mb);
|
|
if (err)
|
|
return err;
|
|
|
|
if (turnc->proto == IPPROTO_TCP) {
|
|
|
|
mb->pos = mb->end;
|
|
|
|
/* padding */
|
|
while (hdr.len++ & 0x03) {
|
|
err = mbuf_write_u8(mb, 0x00);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
mb->pos = pos;
|
|
}
|
|
else {
|
|
indlen = stun_indlen(dst);
|
|
|
|
if (mb->pos < indlen)
|
|
return EINVAL;
|
|
|
|
mb->pos -= indlen;
|
|
pos = mb->pos;
|
|
|
|
err = stun_msg_encode(mb, STUN_METHOD_SEND,
|
|
STUN_CLASS_INDICATION, sendind_tid,
|
|
NULL, NULL, 0, false, 0x00, 2,
|
|
STUN_ATTR_XOR_PEER_ADDR, dst,
|
|
STUN_ATTR_DATA, mb);
|
|
if (err)
|
|
return err;
|
|
|
|
mb->pos = pos;
|
|
}
|
|
|
|
switch (turnc->proto) {
|
|
|
|
case IPPROTO_UDP:
|
|
err = udp_send(turnc->sock, &turnc->srv, mb);
|
|
break;
|
|
|
|
case IPPROTO_TCP:
|
|
err = tcp_send(turnc->sock, mb);
|
|
break;
|
|
|
|
default:
|
|
err = EPROTONOSUPPORT;
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
int turnc_recv(struct turnc *turnc, struct sa *src, struct mbuf *mb)
|
|
{
|
|
struct stun_attr *peer, *data;
|
|
struct stun_unknown_attr ua;
|
|
struct stun_msg *msg;
|
|
int err = 0;
|
|
|
|
if (!turnc || !src || !mb)
|
|
return EINVAL;
|
|
|
|
if (stun_msg_decode(&msg, mb, &ua)) {
|
|
|
|
struct chan_hdr hdr;
|
|
struct chan *chan;
|
|
|
|
if (turnc_chan_hdr_decode(&hdr, mb))
|
|
return EBADMSG;
|
|
|
|
if (mbuf_get_left(mb) < hdr.len)
|
|
return EBADMSG;
|
|
|
|
chan = turnc_chan_find_numb(turnc, hdr.nr);
|
|
if (!chan)
|
|
return EBADMSG;
|
|
|
|
*src = *turnc_chan_peer(chan);
|
|
|
|
return 0;
|
|
}
|
|
|
|
switch (stun_msg_class(msg)) {
|
|
|
|
case STUN_CLASS_INDICATION:
|
|
if (ua.typec > 0) {
|
|
err = ENOSYS;
|
|
break;
|
|
}
|
|
|
|
if (stun_msg_method(msg) != STUN_METHOD_DATA) {
|
|
err = ENOSYS;
|
|
break;
|
|
}
|
|
|
|
peer = stun_msg_attr(msg, STUN_ATTR_XOR_PEER_ADDR);
|
|
data = stun_msg_attr(msg, STUN_ATTR_DATA);
|
|
if (!peer || !data) {
|
|
err = EPROTO;
|
|
break;
|
|
}
|
|
|
|
*src = peer->v.xor_peer_addr;
|
|
|
|
mb->pos = data->v.data.pos;
|
|
mb->end = data->v.data.end;
|
|
break;
|
|
|
|
case STUN_CLASS_ERROR_RESP:
|
|
case STUN_CLASS_SUCCESS_RESP:
|
|
(void)stun_ctrans_recv(turnc->stun, msg, &ua);
|
|
mb->pos = mb->end;
|
|
break;
|
|
|
|
default:
|
|
err = ENOSYS;
|
|
break;
|
|
}
|
|
|
|
mem_deref(msg);
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
bool turnc_request_loops(struct loop_state *ls, uint16_t scode)
|
|
{
|
|
bool loop = false;
|
|
|
|
switch (scode) {
|
|
|
|
case 0:
|
|
ls->failc = 0;
|
|
break;
|
|
|
|
default:
|
|
if (ls->last_scode == scode)
|
|
loop = true;
|
|
/*@fallthrough@*/
|
|
case 300:
|
|
if (++ls->failc >= FAILC_MAX)
|
|
loop = true;
|
|
|
|
break;
|
|
}
|
|
|
|
ls->last_scode = scode;
|
|
|
|
return loop;
|
|
}
|
|
|
|
|
|
void turnc_loopstate_reset(struct loop_state *ls)
|
|
{
|
|
if (!ls)
|
|
return;
|
|
|
|
ls->last_scode = 0;
|
|
ls->failc = 0;
|
|
}
|
|
|
|
|
|
int turnc_keygen(struct turnc *turnc, const struct stun_msg *msg)
|
|
{
|
|
struct stun_attr *realm, *nonce;
|
|
|
|
realm = stun_msg_attr(msg, STUN_ATTR_REALM);
|
|
nonce = stun_msg_attr(msg, STUN_ATTR_NONCE);
|
|
if (!realm || !nonce)
|
|
return EPROTO;
|
|
|
|
mem_deref(turnc->realm);
|
|
mem_deref(turnc->nonce);
|
|
turnc->realm = mem_ref(realm->v.realm);
|
|
turnc->nonce = mem_ref(nonce->v.nonce);
|
|
|
|
return md5_printf(turnc->md5_hash, "%s:%s:%s",
|
|
turnc->username, turnc->realm, turnc->password);
|
|
}
|