/* $Id$ */ /* * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) * Copyright (C) 2003-2008 Benny Prijono * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #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; }; struct conn_bind_t { pj_uint32_t id; /* Connection ID. */ pj_sockaddr peer_addr; /* Peer address. */ unsigned peer_addr_len; }; /* 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)); prm->peer_conn_type = PJ_TURN_TP_UDP; } /* * 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_LOG(4,(sess->obj_name, "TURN client session destroyed")); pj_pool_safe_release(&sess->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; } /* Init DNS resolution option for IPv6 */ if (sess->af == pj_AF_INET6()) opt |= PJ_DNS_SRV_RESOLVE_AAAA_ONLY; /* Fallback to DNS A only if default port is specified */ if (default_port>0 && default_port<65536) { if (sess->af == pj_AF_INET6()) opt |= PJ_DNS_SRV_FALLBACK_AAAA; else 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; } /* Add reference before async DNS resolution */ pj_grp_lock_add_ref(sess->grp_lock); 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); pj_grp_lock_dec_ref(sess->grp_lock); 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; isrv_addr_list[i]; pj_memcpy(addr, &ai[i].ai_addr, sizeof(pj_sockaddr)); addr->addr.sa_family = sess->af; pj_sockaddr_set_port(addr, 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_ASSERT_RETURN(param->peer_conn_type == PJ_TURN_TP_UDP || param->peer_conn_type == PJ_TURN_TP_TCP, PJ_EINVAL); /* Verify address family in allocation param */ if (param && param->af) { PJ_ASSERT_RETURN(param->af==pj_AF_INET() || param->af==pj_AF_INET6(), PJ_EINVAL); } 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(param->peer_conn_type)); /* 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); } /* Include ADDRESS-FAMILY if requested */ if (sess->alloc_param.af || sess->af == pj_AF_INET6()) { enum { IPV4_AF_TYPE = 0x01 << 24, IPV6_AF_TYPE = 0x02 << 24 }; if (sess->alloc_param.af == pj_AF_INET6() || (sess->alloc_param.af == 0 && sess->af == pj_AF_INET6())) { pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, PJ_STUN_ATTR_REQ_ADDR_TYPE, IPV6_AF_TYPE); } else if (sess->alloc_param.af == pj_AF_INET()) { /* For IPv4, only add the attribute when explicitly requested */ pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, PJ_STUN_ATTR_REQ_ADDR_TYPE, IPV4_AF_TYPE); } } /* 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 back to RESOLVED may cause infinite loop (see #1942). */ //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; irenew = (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; } } /* If peer connection is TCP (RFC 6062), send it directly */ if (sess->alloc_param.peer_conn_type == PJ_TURN_TP_TCP) { status = sess->cb.on_send_pkt(sess, pkt, pkt_len, addr, addr_len); goto on_return; } /* 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; } /** * Send ConnectionBind request. */ PJ_DEF(pj_status_t) pj_turn_session_connection_bind( pj_turn_session *sess, pj_pool_t *pool, pj_uint32_t conn_id, const pj_sockaddr_t *peer_addr, unsigned addr_len) { pj_stun_tx_data *tdata; struct conn_bind_t *conn_bind; pj_status_t status; PJ_ASSERT_RETURN(sess && pool && conn_id && peer_addr && addr_len, PJ_EINVAL); PJ_ASSERT_RETURN(sess->state == PJ_TURN_STATE_READY, PJ_EINVALIDOP); pj_grp_lock_acquire(sess->grp_lock); /* Create blank ConnectionBind request */ status = pj_stun_session_create_req(sess->stun, PJ_STUN_CONNECTION_BIND_REQUEST, PJ_STUN_MAGIC, NULL, &tdata); if (status != PJ_SUCCESS) goto on_return; /* Add CONNECTION_ID attribute */ pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, PJ_STUN_ATTR_CONNECTION_ID, conn_id); conn_bind = PJ_POOL_ZALLOC_T(pool, struct conn_bind_t); conn_bind->id = conn_id; pj_sockaddr_cp(&conn_bind->peer_addr, peer_addr); conn_bind->peer_addr_len = addr_len; /* Send the request, associate connection data structure with tdata * for future reference when we receive the ConnectionBind response. */ status = pj_stun_session_send_msg(sess->stun, conn_bind, PJ_FALSE, PJ_FALSE, peer_addr, addr_len, tdata); on_return: pj_grp_lock_release(sess->grp_lock); return status; } 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_turn_session_on_rx_pkt_param prm; pj_status_t status; pj_bzero(&prm, sizeof(prm)); prm.pkt = pkt; prm.pkt_len = pkt_len; status = pj_turn_session_on_rx_pkt2(sess, &prm); if (status == PJ_SUCCESS && parsed_len) *parsed_len = prm.parsed_len; 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_pkt2( pj_turn_session *sess, pj_turn_session_on_rx_pkt_param *prm) { 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*)prm->pkt)[0] & 0xC0) == 0); if (is_stun) { /* This looks like STUN, give it to the STUN session */ unsigned options; const pj_sockaddr_t *src_addr = prm->src_addr? prm->src_addr:sess->srv_addr; unsigned src_addr_len = prm->src_addr_len? prm->src_addr_len: pj_sockaddr_get_len(sess->srv_addr); 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, prm->pkt, prm->pkt_len, options, NULL, &prm->parsed_len, src_addr, src_addr_len); } else { /* This must be ChannelData. */ pj_turn_channel_data cd; struct ch_t *ch; if (prm->pkt_len < 4) { prm->parsed_len = 0; return PJ_ETOOSMALL; } /* Decode ChannelData packet */ pj_memcpy(&cd, prm->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 (prm->pkt_len < cd.length+sizeof(cd)) { if (is_datagram) { /* Discard the datagram */ prm->parsed_len = prm->pkt_len; } else { /* Insufficient fragment */ prm->parsed_len = 0; } status = PJ_ETOOSMALL; goto on_return; } else { /* Apply padding too */ prm->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*)prm->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); if (sess->cb.on_stun_send_pkt) { return (*sess->cb.on_stun_send_pkt)(sess, (const pj_uint8_t*)pkt, (unsigned)pkt_size, dst_addr, addr_len); } else { 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; /* 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 && ((sess->alloc_param.af != 0 && raddr_attr->sockaddr.addr.sa_family != sess->alloc_param.af) || (sess->alloc_param.af == 0 && raddr_attr->sockaddr.addr.sa_family != sess->af))) { on_session_fail(sess, method, PJNATH_EINSTUNMSG, pj_cstr(&s, "Error: Mismatched RELAY-ADDRESS " "address family")); 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) { pj_time_val timeout; 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 if (method == PJ_STUN_CONNECTION_BIND_METHOD) { /* Handle ConnectionBind response */ struct conn_bind_t *conn_bind = (struct conn_bind_t*)token; if (status != PJ_SUCCESS || !PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type)) { pj_str_t reason = {0}; 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); reason = err_attr->reason; } else { status = PJNATH_EINSTUNMSG; } } pj_perror(1, sess->obj_name, status, "ConnectionBind failed: %.*s", (int)reason.slen, reason.ptr); } /* Notify app */ if (sess->cb.on_connection_bind_status) { (*sess->cb.on_connection_bind_status) (sess, status, conn_bind->id, &conn_bind->peer_addr, conn_bind->peer_addr_len); } } 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); /* ConnectionAttempt Indication */ if (msg->hdr.type == PJ_STUN_CONNECTION_ATTEMPT_INDICATION) { pj_stun_uint_attr *connection_id_attr; /* Get CONNECTION-ID attribute */ connection_id_attr = (pj_stun_uint_attr*) pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_CONNECTION_ID, 0); /* 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); /* Must have both XOR-PEER-ADDRESS and CONNECTION-ID attributes */ if (!peer_attr || !connection_id_attr) { PJ_LOG(4,(sess->obj_name, "Received ConnectionAttempt indication with missing " "attributes")); return PJ_EINVALIDOP; } /* Notify application */ if (sess->cb.on_connection_attempt) { (*sess->cb.on_connection_attempt) (sess, connection_id_attr->value, &peer_attr->sockaddr, pj_sockaddr_get_len(&peer_attr->sockaddr)); } return PJ_SUCCESS; } /* Next, 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); pj_grp_lock_dec_ref(sess->grp_lock); return; } /* Calculate total number of server entries in the response */ tot_cnt = 0; for (i=0; icount; ++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; icount && cntentry[i].server.addr_count && cntentry[i].server.addr[j].af == sess->af) { pj_sockaddr *addr = &sess->srv_addr_list[cnt]; addr->addr.sa_family = sess->af; pj_sockaddr_set_port(addr, rec->entry[i].port); if (sess->af == pj_AF_INET6()) addr->ipv6.sin6_addr = rec->entry[i].server.addr[j].ip.v6; else addr->ipv4.sin_addr = rec->entry[i].server.addr[j].ip.v4; ++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_status_t status2; status2 = pj_turn_session_alloc(sess, NULL); if (status2 != PJ_SUCCESS) on_session_fail(sess, PJ_STUN_ALLOCATE_METHOD, status2, NULL); } pj_grp_lock_dec_ref(sess->grp_lock); } /* * 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 hval2 = 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), &hval2)==0) { pj_hash_set(sess->pool, sess->ch_table, &ch->num, sizeof(ch->num), hval2, 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_PERROR(1,(sess->obj_name, status, "Error creating CreatePermission request")); 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_PERROR(1,(sess->obj_name, status, "Error sending CreatePermission request")); 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); }