diff options
Diffstat (limited to 'modules/ice/ice.c')
-rw-r--r-- | modules/ice/ice.c | 696 |
1 files changed, 696 insertions, 0 deletions
diff --git a/modules/ice/ice.c b/modules/ice/ice.c new file mode 100644 index 0000000..e8de4d7 --- /dev/null +++ b/modules/ice/ice.c @@ -0,0 +1,696 @@ +/** + * @file ice.c ICE Module + * + * Copyright (C) 2010 Creytiv.com + */ +#ifdef __APPLE__ +#include <CoreFoundation/CoreFoundation.h> +#include <SystemConfiguration/SCNetworkReachability.h> +#endif +#include <re.h> +#include <baresip.h> + + +/** + * @defgroup ice ice + * + * Interactive Connectivity Establishment (ICE) for media NAT traversal + * + * This module enables ICE for NAT traversal. You can enable ICE + * in your accounts file with the parameter ;medianat=ice. The following + * options can be configured: + * + \verbatim + ice_turn {yes,no} # Enable TURN candidates + ice_debug {yes,no} # Enable ICE debugging/tracing + ice_nomination {regular,aggressive} # Regular or aggressive nomination + ice_mode {full,lite} # Full ICE-mode or ICE-lite + \endverbatim + */ + + +struct mnat_sess { + struct list medial; + struct sa srv; + struct stun_dns *dnsq; + struct sdp_session *sdp; + struct ice *ice; + char *user; + char *pass; + int mediac; + bool started; + bool send_reinvite; + mnat_estab_h *estabh; + void *arg; +}; + +struct mnat_media { + struct comp { + struct sa laddr; + void *sock; + } compv[2]; + struct le le; + struct mnat_sess *sess; + struct sdp_media *sdpm; + struct icem *icem; + bool complete; +}; + + +static struct mnat *mnat; +static struct { + enum ice_mode mode; + enum ice_nomination nom; + bool turn; + bool debug; +} ice = { + ICE_MODE_FULL, + ICE_NOMINATION_REGULAR, + true, + false +}; + + +static void gather_handler(int err, uint16_t scode, const char *reason, + void *arg); + + +static bool is_cellular(const struct sa *laddr) +{ +#if TARGET_OS_IPHONE + SCNetworkReachabilityRef r; + SCNetworkReachabilityFlags flags = 0; + bool cell = false; + + r = SCNetworkReachabilityCreateWithAddressPair(NULL, + &laddr->u.sa, NULL); + if (!r) + return false; + + if (SCNetworkReachabilityGetFlags(r, &flags)) { + + if (flags & kSCNetworkReachabilityFlagsIsWWAN) + cell = true; + } + + CFRelease(r); + + return cell; +#else + (void)laddr; + return false; +#endif +} + + +static void ice_printf(struct mnat_media *m, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + debug("%s: %v", m ? sdp_media_name(m->sdpm) : "ICE", fmt, &ap); + va_end(ap); +} + + +static void session_destructor(void *arg) +{ + struct mnat_sess *sess = arg; + + list_flush(&sess->medial); + mem_deref(sess->dnsq); + mem_deref(sess->user); + mem_deref(sess->pass); + mem_deref(sess->ice); + mem_deref(sess->sdp); +} + + +static void media_destructor(void *arg) +{ + struct mnat_media *m = arg; + unsigned i; + + list_unlink(&m->le); + mem_deref(m->sdpm); + mem_deref(m->icem); + for (i=0; i<2; i++) + mem_deref(m->compv[i].sock); +} + + +static bool candidate_handler(struct le *le, void *arg) +{ + return 0 != sdp_media_set_lattr(arg, false, ice_attr_cand, "%H", + ice_cand_encode, le->data); +} + + +/** + * Update the local SDP attributes, this can be called multiple times + * when the state of the ICE machinery changes + */ +static int set_media_attributes(struct mnat_media *m) +{ + int err = 0; + + if (icem_mismatch(m->icem)) { + err = sdp_media_set_lattr(m->sdpm, true, + ice_attr_mismatch, NULL); + return err; + } + else { + sdp_media_del_lattr(m->sdpm, ice_attr_mismatch); + } + + /* Encode all my candidates */ + sdp_media_del_lattr(m->sdpm, ice_attr_cand); + if (list_apply(icem_lcandl(m->icem), true, candidate_handler, m->sdpm)) + return ENOMEM; + + if (ice_remotecands_avail(m->icem)) { + err |= sdp_media_set_lattr(m->sdpm, true, + ice_attr_remote_cand, "%H", + ice_remotecands_encode, m->icem); + } + + return err; +} + + +static bool if_handler(const char *ifname, const struct sa *sa, void *arg) +{ + struct mnat_media *m = arg; + uint16_t lprio; + unsigned i; + int err = 0; + + /* Skip loopback and link-local addresses */ + if (sa_is_loopback(sa) || sa_is_linklocal(sa)) + return false; + + lprio = is_cellular(sa) ? 0 : 10; + + ice_printf(m, "added interface: %s:%j (local prio %u)\n", + ifname, sa, lprio); + + for (i=0; i<2; i++) { + if (m->compv[i].sock) + err |= icem_cand_add(m->icem, i+1, lprio, ifname, sa); + } + + if (err) { + warning("ice: %s:%j: icem_cand_add: %m\n", ifname, sa, err); + } + + return false; +} + + +static int media_start(struct mnat_sess *sess, struct mnat_media *m) +{ + int err = 0; + + net_if_apply(if_handler, m); + + switch (ice.mode) { + + default: + case ICE_MODE_FULL: + if (ice.turn) { + err = icem_gather_relay(m->icem, &sess->srv, + sess->user, sess->pass); + } + else { + err = icem_gather_srflx(m->icem, &sess->srv); + } + break; + + case ICE_MODE_LITE: + gather_handler(0, 0, NULL, m); + break; + } + + return err; +} + + +static void dns_handler(int err, const struct sa *srv, void *arg) +{ + struct mnat_sess *sess = arg; + struct le *le; + + if (err) + goto out; + + sess->srv = *srv; + + for (le=sess->medial.head; le; le=le->next) { + + struct mnat_media *m = le->data; + + err = media_start(sess, m); + if (err) + goto out; + } + + return; + + out: + sess->estabh(err, 0, NULL, sess->arg); +} + + +static int session_alloc(struct mnat_sess **sessp, struct dnsc *dnsc, + int af, const char *srv, uint16_t port, + const char *user, const char *pass, + struct sdp_session *ss, bool offerer, + mnat_estab_h *estabh, void *arg) +{ + struct mnat_sess *sess; + const char *usage; + int err; + + if (!sessp || !dnsc || !srv || !user || !pass || !ss || !estabh) + return EINVAL; + + sess = mem_zalloc(sizeof(*sess), session_destructor); + if (!sess) + return ENOMEM; + + sess->sdp = mem_ref(ss); + sess->estabh = estabh; + sess->arg = arg; + + err = str_dup(&sess->user, user); + err |= str_dup(&sess->pass, pass); + if (err) + goto out; + + err = ice_alloc(&sess->ice, ice.mode, offerer); + if (err) + goto out; + + ice_conf(sess->ice)->nom = ice.nom; + ice_conf(sess->ice)->debug = ice.debug; + + if (ICE_MODE_LITE == ice.mode) { + err |= sdp_session_set_lattr(ss, true, + ice_attr_lite, NULL); + } + + err |= sdp_session_set_lattr(ss, true, + ice_attr_ufrag, ice_ufrag(sess->ice)); + err |= sdp_session_set_lattr(ss, true, + ice_attr_pwd, ice_pwd(sess->ice)); + if (err) + goto out; + + usage = ice.turn ? stun_usage_relay : stun_usage_binding; + + err = stun_server_discover(&sess->dnsq, dnsc, usage, stun_proto_udp, + af, srv, port, dns_handler, sess); + + out: + if (err) + mem_deref(sess); + else + *sessp = sess; + + return err; +} + + +static bool verify_peer_ice(struct mnat_sess *ms) +{ + struct le *le; + + for (le = ms->medial.head; le; le = le->next) { + struct mnat_media *m = le->data; + struct sa raddr[2]; + unsigned i; + + if (!sdp_media_has_media(m->sdpm)) { + info("ice: stream '%s' is disabled -- ignore\n", + sdp_media_name(m->sdpm)); + continue; + } + + raddr[0] = *sdp_media_raddr(m->sdpm); + sdp_media_raddr_rtcp(m->sdpm, &raddr[1]); + + for (i=0; i<2; i++) { + if (m->compv[i].sock && + !icem_verify_support(m->icem, i+1, &raddr[i])) { + warning("ice: %s.%u: no remote candidates" + " found (address = %J)\n", + sdp_media_name(m->sdpm), + i+1, &raddr[i]); + return false; + } + } + } + + return true; +} + + +static bool refresh_comp_laddr(struct mnat_media *m, unsigned id, + struct comp *comp, const struct sa *laddr) +{ + bool changed = false; + + if (!m || !comp || !comp->sock || !laddr) + return false; + + if (!sa_cmp(&comp->laddr, laddr, SA_ALL)) { + changed = true; + + ice_printf(m, "comp%u setting local: %J\n", id, laddr); + } + + sa_cpy(&comp->laddr, laddr); + + if (id == 1) + sdp_media_set_laddr(m->sdpm, &comp->laddr); + else if (id == 2) + sdp_media_set_laddr_rtcp(m->sdpm, &comp->laddr); + + return changed; +} + + +/* + * Update SDP Media with local addresses + */ +static bool refresh_laddr(struct mnat_media *m, + const struct sa *laddr1, + const struct sa *laddr2) +{ + bool changed = false; + + changed |= refresh_comp_laddr(m, 1, &m->compv[0], laddr1); + changed |= refresh_comp_laddr(m, 2, &m->compv[1], laddr2); + + return changed; +} + + +static void gather_handler(int err, uint16_t scode, const char *reason, + void *arg) +{ + struct mnat_media *m = arg; + + if (err || scode) { + warning("ice: gather error: %m (%u %s)\n", + err, scode, reason); + } + else { + refresh_laddr(m, + icem_cand_default(m->icem, 1), + icem_cand_default(m->icem, 2)); + + info("ice: %s: Default local candidates: %J / %J\n", + sdp_media_name(m->sdpm), + &m->compv[0].laddr, &m->compv[1].laddr); + + (void)set_media_attributes(m); + + if (--m->sess->mediac) + return; + } + + m->sess->estabh(err, scode, reason, m->sess->arg); +} + + +static void conncheck_handler(int err, bool update, void *arg) +{ + struct mnat_media *m = arg; + struct mnat_sess *sess = m->sess; + struct le *le; + + info("ice: %s: connectivity check is complete (update=%d)\n", + sdp_media_name(m->sdpm), update); + + ice_printf(m, "Dumping media state: %H\n", icem_debug, m->icem); + + if (err) { + warning("ice: connectivity check failed: %m\n", err); + } + else { + bool changed; + + m->complete = true; + + changed = refresh_laddr(m, + icem_selected_laddr(m->icem, 1), + icem_selected_laddr(m->icem, 2)); + if (changed) + sess->send_reinvite = true; + + (void)set_media_attributes(m); + + /* Check all conncheck flags */ + LIST_FOREACH(&sess->medial, le) { + struct mnat_media *mx = le->data; + if (!mx->complete) + return; + } + } + + /* call estab-handler and send re-invite */ + if (sess->send_reinvite && update) { + + info("ice: %s: sending Re-INVITE with updated" + " default candidates\n", + sdp_media_name(m->sdpm)); + + sess->estabh(0, 0, NULL, sess->arg); + sess->send_reinvite = false; + } +} + + +static int ice_start(struct mnat_sess *sess) +{ + struct le *le; + int err = 0; + + ice_printf(NULL, "ICE Start: %H", ice_debug, sess->ice); + + /* Update SDP media */ + if (sess->started) { + + LIST_FOREACH(&sess->medial, le) { + struct mnat_media *m = le->data; + + icem_update(m->icem); + + refresh_laddr(m, + icem_selected_laddr(m->icem, 1), + icem_selected_laddr(m->icem, 2)); + + err |= set_media_attributes(m); + } + + return err; + } + + /* Clear all conncheck flags */ + LIST_FOREACH(&sess->medial, le) { + struct mnat_media *m = le->data; + + if (sdp_media_has_media(m->sdpm)) { + m->complete = false; + + if (ice.mode == ICE_MODE_FULL) { + err = icem_conncheck_start(m->icem); + if (err) + return err; + } + } + else { + m->complete = true; + } + } + + sess->started = true; + + return 0; +} + + +static int media_alloc(struct mnat_media **mp, struct mnat_sess *sess, + int proto, void *sock1, void *sock2, + struct sdp_media *sdpm) +{ + struct mnat_media *m; + unsigned i; + int err = 0; + + if (!mp || !sess || !sdpm) + return EINVAL; + + m = mem_zalloc(sizeof(*m), media_destructor); + if (!m) + return ENOMEM; + + list_append(&sess->medial, &m->le, m); + m->sdpm = mem_ref(sdpm); + m->sess = sess; + m->compv[0].sock = mem_ref(sock1); + m->compv[1].sock = mem_ref(sock2); + + err = icem_alloc(&m->icem, sess->ice, proto, 0, + gather_handler, conncheck_handler, m); + if (err) + goto out; + + icem_set_name(m->icem, sdp_media_name(sdpm)); + + for (i=0; i<2; i++) { + if (m->compv[i].sock) + err |= icem_comp_add(m->icem, i+1, m->compv[i].sock); + } + + if (sa_isset(&sess->srv, SA_ALL)) + err |= media_start(sess, m); + + out: + if (err) + mem_deref(m); + else { + *mp = m; + ++sess->mediac; + } + + return err; +} + + +static bool sdp_attr_handler(const char *name, const char *value, void *arg) +{ + struct mnat_sess *sess = arg; + return 0 != ice_sdp_decode(sess->ice, name, value); +} + + +static bool media_attr_handler(const char *name, const char *value, void *arg) +{ + struct mnat_media *m = arg; + return 0 != icem_sdp_decode(m->icem, name, value); +} + + +static int enable_turn_channels(struct mnat_sess *sess) +{ + struct le *le; + int err = 0; + + for (le = sess->medial.head; le; le = le->next) { + + struct mnat_media *m = le->data; + struct sa raddr[2]; + unsigned i; + + err |= set_media_attributes(m); + + raddr[0] = *sdp_media_raddr(m->sdpm); + sdp_media_raddr_rtcp(m->sdpm, &raddr[1]); + + for (i=0; i<2; i++) { + if (m->compv[i].sock && sa_isset(&raddr[i], SA_ALL)) + err |= icem_add_chan(m->icem, i+1, &raddr[i]); + } + } + + return err; +} + + +/** This can be called several times */ +static int update(struct mnat_sess *sess) +{ + struct le *le; + int err = 0; + + /* SDP session */ + (void)sdp_session_rattr_apply(sess->sdp, NULL, sdp_attr_handler, sess); + + /* SDP medialines */ + for (le = sess->medial.head; le; le = le->next) { + struct mnat_media *m = le->data; + + sdp_media_rattr_apply(m->sdpm, NULL, media_attr_handler, m); + } + + /* 5.1. Verifying ICE Support */ + if (verify_peer_ice(sess)) { + err = ice_start(sess); + } + else if (ice.turn) { + info("ice: ICE not supported by peer, fallback to TURN\n"); + err = enable_turn_channels(sess); + } + else { + info("ice: ICE not supported by peer\n"); + + LIST_FOREACH(&sess->medial, le) { + struct mnat_media *m = le->data; + + err |= set_media_attributes(m); + } + } + + return err; +} + + +static int module_init(void) +{ +#ifdef MODULE_CONF + struct pl pl; + + conf_get_bool(conf_cur(), "ice_turn", &ice.turn); + conf_get_bool(conf_cur(), "ice_debug", &ice.debug); + + if (!conf_get(conf_cur(), "ice_nomination", &pl)) { + if (0 == pl_strcasecmp(&pl, "regular")) + ice.nom = ICE_NOMINATION_REGULAR; + else if (0 == pl_strcasecmp(&pl, "aggressive")) + ice.nom = ICE_NOMINATION_AGGRESSIVE; + else { + warning("ice: unknown nomination: %r\n", &pl); + } + } + if (!conf_get(conf_cur(), "ice_mode", &pl)) { + if (!pl_strcasecmp(&pl, "full")) + ice.mode = ICE_MODE_FULL; + else if (!pl_strcasecmp(&pl, "lite")) + ice.mode = ICE_MODE_LITE; + else { + warning("ice: unknown mode: %r\n", &pl); + } + } +#endif + + return mnat_register(&mnat, "ice", "+sip.ice", + session_alloc, media_alloc, update); +} + + +static int module_close(void) +{ + mnat = mem_deref(mnat); + + return 0; +} + + +EXPORT_SYM const struct mod_export DECL_EXPORTS(ice) = { + "ice", + "mnat", + module_init, + module_close, +}; |