blob: 8ab3e3155340980453e49c0370e8fd98858d504e [file] [log] [blame]
/* $Id$ */
/*
* Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
* Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <pjnath/turn_session.h>
#include <pjnath/errno.h>
#include <pjlib-util/srv_resolver.h>
#include <pj/addr_resolv.h>
#include <pj/assert.h>
#include <pj/errno.h>
#include <pj/hash.h>
#include <pj/lock.h>
#include <pj/log.h>
#include <pj/os.h>
#include <pj/pool.h>
#include <pj/rand.h>
#include <pj/sock.h>
#define PJ_TURN_CHANNEL_MIN 0x4000
#define PJ_TURN_CHANNEL_MAX 0x7FFF /* inclusive */
#define PJ_TURN_CHANNEL_HTABLE_SIZE 8
#define PJ_TURN_PERM_HTABLE_SIZE 8
static const char *state_names[] =
{
"Null",
"Resolving",
"Resolved",
"Allocating",
"Ready",
"Deallocating",
"Deallocated",
"Destroying"
};
enum timer_id_t
{
TIMER_NONE,
TIMER_KEEP_ALIVE,
TIMER_DESTROY
};
/* This structure describes a channel binding. A channel binding is index by
* the channel number or IP address and port number of the peer.
*/
struct ch_t
{
/* The channel number */
pj_uint16_t num;
/* PJ_TRUE if we've received successful response to ChannelBind request
* for this channel.
*/
pj_bool_t bound;
/* The peer IP address and port */
pj_sockaddr addr;
/* The channel binding expiration */
pj_time_val expiry;
};
/* This structure describes a permission. A permission is identified by the
* IP address only.
*/
struct perm_t
{
/* Cache of hash value to speed-up lookup */
pj_uint32_t hval;
/* The permission IP address. The port number MUST be zero */
pj_sockaddr addr;
/* Number of peers that uses this permission. */
unsigned peer_cnt;
/* Automatically renew this permission once it expires? */
pj_bool_t renew;
/* The permission expiration */
pj_time_val expiry;
/* Arbitrary/random pointer value (token) to map this perm with the
* request to create it. It is used to invalidate this perm when the
* request fails.
*/
void *req_token;
};
/* The TURN client session structure */
struct pj_turn_session
{
pj_pool_t *pool;
const char *obj_name;
pj_turn_session_cb cb;
void *user_data;
pj_stun_config stun_cfg;
pj_bool_t is_destroying;
pj_grp_lock_t *grp_lock;
int busy;
pj_turn_state_t state;
pj_status_t last_status;
pj_bool_t pending_destroy;
pj_stun_session *stun;
unsigned lifetime;
int ka_interval;
pj_time_val expiry;
pj_timer_heap_t *timer_heap;
pj_timer_entry timer;
pj_uint16_t default_port;
pj_uint16_t af;
pj_turn_tp_type conn_type;
pj_uint16_t srv_addr_cnt;
pj_sockaddr *srv_addr_list;
pj_sockaddr *srv_addr;
pj_bool_t pending_alloc;
pj_turn_alloc_param alloc_param;
pj_sockaddr mapped_addr;
pj_sockaddr relay_addr;
pj_hash_table_t *ch_table;
pj_hash_table_t *perm_table;
pj_uint32_t send_ind_tsx_id[3];
/* tx_pkt must be 16bit aligned */
pj_uint8_t tx_pkt[PJ_TURN_MAX_PKT_LEN];
pj_uint16_t next_ch;
};
/*
* Prototypes.
*/
static void sess_shutdown(pj_turn_session *sess,
pj_status_t status);
static void turn_sess_on_destroy(void *comp);
static void do_destroy(pj_turn_session *sess);
static void send_refresh(pj_turn_session *sess, int lifetime);
static pj_status_t stun_on_send_msg(pj_stun_session *sess,
void *token,
const void *pkt,
pj_size_t pkt_size,
const pj_sockaddr_t *dst_addr,
unsigned addr_len);
static void stun_on_request_complete(pj_stun_session *sess,
pj_status_t status,
void *token,
pj_stun_tx_data *tdata,
const pj_stun_msg *response,
const pj_sockaddr_t *src_addr,
unsigned src_addr_len);
static pj_status_t stun_on_rx_indication(pj_stun_session *sess,
const pj_uint8_t *pkt,
unsigned pkt_len,
const pj_stun_msg *msg,
void *token,
const pj_sockaddr_t *src_addr,
unsigned src_addr_len);
static void dns_srv_resolver_cb(void *user_data,
pj_status_t status,
const pj_dns_srv_record *rec);
static struct ch_t *lookup_ch_by_addr(pj_turn_session *sess,
const pj_sockaddr_t *addr,
unsigned addr_len,
pj_bool_t update,
pj_bool_t bind_channel);
static struct ch_t *lookup_ch_by_chnum(pj_turn_session *sess,
pj_uint16_t chnum);
static struct perm_t *lookup_perm(pj_turn_session *sess,
const pj_sockaddr_t *addr,
unsigned addr_len,
pj_bool_t update);
static void invalidate_perm(pj_turn_session *sess,
struct perm_t *perm);
static void on_timer_event(pj_timer_heap_t *th, pj_timer_entry *e);
/*
* Create default pj_turn_alloc_param.
*/
PJ_DEF(void) pj_turn_alloc_param_default(pj_turn_alloc_param *prm)
{
pj_bzero(prm, sizeof(*prm));
}
/*
* Duplicate pj_turn_alloc_param.
*/
PJ_DEF(void) pj_turn_alloc_param_copy( pj_pool_t *pool,
pj_turn_alloc_param *dst,
const pj_turn_alloc_param *src)
{
PJ_UNUSED_ARG(pool);
pj_memcpy(dst, src, sizeof(*dst));
}
/*
* Get TURN state name.
*/
PJ_DEF(const char*) pj_turn_state_name(pj_turn_state_t state)
{
return state_names[state];
}
/*
* Create TURN client session.
*/
PJ_DEF(pj_status_t) pj_turn_session_create( const pj_stun_config *cfg,
const char *name,
int af,
pj_turn_tp_type conn_type,
pj_grp_lock_t *grp_lock,
const pj_turn_session_cb *cb,
unsigned options,
void *user_data,
pj_turn_session **p_sess)
{
pj_pool_t *pool;
pj_turn_session *sess;
pj_stun_session_cb stun_cb;
pj_status_t status;
PJ_ASSERT_RETURN(cfg && cfg->pf && cb && p_sess, PJ_EINVAL);
PJ_ASSERT_RETURN(cb->on_send_pkt, PJ_EINVAL);
PJ_UNUSED_ARG(options);
if (name == NULL)
name = "turn%p";
/* Allocate and create TURN session */
pool = pj_pool_create(cfg->pf, name, PJNATH_POOL_LEN_TURN_SESS,
PJNATH_POOL_INC_TURN_SESS, NULL);
sess = PJ_POOL_ZALLOC_T(pool, pj_turn_session);
sess->pool = pool;
sess->obj_name = pool->obj_name;
sess->timer_heap = cfg->timer_heap;
sess->af = (pj_uint16_t)af;
sess->conn_type = conn_type;
sess->ka_interval = PJ_TURN_KEEP_ALIVE_SEC;
sess->user_data = user_data;
sess->next_ch = PJ_TURN_CHANNEL_MIN;
/* Copy STUN session */
pj_memcpy(&sess->stun_cfg, cfg, sizeof(pj_stun_config));
/* Copy callback */
pj_memcpy(&sess->cb, cb, sizeof(*cb));
/* Peer hash table */
sess->ch_table = pj_hash_create(pool, PJ_TURN_CHANNEL_HTABLE_SIZE);
/* Permission hash table */
sess->perm_table = pj_hash_create(pool, PJ_TURN_PERM_HTABLE_SIZE);
/* Session lock */
if (grp_lock) {
sess->grp_lock = grp_lock;
} else {
status = pj_grp_lock_create(pool, NULL, &sess->grp_lock);
if (status != PJ_SUCCESS) {
pj_pool_release(pool);
return status;
}
}
pj_grp_lock_add_ref(sess->grp_lock);
pj_grp_lock_add_handler(sess->grp_lock, pool, sess,
&turn_sess_on_destroy);
/* Timer */
pj_timer_entry_init(&sess->timer, TIMER_NONE, sess, &on_timer_event);
/* Create STUN session */
pj_bzero(&stun_cb, sizeof(stun_cb));
stun_cb.on_send_msg = &stun_on_send_msg;
stun_cb.on_request_complete = &stun_on_request_complete;
stun_cb.on_rx_indication = &stun_on_rx_indication;
status = pj_stun_session_create(&sess->stun_cfg, sess->obj_name, &stun_cb,
PJ_FALSE, sess->grp_lock, &sess->stun);
if (status != PJ_SUCCESS) {
do_destroy(sess);
return status;
}
/* Attach ourself to STUN session */
pj_stun_session_set_user_data(sess->stun, sess);
/* Done */
PJ_LOG(4,(sess->obj_name, "TURN client session created"));
*p_sess = sess;
return PJ_SUCCESS;
}
static void turn_sess_on_destroy(void *comp)
{
pj_turn_session *sess = (pj_turn_session*) comp;
/* Destroy pool */
if (sess->pool) {
pj_pool_t *pool = sess->pool;
PJ_LOG(4,(sess->obj_name, "TURN client session destroyed"));
sess->pool = NULL;
pj_pool_release(pool);
}
}
/* Destroy */
static void do_destroy(pj_turn_session *sess)
{
PJ_LOG(4,(sess->obj_name, "TURN session destroy request, ref_cnt=%d",
pj_grp_lock_get_ref(sess->grp_lock)));
pj_grp_lock_acquire(sess->grp_lock);
if (sess->is_destroying) {
pj_grp_lock_release(sess->grp_lock);
return;
}
sess->is_destroying = PJ_TRUE;
pj_timer_heap_cancel_if_active(sess->timer_heap, &sess->timer, TIMER_NONE);
pj_stun_session_destroy(sess->stun);
pj_grp_lock_dec_ref(sess->grp_lock);
pj_grp_lock_release(sess->grp_lock);
}
/* Set session state */
static void set_state(pj_turn_session *sess, enum pj_turn_state_t state)
{
pj_turn_state_t old_state = sess->state;
if (state==sess->state)
return;
PJ_LOG(4,(sess->obj_name, "State changed %s --> %s",
state_names[old_state], state_names[state]));
sess->state = state;
if (sess->cb.on_state) {
(*sess->cb.on_state)(sess, old_state, state);
}
}
/*
* Notify application and shutdown the TURN session.
*/
static void sess_shutdown(pj_turn_session *sess,
pj_status_t status)
{
pj_bool_t can_destroy = PJ_TRUE;
PJ_LOG(4,(sess->obj_name, "Request to shutdown in state %s, cause:%d",
state_names[sess->state], status));
if (sess->last_status == PJ_SUCCESS && status != PJ_SUCCESS)
sess->last_status = status;
switch (sess->state) {
case PJ_TURN_STATE_NULL:
break;
case PJ_TURN_STATE_RESOLVING:
/* Wait for DNS callback invoked, it will call the this function
* again. If the callback happens to get pending_destroy==FALSE,
* the TURN allocation will call this function again.
*/
sess->pending_destroy = PJ_TRUE;
can_destroy = PJ_FALSE;
break;
case PJ_TURN_STATE_RESOLVED:
break;
case PJ_TURN_STATE_ALLOCATING:
/* We need to wait until allocation complete */
sess->pending_destroy = PJ_TRUE;
can_destroy = PJ_FALSE;
break;
case PJ_TURN_STATE_READY:
/* Send REFRESH with LIFETIME=0 */
can_destroy = PJ_FALSE;
send_refresh(sess, 0);
break;
case PJ_TURN_STATE_DEALLOCATING:
can_destroy = PJ_FALSE;
/* This may recursively call this function again with
* state==PJ_TURN_STATE_DEALLOCATED.
*/
/* No need to deallocate as we're already deallocating!
* See https://trac.pjsip.org/repos/ticket/1551
send_refresh(sess, 0);
*/
break;
case PJ_TURN_STATE_DEALLOCATED:
case PJ_TURN_STATE_DESTROYING:
break;
}
if (can_destroy) {
/* Schedule destroy */
pj_time_val delay = {0, 0};
set_state(sess, PJ_TURN_STATE_DESTROYING);
pj_timer_heap_cancel_if_active(sess->timer_heap, &sess->timer,
TIMER_NONE);
pj_timer_heap_schedule_w_grp_lock(sess->timer_heap, &sess->timer,
&delay, TIMER_DESTROY,
sess->grp_lock);
}
}
/*
* Public API to destroy TURN client session.
*/
PJ_DEF(pj_status_t) pj_turn_session_shutdown(pj_turn_session *sess)
{
PJ_ASSERT_RETURN(sess, PJ_EINVAL);
pj_grp_lock_acquire(sess->grp_lock);
sess_shutdown(sess, PJ_SUCCESS);
pj_grp_lock_release(sess->grp_lock);
return PJ_SUCCESS;
}
/**
* Forcefully destroy the TURN session.
*/
PJ_DEF(pj_status_t) pj_turn_session_destroy( pj_turn_session *sess,
pj_status_t last_err)
{
PJ_ASSERT_RETURN(sess, PJ_EINVAL);
if (last_err != PJ_SUCCESS && sess->last_status == PJ_SUCCESS)
sess->last_status = last_err;
set_state(sess, PJ_TURN_STATE_DEALLOCATED);
sess_shutdown(sess, PJ_SUCCESS);
return PJ_SUCCESS;
}
/*
* Get TURN session info.
*/
PJ_DEF(pj_status_t) pj_turn_session_get_info( pj_turn_session *sess,
pj_turn_session_info *info)
{
pj_time_val now;
PJ_ASSERT_RETURN(sess && info, PJ_EINVAL);
pj_gettimeofday(&now);
info->state = sess->state;
info->conn_type = sess->conn_type;
info->lifetime = sess->expiry.sec - now.sec;
info->last_status = sess->last_status;
if (sess->srv_addr)
pj_memcpy(&info->server, sess->srv_addr, sizeof(info->server));
else
pj_bzero(&info->server, sizeof(info->server));
pj_memcpy(&info->mapped_addr, &sess->mapped_addr,
sizeof(sess->mapped_addr));
pj_memcpy(&info->relay_addr, &sess->relay_addr,
sizeof(sess->relay_addr));
return PJ_SUCCESS;
}
/*
* Re-assign user data.
*/
PJ_DEF(pj_status_t) pj_turn_session_set_user_data( pj_turn_session *sess,
void *user_data)
{
sess->user_data = user_data;
return PJ_SUCCESS;
}
/**
* Retrieve user data.
*/
PJ_DEF(void*) pj_turn_session_get_user_data(pj_turn_session *sess)
{
return sess->user_data;
}
/**
* Get group lock.
*/
PJ_DEF(pj_grp_lock_t *) pj_turn_session_get_grp_lock(pj_turn_session *sess)
{
PJ_ASSERT_RETURN(sess, NULL);
return sess->grp_lock;
}
/*
* Configure message logging. By default all flags are enabled.
*
* @param sess The TURN client session.
* @param flags Bitmask combination of #pj_stun_sess_msg_log_flag
*/
PJ_DEF(void) pj_turn_session_set_log( pj_turn_session *sess,
unsigned flags)
{
pj_stun_session_set_log(sess->stun, flags);
}
/*
* Set software name
*/
PJ_DEF(pj_status_t) pj_turn_session_set_software_name( pj_turn_session *sess,
const pj_str_t *sw)
{
pj_status_t status;
pj_grp_lock_acquire(sess->grp_lock);
status = pj_stun_session_set_software_name(sess->stun, sw);
pj_grp_lock_release(sess->grp_lock);
return status;
}
/**
* Set the server or domain name of the server.
*/
PJ_DEF(pj_status_t) pj_turn_session_set_server( pj_turn_session *sess,
const pj_str_t *domain,
int default_port,
pj_dns_resolver *resolver)
{
pj_sockaddr tmp_addr;
pj_bool_t is_ip_addr;
pj_status_t status;
PJ_ASSERT_RETURN(sess && domain, PJ_EINVAL);
PJ_ASSERT_RETURN(sess->state == PJ_TURN_STATE_NULL, PJ_EINVALIDOP);
pj_grp_lock_acquire(sess->grp_lock);
/* See if "domain" contains just IP address */
tmp_addr.addr.sa_family = sess->af;
status = pj_inet_pton(sess->af, domain,
pj_sockaddr_get_addr(&tmp_addr));
is_ip_addr = (status == PJ_SUCCESS);
if (!is_ip_addr && resolver) {
/* Resolve with DNS SRV resolution, and fallback to DNS A resolution
* if default_port is specified.
*/
unsigned opt = 0;
pj_str_t res_name;
switch (sess->conn_type) {
case PJ_TURN_TP_UDP:
res_name = pj_str("_turn._udp.");
break;
case PJ_TURN_TP_TCP:
res_name = pj_str("_turn._tcp.");
break;
case PJ_TURN_TP_TLS:
res_name = pj_str("_turns._tcp.");
break;
default:
status = PJNATH_ETURNINTP;
goto on_return;
}
/* Fallback to DNS A only if default port is specified */
if (default_port>0 && default_port<65536) {
opt = PJ_DNS_SRV_FALLBACK_A;
sess->default_port = (pj_uint16_t)default_port;
}
PJ_LOG(5,(sess->obj_name, "Resolving %.*s%.*s with DNS SRV",
(int)res_name.slen, res_name.ptr,
(int)domain->slen, domain->ptr));
set_state(sess, PJ_TURN_STATE_RESOLVING);
/* User may have destroyed us in the callback */
if (sess->state != PJ_TURN_STATE_RESOLVING) {
status = PJ_ECANCELLED;
goto on_return;
}
status = pj_dns_srv_resolve(domain, &res_name, default_port,
sess->pool, resolver, opt, sess,
&dns_srv_resolver_cb, NULL);
if (status != PJ_SUCCESS) {
set_state(sess, PJ_TURN_STATE_NULL);
goto on_return;
}
} else {
/* Resolver is not specified, resolve with standard gethostbyname().
* The default_port MUST be specified in this case.
*/
pj_addrinfo *ai;
unsigned i, cnt;
/* Default port must be specified */
PJ_ASSERT_RETURN(default_port>0 && default_port<65536, PJ_EINVAL);
sess->default_port = (pj_uint16_t)default_port;
cnt = PJ_TURN_MAX_DNS_SRV_CNT;
ai = (pj_addrinfo*)
pj_pool_calloc(sess->pool, cnt, sizeof(pj_addrinfo));
PJ_LOG(5,(sess->obj_name, "Resolving %.*s with DNS A",
(int)domain->slen, domain->ptr));
set_state(sess, PJ_TURN_STATE_RESOLVING);
/* User may have destroyed us in the callback */
if (sess->state != PJ_TURN_STATE_RESOLVING) {
status = PJ_ECANCELLED;
goto on_return;
}
status = pj_getaddrinfo(sess->af, domain, &cnt, ai);
if (status != PJ_SUCCESS)
goto on_return;
sess->srv_addr_cnt = (pj_uint16_t)cnt;
sess->srv_addr_list = (pj_sockaddr*)
pj_pool_calloc(sess->pool, cnt,
sizeof(pj_sockaddr));
for (i=0; i<cnt; ++i) {
pj_sockaddr *addr = &sess->srv_addr_list[i];
pj_memcpy(addr, &ai[i].ai_addr, sizeof(pj_sockaddr));
addr->addr.sa_family = sess->af;
addr->ipv4.sin_port = pj_htons(sess->default_port);
}
sess->srv_addr = &sess->srv_addr_list[0];
set_state(sess, PJ_TURN_STATE_RESOLVED);
}
on_return:
pj_grp_lock_release(sess->grp_lock);
return status;
}
/**
* Set credential to be used by the session.
*/
PJ_DEF(pj_status_t) pj_turn_session_set_credential(pj_turn_session *sess,
const pj_stun_auth_cred *cred)
{
PJ_ASSERT_RETURN(sess && cred, PJ_EINVAL);
PJ_ASSERT_RETURN(sess->stun, PJ_EINVALIDOP);
pj_grp_lock_acquire(sess->grp_lock);
pj_stun_session_set_credential(sess->stun, PJ_STUN_AUTH_LONG_TERM, cred);
pj_grp_lock_release(sess->grp_lock);
return PJ_SUCCESS;
}
/**
* Create TURN allocation.
*/
PJ_DEF(pj_status_t) pj_turn_session_alloc(pj_turn_session *sess,
const pj_turn_alloc_param *param)
{
pj_stun_tx_data *tdata;
pj_bool_t retransmit;
pj_status_t status;
PJ_ASSERT_RETURN(sess, PJ_EINVAL);
PJ_ASSERT_RETURN(sess->state>PJ_TURN_STATE_NULL &&
sess->state<=PJ_TURN_STATE_RESOLVED,
PJ_EINVALIDOP);
pj_grp_lock_acquire(sess->grp_lock);
if (param && param != &sess->alloc_param)
pj_turn_alloc_param_copy(sess->pool, &sess->alloc_param, param);
if (sess->state < PJ_TURN_STATE_RESOLVED) {
sess->pending_alloc = PJ_TRUE;
PJ_LOG(4,(sess->obj_name, "Pending ALLOCATE in state %s",
state_names[sess->state]));
pj_grp_lock_release(sess->grp_lock);
return PJ_SUCCESS;
}
/* Ready to allocate */
pj_assert(sess->state == PJ_TURN_STATE_RESOLVED);
/* Create a bare request */
status = pj_stun_session_create_req(sess->stun, PJ_STUN_ALLOCATE_REQUEST,
PJ_STUN_MAGIC, NULL, &tdata);
if (status != PJ_SUCCESS) {
pj_grp_lock_release(sess->grp_lock);
return status;
}
/* MUST include REQUESTED-TRANSPORT attribute */
pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_REQ_TRANSPORT,
PJ_STUN_SET_RT_PROTO(PJ_TURN_TP_UDP));
/* Include BANDWIDTH if requested */
if (sess->alloc_param.bandwidth > 0) {
pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_BANDWIDTH,
sess->alloc_param.bandwidth);
}
/* Include LIFETIME if requested */
if (sess->alloc_param.lifetime > 0) {
pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_LIFETIME,
sess->alloc_param.lifetime);
}
/* Server address must be set */
pj_assert(sess->srv_addr != NULL);
/* Send request */
set_state(sess, PJ_TURN_STATE_ALLOCATING);
retransmit = (sess->conn_type == PJ_TURN_TP_UDP);
status = pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE,
retransmit, sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
if (status != PJ_SUCCESS) {
/* Set state back to RESOLVED. We don't want to destroy session now,
* let the application do it if it wants to.
*/
set_state(sess, PJ_TURN_STATE_RESOLVED);
}
pj_grp_lock_release(sess->grp_lock);
return status;
}
/*
* Install or renew permissions
*/
PJ_DEF(pj_status_t) pj_turn_session_set_perm( pj_turn_session *sess,
unsigned addr_cnt,
const pj_sockaddr addr[],
unsigned options)
{
pj_stun_tx_data *tdata;
pj_hash_iterator_t it_buf, *it;
void *req_token;
unsigned i, attr_added=0;
pj_status_t status;
PJ_ASSERT_RETURN(sess && addr_cnt && addr, PJ_EINVAL);
pj_grp_lock_acquire(sess->grp_lock);
/* Create a bare CreatePermission request */
status = pj_stun_session_create_req(sess->stun,
PJ_STUN_CREATE_PERM_REQUEST,
PJ_STUN_MAGIC, NULL, &tdata);
if (status != PJ_SUCCESS) {
pj_grp_lock_release(sess->grp_lock);
return status;
}
/* Create request token to map the request to the perm structures
* which the request belongs.
*/
req_token = (void*)(pj_ssize_t)pj_rand();
/* Process the addresses */
for (i=0; i<addr_cnt; ++i) {
struct perm_t *perm;
/* Lookup the perm structure and create if it doesn't exist */
perm = lookup_perm(sess, &addr[i], pj_sockaddr_get_len(&addr[i]),
PJ_TRUE);
perm->renew = (options & 0x01);
/* Only add to the request if the request doesn't contain this
* address yet.
*/
if (perm->req_token != req_token) {
perm->req_token = req_token;
/* Add XOR-PEER-ADDRESS */
status = pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_XOR_PEER_ADDR,
PJ_TRUE,
&addr[i],
sizeof(addr[i]));
if (status != PJ_SUCCESS)
goto on_error;
++attr_added;
}
}
pj_assert(attr_added != 0);
/* Send the request */
status = pj_stun_session_send_msg(sess->stun, req_token, PJ_FALSE,
(sess->conn_type==PJ_TURN_TP_UDP),
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
if (status != PJ_SUCCESS) {
/* tdata is already destroyed */
tdata = NULL;
goto on_error;
}
pj_grp_lock_release(sess->grp_lock);
return PJ_SUCCESS;
on_error:
/* destroy tdata */
if (tdata) {
pj_stun_msg_destroy_tdata(sess->stun, tdata);
}
/* invalidate perm structures associated with this request */
it = pj_hash_first(sess->perm_table, &it_buf);
while (it) {
struct perm_t *perm = (struct perm_t*)
pj_hash_this(sess->perm_table, it);
it = pj_hash_next(sess->perm_table, it);
if (perm->req_token == req_token)
invalidate_perm(sess, perm);
}
pj_grp_lock_release(sess->grp_lock);
return status;
}
/*
* Send REFRESH
*/
static void send_refresh(pj_turn_session *sess, int lifetime)
{
pj_stun_tx_data *tdata;
pj_status_t status;
PJ_ASSERT_ON_FAIL(sess->state==PJ_TURN_STATE_READY, return);
/* Create a bare REFRESH request */
status = pj_stun_session_create_req(sess->stun, PJ_STUN_REFRESH_REQUEST,
PJ_STUN_MAGIC, NULL, &tdata);
if (status != PJ_SUCCESS)
goto on_error;
/* Add LIFETIME */
if (lifetime >= 0) {
pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_LIFETIME, lifetime);
}
/* Send request */
if (lifetime == 0) {
set_state(sess, PJ_TURN_STATE_DEALLOCATING);
}
status = pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE,
(sess->conn_type==PJ_TURN_TP_UDP),
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
if (status != PJ_SUCCESS)
goto on_error;
return;
on_error:
if (lifetime == 0) {
set_state(sess, PJ_TURN_STATE_DEALLOCATED);
sess_shutdown(sess, status);
}
}
/**
* Relay data to the specified peer through the session.
*/
PJ_DEF(pj_status_t) pj_turn_session_sendto( pj_turn_session *sess,
const pj_uint8_t *pkt,
unsigned pkt_len,
const pj_sockaddr_t *addr,
unsigned addr_len)
{
struct ch_t *ch;
struct perm_t *perm;
pj_status_t status;
PJ_ASSERT_RETURN(sess && pkt && pkt_len && addr && addr_len,
PJ_EINVAL);
/* Return error if we're not ready */
if (sess->state != PJ_TURN_STATE_READY) {
return PJ_EIGNORED;
}
/* Lock session now */
pj_grp_lock_acquire(sess->grp_lock);
/* Lookup permission first */
perm = lookup_perm(sess, addr, pj_sockaddr_get_len(addr), PJ_FALSE);
if (perm == NULL) {
/* Permission doesn't exist, install it first */
char ipstr[PJ_INET6_ADDRSTRLEN+2];
PJ_LOG(4,(sess->obj_name,
"sendto(): IP %s has no permission, requesting it first..",
pj_sockaddr_print(addr, ipstr, sizeof(ipstr), 2)));
status = pj_turn_session_set_perm(sess, 1, (const pj_sockaddr*)addr,
0);
if (status != PJ_SUCCESS) {
pj_grp_lock_release(sess->grp_lock);
return status;
}
}
/* See if the peer is bound to a channel number */
ch = lookup_ch_by_addr(sess, addr, pj_sockaddr_get_len(addr),
PJ_FALSE, PJ_FALSE);
if (ch && ch->num != PJ_TURN_INVALID_CHANNEL && ch->bound) {
unsigned total_len;
/* Peer is assigned a channel number, we can use ChannelData */
pj_turn_channel_data *cd = (pj_turn_channel_data*)sess->tx_pkt;
pj_assert(sizeof(*cd)==4);
/* Calculate total length, including paddings */
total_len = (pkt_len + sizeof(*cd) + 3) & (~3);
if (total_len > sizeof(sess->tx_pkt)) {
status = PJ_ETOOBIG;
goto on_return;
}
cd->ch_number = pj_htons((pj_uint16_t)ch->num);
cd->length = pj_htons((pj_uint16_t)pkt_len);
pj_memcpy(cd+1, pkt, pkt_len);
pj_assert(sess->srv_addr != NULL);
status = sess->cb.on_send_pkt(sess, sess->tx_pkt, total_len,
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr));
} else {
/* Use Send Indication. */
pj_stun_sockaddr_attr peer_attr;
pj_stun_binary_attr data_attr;
pj_stun_msg send_ind;
pj_size_t send_ind_len;
/* Increment counter */
++sess->send_ind_tsx_id[2];
/* Create blank SEND-INDICATION */
status = pj_stun_msg_init(&send_ind, PJ_STUN_SEND_INDICATION,
PJ_STUN_MAGIC,
(const pj_uint8_t*)sess->send_ind_tsx_id);
if (status != PJ_SUCCESS)
goto on_return;
/* Add XOR-PEER-ADDRESS */
pj_stun_sockaddr_attr_init(&peer_attr, PJ_STUN_ATTR_XOR_PEER_ADDR,
PJ_TRUE, addr, addr_len);
pj_stun_msg_add_attr(&send_ind, (pj_stun_attr_hdr*)&peer_attr);
/* Add DATA attribute */
pj_stun_binary_attr_init(&data_attr, NULL, PJ_STUN_ATTR_DATA, NULL, 0);
data_attr.data = (pj_uint8_t*)pkt;
data_attr.length = pkt_len;
pj_stun_msg_add_attr(&send_ind, (pj_stun_attr_hdr*)&data_attr);
/* Encode the message */
status = pj_stun_msg_encode(&send_ind, sess->tx_pkt,
sizeof(sess->tx_pkt), 0,
NULL, &send_ind_len);
if (status != PJ_SUCCESS)
goto on_return;
/* Send the Send Indication */
status = sess->cb.on_send_pkt(sess, sess->tx_pkt,
(unsigned)send_ind_len,
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr));
}
on_return:
pj_grp_lock_release(sess->grp_lock);
return status;
}
/**
* Bind a peer address to a channel number.
*/
PJ_DEF(pj_status_t) pj_turn_session_bind_channel(pj_turn_session *sess,
const pj_sockaddr_t *peer_adr,
unsigned addr_len)
{
struct ch_t *ch;
pj_stun_tx_data *tdata;
pj_uint16_t ch_num;
pj_status_t status;
PJ_ASSERT_RETURN(sess && peer_adr && addr_len, PJ_EINVAL);
PJ_ASSERT_RETURN(sess->state == PJ_TURN_STATE_READY, PJ_EINVALIDOP);
pj_grp_lock_acquire(sess->grp_lock);
/* Create blank ChannelBind request */
status = pj_stun_session_create_req(sess->stun,
PJ_STUN_CHANNEL_BIND_REQUEST,
PJ_STUN_MAGIC, NULL, &tdata);
if (status != PJ_SUCCESS)
goto on_return;
/* Lookup if this peer has already been assigned a number */
ch = lookup_ch_by_addr(sess, peer_adr, pj_sockaddr_get_len(peer_adr),
PJ_TRUE, PJ_FALSE);
pj_assert(ch);
if (ch->num != PJ_TURN_INVALID_CHANNEL) {
/* Channel is already bound. This is a refresh request. */
ch_num = ch->num;
} else {
PJ_ASSERT_ON_FAIL(sess->next_ch <= PJ_TURN_CHANNEL_MAX,
{status=PJ_ETOOMANY; goto on_return;});
ch->num = ch_num = sess->next_ch++;
}
/* Add CHANNEL-NUMBER attribute */
pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_CHANNEL_NUMBER,
PJ_STUN_SET_CH_NB(ch_num));
/* Add XOR-PEER-ADDRESS attribute */
pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_XOR_PEER_ADDR, PJ_TRUE,
peer_adr, addr_len);
/* Send the request, associate peer data structure with tdata
* for future reference when we receive the ChannelBind response.
*/
status = pj_stun_session_send_msg(sess->stun, ch, PJ_FALSE,
(sess->conn_type==PJ_TURN_TP_UDP),
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
on_return:
pj_grp_lock_release(sess->grp_lock);
return status;
}
/**
* Notify TURN client session upon receiving a packet from server.
* The packet maybe a STUN packet or ChannelData packet.
*/
PJ_DEF(pj_status_t) pj_turn_session_on_rx_pkt(pj_turn_session *sess,
void *pkt,
pj_size_t pkt_len,
pj_size_t *parsed_len)
{
pj_bool_t is_stun;
pj_status_t status;
pj_bool_t is_datagram;
/* Packet could be ChannelData or STUN message (response or
* indication).
*/
/* Start locking the session */
pj_grp_lock_acquire(sess->grp_lock);
is_datagram = (sess->conn_type==PJ_TURN_TP_UDP);
/* Quickly check if this is STUN message */
is_stun = ((((pj_uint8_t*)pkt)[0] & 0xC0) == 0);
if (is_stun) {
/* This looks like STUN, give it to the STUN session */
unsigned options;
options = PJ_STUN_CHECK_PACKET | PJ_STUN_NO_FINGERPRINT_CHECK;
if (is_datagram)
options |= PJ_STUN_IS_DATAGRAM;
status=pj_stun_session_on_rx_pkt(sess->stun, pkt, pkt_len,
options, NULL, parsed_len,
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr));
} else {
/* This must be ChannelData. */
pj_turn_channel_data cd;
struct ch_t *ch;
if (pkt_len < 4) {
if (parsed_len) *parsed_len = 0;
return PJ_ETOOSMALL;
}
/* Decode ChannelData packet */
pj_memcpy(&cd, pkt, sizeof(pj_turn_channel_data));
cd.ch_number = pj_ntohs(cd.ch_number);
cd.length = pj_ntohs(cd.length);
/* Check that size is sane */
if (pkt_len < cd.length+sizeof(cd)) {
if (parsed_len) {
if (is_datagram) {
/* Discard the datagram */
*parsed_len = pkt_len;
} else {
/* Insufficient fragment */
*parsed_len = 0;
}
}
status = PJ_ETOOSMALL;
goto on_return;
} else {
if (parsed_len) {
/* Apply padding too */
*parsed_len = ((cd.length + 3) & (~3)) + sizeof(cd);
}
}
/* Lookup channel */
ch = lookup_ch_by_chnum(sess, cd.ch_number);
if (!ch || !ch->bound) {
status = PJ_ENOTFOUND;
goto on_return;
}
/* Notify application */
if (sess->cb.on_rx_data) {
(*sess->cb.on_rx_data)(sess, ((pj_uint8_t*)pkt)+sizeof(cd),
cd.length, &ch->addr,
pj_sockaddr_get_len(&ch->addr));
}
status = PJ_SUCCESS;
}
on_return:
pj_grp_lock_release(sess->grp_lock);
return status;
}
/*
* This is a callback from STUN session to send outgoing packet.
*/
static pj_status_t stun_on_send_msg(pj_stun_session *stun,
void *token,
const void *pkt,
pj_size_t pkt_size,
const pj_sockaddr_t *dst_addr,
unsigned addr_len)
{
pj_turn_session *sess;
PJ_UNUSED_ARG(token);
sess = (pj_turn_session*) pj_stun_session_get_user_data(stun);
return (*sess->cb.on_send_pkt)(sess, (const pj_uint8_t*)pkt,
(unsigned)pkt_size,
dst_addr, addr_len);
}
/*
* Handle failed ALLOCATE or REFRESH request. This may switch to alternate
* server if we have one.
*/
static void on_session_fail( pj_turn_session *sess,
enum pj_stun_method_e method,
pj_status_t status,
const pj_str_t *reason)
{
sess->last_status = status;
do {
pj_str_t reason1;
char err_msg[PJ_ERR_MSG_SIZE];
if (reason == NULL) {
pj_strerror(status, err_msg, sizeof(err_msg));
reason1 = pj_str(err_msg);
reason = &reason1;
}
PJ_LOG(4,(sess->obj_name, "%s error: %.*s",
pj_stun_get_method_name(method),
(int)reason->slen, reason->ptr));
/* If this is ALLOCATE response and we don't have more server
* addresses to try, notify application and destroy the TURN
* session.
*/
if (method==PJ_STUN_ALLOCATE_METHOD &&
sess->srv_addr == &sess->srv_addr_list[sess->srv_addr_cnt-1])
{
set_state(sess, PJ_TURN_STATE_DEALLOCATED);
sess_shutdown(sess, status);
return;
}
/* Otherwise if this is not ALLOCATE response, notify application
* that session has been TERMINATED.
*/
if (method!=PJ_STUN_ALLOCATE_METHOD) {
set_state(sess, PJ_TURN_STATE_DEALLOCATED);
sess_shutdown(sess, status);
return;
}
/* Try next server */
++sess->srv_addr;
reason = NULL;
PJ_LOG(4,(sess->obj_name, "Trying next server"));
set_state(sess, PJ_TURN_STATE_RESOLVED);
} while (0);
}
/*
* Handle successful response to ALLOCATE or REFRESH request.
*/
static void on_allocate_success(pj_turn_session *sess,
enum pj_stun_method_e method,
const pj_stun_msg *msg)
{
const pj_stun_lifetime_attr *lf_attr;
const pj_stun_xor_relayed_addr_attr *raddr_attr;
const pj_stun_sockaddr_attr *mapped_attr;
pj_str_t s;
pj_time_val timeout;
/* Must have LIFETIME attribute */
lf_attr = (const pj_stun_lifetime_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_LIFETIME, 0);
if (lf_attr == NULL) {
on_session_fail(sess, method, PJNATH_EINSTUNMSG,
pj_cstr(&s, "Error: Missing LIFETIME attribute"));
return;
}
/* If LIFETIME is zero, this is a deallocation */
if (lf_attr->value == 0) {
set_state(sess, PJ_TURN_STATE_DEALLOCATED);
sess_shutdown(sess, PJ_SUCCESS);
return;
}
/* Update lifetime and keep-alive interval */
sess->lifetime = lf_attr->value;
pj_gettimeofday(&sess->expiry);
if (sess->lifetime < PJ_TURN_KEEP_ALIVE_SEC) {
if (sess->lifetime <= 2) {
on_session_fail(sess, method, PJ_ETOOSMALL,
pj_cstr(&s, "Error: LIFETIME too small"));
return;
}
sess->ka_interval = sess->lifetime - 2;
sess->expiry.sec += (sess->ka_interval-1);
} else {
int timeout;
sess->ka_interval = PJ_TURN_KEEP_ALIVE_SEC;
timeout = sess->lifetime - PJ_TURN_REFRESH_SEC_BEFORE;
if (timeout < sess->ka_interval)
timeout = sess->ka_interval - 1;
sess->expiry.sec += timeout;
}
/* Check that relayed transport address contains correct
* address family.
*/
raddr_attr = (const pj_stun_xor_relayed_addr_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_RELAYED_ADDR, 0);
if (raddr_attr == NULL && method==PJ_STUN_ALLOCATE_METHOD) {
on_session_fail(sess, method, PJNATH_EINSTUNMSG,
pj_cstr(&s, "Error: Received ALLOCATE without "
"RELAY-ADDRESS attribute"));
return;
}
if (raddr_attr && raddr_attr->sockaddr.addr.sa_family != sess->af) {
on_session_fail(sess, method, PJNATH_EINSTUNMSG,
pj_cstr(&s, "Error: RELAY-ADDRESS with non IPv4"
" address family is not supported "
"for now"));
return;
}
if (raddr_attr && !pj_sockaddr_has_addr(&raddr_attr->sockaddr)) {
on_session_fail(sess, method, PJNATH_EINSTUNMSG,
pj_cstr(&s, "Error: Invalid IP address in "
"RELAY-ADDRESS attribute"));
return;
}
/* Save relayed address */
if (raddr_attr) {
/* If we already have relay address, check if the relay address
* in the response matches our relay address.
*/
if (pj_sockaddr_has_addr(&sess->relay_addr)) {
if (pj_sockaddr_cmp(&sess->relay_addr, &raddr_attr->sockaddr)) {
on_session_fail(sess, method, PJNATH_EINSTUNMSG,
pj_cstr(&s, "Error: different RELAY-ADDRESS is"
"returned by server"));
return;
}
} else {
/* Otherwise save the relayed address */
pj_memcpy(&sess->relay_addr, &raddr_attr->sockaddr,
sizeof(pj_sockaddr));
}
}
/* Get mapped address */
mapped_attr = (const pj_stun_sockaddr_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_MAPPED_ADDR, 0);
if (mapped_attr) {
pj_memcpy(&sess->mapped_addr, &mapped_attr->sockaddr,
sizeof(mapped_attr->sockaddr));
}
/* Success */
/* Cancel existing keep-alive timer, if any */
pj_assert(sess->timer.id != TIMER_DESTROY);
if (sess->timer.id == TIMER_KEEP_ALIVE) {
pj_timer_heap_cancel_if_active(sess->timer_heap, &sess->timer,
TIMER_NONE);
}
/* Start keep-alive timer once allocation succeeds */
if (sess->state < PJ_TURN_STATE_DEALLOCATING) {
timeout.sec = sess->ka_interval;
timeout.msec = 0;
pj_timer_heap_schedule_w_grp_lock(sess->timer_heap, &sess->timer,
&timeout, TIMER_KEEP_ALIVE,
sess->grp_lock);
set_state(sess, PJ_TURN_STATE_READY);
}
}
/*
* Notification from STUN session on request completion.
*/
static void stun_on_request_complete(pj_stun_session *stun,
pj_status_t status,
void *token,
pj_stun_tx_data *tdata,
const pj_stun_msg *response,
const pj_sockaddr_t *src_addr,
unsigned src_addr_len)
{
pj_turn_session *sess;
enum pj_stun_method_e method = (enum pj_stun_method_e)
PJ_STUN_GET_METHOD(tdata->msg->hdr.type);
PJ_UNUSED_ARG(src_addr);
PJ_UNUSED_ARG(src_addr_len);
sess = (pj_turn_session*)pj_stun_session_get_user_data(stun);
if (method == PJ_STUN_ALLOCATE_METHOD) {
/* Destroy if we have pending destroy request */
if (sess->pending_destroy) {
if (status == PJ_SUCCESS)
sess->state = PJ_TURN_STATE_READY;
else
sess->state = PJ_TURN_STATE_DEALLOCATED;
sess_shutdown(sess, PJ_SUCCESS);
return;
}
/* Handle ALLOCATE response */
if (status==PJ_SUCCESS &&
PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type))
{
/* Successful Allocate response */
on_allocate_success(sess, method, response);
} else {
/* Failed Allocate request */
const pj_str_t *err_msg = NULL;
if (status == PJ_SUCCESS) {
const pj_stun_errcode_attr *err_attr;
err_attr = (const pj_stun_errcode_attr*)
pj_stun_msg_find_attr(response,
PJ_STUN_ATTR_ERROR_CODE, 0);
if (err_attr) {
status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code);
err_msg = &err_attr->reason;
} else {
status = PJNATH_EINSTUNMSG;
}
}
on_session_fail(sess, method, status, err_msg);
}
} else if (method == PJ_STUN_REFRESH_METHOD) {
/* Handle Refresh response */
if (status==PJ_SUCCESS &&
PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type))
{
/* Success, schedule next refresh. */
on_allocate_success(sess, method, response);
} else {
/* Failed Refresh request */
const pj_str_t *err_msg = NULL;
pj_assert(status != PJ_SUCCESS);
if (response) {
const pj_stun_errcode_attr *err_attr;
err_attr = (const pj_stun_errcode_attr*)
pj_stun_msg_find_attr(response,
PJ_STUN_ATTR_ERROR_CODE, 0);
if (err_attr) {
status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code);
err_msg = &err_attr->reason;
}
}
/* Notify and destroy */
on_session_fail(sess, method, status, err_msg);
}
} else if (method == PJ_STUN_CHANNEL_BIND_METHOD) {
/* Handle ChannelBind response */
if (status==PJ_SUCCESS &&
PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type))
{
/* Successful ChannelBind response */
struct ch_t *ch = (struct ch_t*)token;
pj_assert(ch->num != PJ_TURN_INVALID_CHANNEL);
ch->bound = PJ_TRUE;
/* Update hash table */
lookup_ch_by_addr(sess, &ch->addr,
pj_sockaddr_get_len(&ch->addr),
PJ_TRUE, PJ_TRUE);
} else {
/* Failed ChannelBind response */
pj_str_t reason = {"", 0};
int err_code = 0;
char errbuf[PJ_ERR_MSG_SIZE];
pj_assert(status != PJ_SUCCESS);
if (response) {
const pj_stun_errcode_attr *err_attr;
err_attr = (const pj_stun_errcode_attr*)
pj_stun_msg_find_attr(response,
PJ_STUN_ATTR_ERROR_CODE, 0);
if (err_attr) {
err_code = err_attr->err_code;
status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code);
reason = err_attr->reason;
}
} else {
err_code = status;
reason = pj_strerror(status, errbuf, sizeof(errbuf));
}
PJ_LOG(1,(sess->obj_name, "ChannelBind failed: %d/%.*s",
err_code, (int)reason.slen, reason.ptr));
if (err_code == PJ_STUN_SC_ALLOCATION_MISMATCH) {
/* Allocation mismatch means allocation no longer exists */
on_session_fail(sess, PJ_STUN_CHANNEL_BIND_METHOD,
status, &reason);
return;
}
}
} else if (method == PJ_STUN_CREATE_PERM_METHOD) {
/* Handle CreatePermission response */
if (status==PJ_SUCCESS &&
PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type))
{
/* No special handling when the request is successful. */
} else {
/* Iterate the permission table and invalidate all permissions
* that are related to this request.
*/
pj_hash_iterator_t it_buf, *it;
char ipstr[PJ_INET6_ADDRSTRLEN+10];
int err_code;
char errbuf[PJ_ERR_MSG_SIZE];
pj_str_t reason;
pj_assert(status != PJ_SUCCESS);
if (response) {
const pj_stun_errcode_attr *eattr;
eattr = (const pj_stun_errcode_attr*)
pj_stun_msg_find_attr(response,
PJ_STUN_ATTR_ERROR_CODE, 0);
if (eattr) {
err_code = eattr->err_code;
reason = eattr->reason;
} else {
err_code = -1;
reason = pj_str("?");
}
} else {
err_code = status;
reason = pj_strerror(status, errbuf, sizeof(errbuf));
}
it = pj_hash_first(sess->perm_table, &it_buf);
while (it) {
struct perm_t *perm = (struct perm_t*)
pj_hash_this(sess->perm_table, it);
it = pj_hash_next(sess->perm_table, it);
if (perm->req_token == token) {
PJ_LOG(1,(sess->obj_name,
"CreatePermission failed for IP %s: %d/%.*s",
pj_sockaddr_print(&perm->addr, ipstr,
sizeof(ipstr), 2),
err_code, (int)reason.slen, reason.ptr));
invalidate_perm(sess, perm);
}
}
if (err_code == PJ_STUN_SC_ALLOCATION_MISMATCH) {
/* Allocation mismatch means allocation no longer exists */
on_session_fail(sess, PJ_STUN_CREATE_PERM_METHOD,
status, &reason);
return;
}
}
} else {
PJ_LOG(4,(sess->obj_name, "Unexpected STUN %s response",
pj_stun_get_method_name(response->hdr.type)));
}
}
/*
* Notification from STUN session on incoming STUN Indication
* message.
*/
static pj_status_t stun_on_rx_indication(pj_stun_session *stun,
const pj_uint8_t *pkt,
unsigned pkt_len,
const pj_stun_msg *msg,
void *token,
const pj_sockaddr_t *src_addr,
unsigned src_addr_len)
{
pj_turn_session *sess;
pj_stun_xor_peer_addr_attr *peer_attr;
pj_stun_icmp_attr *icmp;
pj_stun_data_attr *data_attr;
PJ_UNUSED_ARG(token);
PJ_UNUSED_ARG(pkt);
PJ_UNUSED_ARG(pkt_len);
PJ_UNUSED_ARG(src_addr);
PJ_UNUSED_ARG(src_addr_len);
sess = (pj_turn_session*)pj_stun_session_get_user_data(stun);
/* Expecting Data Indication only */
if (msg->hdr.type != PJ_STUN_DATA_INDICATION) {
PJ_LOG(4,(sess->obj_name, "Unexpected STUN %s indication",
pj_stun_get_method_name(msg->hdr.type)));
return PJ_EINVALIDOP;
}
/* Check if there is ICMP attribute in the message */
icmp = (pj_stun_icmp_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_ICMP, 0);
if (icmp != NULL) {
/* This is a forwarded ICMP packet. Ignore it for now */
return PJ_SUCCESS;
}
/* Get XOR-PEER-ADDRESS attribute */
peer_attr = (pj_stun_xor_peer_addr_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_PEER_ADDR, 0);
/* Get DATA attribute */
data_attr = (pj_stun_data_attr*)
pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_DATA, 0);
/* Must have both XOR-PEER-ADDRESS and DATA attributes */
if (!peer_attr || !data_attr) {
PJ_LOG(4,(sess->obj_name,
"Received Data indication with missing attributes"));
return PJ_EINVALIDOP;
}
/* Notify application */
if (sess->cb.on_rx_data) {
(*sess->cb.on_rx_data)(sess, data_attr->data, data_attr->length,
&peer_attr->sockaddr,
pj_sockaddr_get_len(&peer_attr->sockaddr));
}
return PJ_SUCCESS;
}
/*
* Notification on completion of DNS SRV resolution.
*/
static void dns_srv_resolver_cb(void *user_data,
pj_status_t status,
const pj_dns_srv_record *rec)
{
pj_turn_session *sess = (pj_turn_session*) user_data;
unsigned i, cnt, tot_cnt;
/* Check failure */
if (status != PJ_SUCCESS || sess->pending_destroy) {
set_state(sess, PJ_TURN_STATE_DESTROYING);
sess_shutdown(sess, status);
return;
}
/* Calculate total number of server entries in the response */
tot_cnt = 0;
for (i=0; i<rec->count; ++i) {
tot_cnt += rec->entry[i].server.addr_count;
}
if (tot_cnt > PJ_TURN_MAX_DNS_SRV_CNT)
tot_cnt = PJ_TURN_MAX_DNS_SRV_CNT;
/* Allocate server entries */
sess->srv_addr_list = (pj_sockaddr*)
pj_pool_calloc(sess->pool, tot_cnt,
sizeof(pj_sockaddr));
/* Copy results to server entries */
for (i=0, cnt=0; i<rec->count && cnt<PJ_TURN_MAX_DNS_SRV_CNT; ++i) {
unsigned j;
for (j=0; j<rec->entry[i].server.addr_count &&
cnt<PJ_TURN_MAX_DNS_SRV_CNT; ++j)
{
pj_sockaddr_in *addr = &sess->srv_addr_list[cnt].ipv4;
addr->sin_family = sess->af;
addr->sin_port = pj_htons(rec->entry[i].port);
addr->sin_addr.s_addr = rec->entry[i].server.addr[j].s_addr;
++cnt;
}
}
sess->srv_addr_cnt = (pj_uint16_t)cnt;
/* Set current server */
sess->srv_addr = &sess->srv_addr_list[0];
/* Set state to PJ_TURN_STATE_RESOLVED */
set_state(sess, PJ_TURN_STATE_RESOLVED);
/* Run pending allocation */
if (sess->pending_alloc) {
pj_turn_session_alloc(sess, NULL);
}
}
/*
* Lookup peer descriptor from its address.
*/
static struct ch_t *lookup_ch_by_addr(pj_turn_session *sess,
const pj_sockaddr_t *addr,
unsigned addr_len,
pj_bool_t update,
pj_bool_t bind_channel)
{
pj_uint32_t hval = 0;
struct ch_t *ch;
ch = (struct ch_t*)
pj_hash_get(sess->ch_table, addr, addr_len, &hval);
if (ch == NULL && update) {
ch = PJ_POOL_ZALLOC_T(sess->pool, struct ch_t);
ch->num = PJ_TURN_INVALID_CHANNEL;
pj_memcpy(&ch->addr, addr, addr_len);
/* Register by peer address */
pj_hash_set(sess->pool, sess->ch_table, &ch->addr, addr_len,
hval, ch);
}
if (ch && update) {
pj_gettimeofday(&ch->expiry);
ch->expiry.sec += PJ_TURN_PERM_TIMEOUT - sess->ka_interval - 1;
if (bind_channel) {
pj_uint32_t hval = 0;
/* Register by channel number */
pj_assert(ch->num != PJ_TURN_INVALID_CHANNEL && ch->bound);
if (pj_hash_get(sess->ch_table, &ch->num,
sizeof(ch->num), &hval)==0) {
pj_hash_set(sess->pool, sess->ch_table, &ch->num,
sizeof(ch->num), hval, ch);
}
}
}
/* Also create/update permission for this destination. Ideally we
* should update this when we receive the successful response,
* but that would cause duplicate CreatePermission to be sent
* during refreshing.
*/
if (ch && update) {
lookup_perm(sess, &ch->addr, pj_sockaddr_get_len(&ch->addr), PJ_TRUE);
}
return ch;
}
/*
* Lookup channel descriptor from its channel number.
*/
static struct ch_t *lookup_ch_by_chnum(pj_turn_session *sess,
pj_uint16_t chnum)
{
return (struct ch_t*) pj_hash_get(sess->ch_table, &chnum,
sizeof(chnum), NULL);
}
/*
* Lookup permission and optionally create if it doesn't exist.
*/
static struct perm_t *lookup_perm(pj_turn_session *sess,
const pj_sockaddr_t *addr,
unsigned addr_len,
pj_bool_t update)
{
pj_uint32_t hval = 0;
pj_sockaddr perm_addr;
struct perm_t *perm;
/* make sure port number if zero */
if (pj_sockaddr_get_port(addr) != 0) {
pj_memcpy(&perm_addr, addr, addr_len);
pj_sockaddr_set_port(&perm_addr, 0);
addr = &perm_addr;
}
/* lookup and create if it doesn't exist and wanted */
perm = (struct perm_t*)
pj_hash_get(sess->perm_table, addr, addr_len, &hval);
if (perm == NULL && update) {
perm = PJ_POOL_ZALLOC_T(sess->pool, struct perm_t);
pj_memcpy(&perm->addr, addr, addr_len);
perm->hval = hval;
pj_hash_set(sess->pool, sess->perm_table, &perm->addr, addr_len,
perm->hval, perm);
}
if (perm && update) {
pj_gettimeofday(&perm->expiry);
perm->expiry.sec += PJ_TURN_PERM_TIMEOUT - sess->ka_interval - 1;
}
return perm;
}
/*
* Delete permission
*/
static void invalidate_perm(pj_turn_session *sess,
struct perm_t *perm)
{
pj_hash_set(NULL, sess->perm_table, &perm->addr,
pj_sockaddr_get_len(&perm->addr), perm->hval, NULL);
}
/*
* Scan permission's hash table to refresh the permission.
*/
static unsigned refresh_permissions(pj_turn_session *sess,
const pj_time_val *now)
{
pj_stun_tx_data *tdata = NULL;
unsigned count = 0;
void *req_token = NULL;
pj_hash_iterator_t *it, itbuf;
pj_status_t status;
it = pj_hash_first(sess->perm_table, &itbuf);
while (it) {
struct perm_t *perm = (struct perm_t*)
pj_hash_this(sess->perm_table, it);
it = pj_hash_next(sess->perm_table, it);
if (perm->expiry.sec-1 <= now->sec) {
if (perm->renew) {
/* Renew this permission */
if (tdata == NULL) {
/* Create a bare CreatePermission request */
status = pj_stun_session_create_req(
sess->stun,
PJ_STUN_CREATE_PERM_REQUEST,
PJ_STUN_MAGIC, NULL, &tdata);
if (status != PJ_SUCCESS) {
PJ_LOG(1,(sess->obj_name,
"Error creating CreatePermission request: %d",
status));
return 0;
}
/* Create request token to map the request to the perm
* structures which the request belongs.
*/
req_token = (void*)(pj_ssize_t)pj_rand();
}
status = pj_stun_msg_add_sockaddr_attr(
tdata->pool,
tdata->msg,
PJ_STUN_ATTR_XOR_PEER_ADDR,
PJ_TRUE,
&perm->addr,
sizeof(perm->addr));
if (status != PJ_SUCCESS) {
pj_stun_msg_destroy_tdata(sess->stun, tdata);
return 0;
}
perm->expiry = *now;
perm->expiry.sec += PJ_TURN_PERM_TIMEOUT-sess->ka_interval-1;
perm->req_token = req_token;
++count;
} else {
/* This permission has expired and app doesn't want
* us to renew, so delete it from the hash table.
*/
invalidate_perm(sess, perm);
}
}
}
if (tdata) {
status = pj_stun_session_send_msg(sess->stun, req_token, PJ_FALSE,
(sess->conn_type==PJ_TURN_TP_UDP),
sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
if (status != PJ_SUCCESS) {
PJ_LOG(1,(sess->obj_name,
"Error sending CreatePermission request: %d",
status));
count = 0;
}
}
return count;
}
/*
* Timer event.
*/
static void on_timer_event(pj_timer_heap_t *th, pj_timer_entry *e)
{
pj_turn_session *sess = (pj_turn_session*)e->user_data;
enum timer_id_t eid;
PJ_UNUSED_ARG(th);
pj_grp_lock_acquire(sess->grp_lock);
eid = (enum timer_id_t) e->id;
e->id = TIMER_NONE;
if (eid == TIMER_KEEP_ALIVE) {
pj_time_val now;
pj_hash_iterator_t itbuf, *it;
pj_bool_t resched = PJ_TRUE;
pj_bool_t pkt_sent = PJ_FALSE;
if (sess->state >= PJ_TURN_STATE_DEALLOCATING) {
/* Ignore if we're deallocating */
goto on_return;
}
pj_gettimeofday(&now);
/* Refresh allocation if it's time to do so */
if (PJ_TIME_VAL_LTE(sess->expiry, now)) {
int lifetime = sess->alloc_param.lifetime;
if (lifetime == 0)
lifetime = -1;
send_refresh(sess, lifetime);
resched = PJ_FALSE;
pkt_sent = PJ_TRUE;
}
/* Scan hash table to refresh bound channels */
it = pj_hash_first(sess->ch_table, &itbuf);
while (it) {
struct ch_t *ch = (struct ch_t*)
pj_hash_this(sess->ch_table, it);
if (ch->bound && PJ_TIME_VAL_LTE(ch->expiry, now)) {
/* Send ChannelBind to refresh channel binding and
* permission.
*/
pj_turn_session_bind_channel(sess, &ch->addr,
pj_sockaddr_get_len(&ch->addr));
pkt_sent = PJ_TRUE;
}
it = pj_hash_next(sess->ch_table, it);
}
/* Scan permission table to refresh permissions */
if (refresh_permissions(sess, &now))
pkt_sent = PJ_TRUE;
/* If no packet is sent, send a blank Send indication to
* refresh local NAT.
*/
if (!pkt_sent && sess->alloc_param.ka_interval > 0) {
pj_stun_tx_data *tdata;
pj_status_t rc;
/* Create blank SEND-INDICATION */
rc = pj_stun_session_create_ind(sess->stun,
PJ_STUN_SEND_INDICATION, &tdata);
if (rc == PJ_SUCCESS) {
/* Add DATA attribute with zero length */
pj_stun_msg_add_binary_attr(tdata->pool, tdata->msg,
PJ_STUN_ATTR_DATA, NULL, 0);
/* Send the indication */
pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE,
PJ_FALSE, sess->srv_addr,
pj_sockaddr_get_len(sess->srv_addr),
tdata);
}
}
/* Reshcedule timer */
if (resched) {
pj_time_val delay;
delay.sec = sess->ka_interval;
delay.msec = 0;
pj_timer_heap_schedule_w_grp_lock(sess->timer_heap, &sess->timer,
&delay, TIMER_KEEP_ALIVE,
sess->grp_lock);
}
} else if (eid == TIMER_DESTROY) {
/* Time to destroy */
do_destroy(sess);
} else {
pj_assert(!"Unknown timer event");
}
on_return:
pj_grp_lock_release(sess->grp_lock);
}