Manual ICE Host Candidates

Tip

PJSUA-LIB readers — symbol equivalents are listed at the bottom of this page.

Overview

ICE host candidates are normally discovered by enumerating the local network interfaces and binding the media socket to each. That works well on a typical desktop or mobile device but breaks in environments where the address the rest of the network reaches you on isn’t an address the host’s kernel sees:

  • Docker / container deployments — the application sees the container’s private bridge address (172.17.0.x on docker0), but peers reach it on the host’s published address.

  • NAT’d virtualisation — VMs that use host-only or NAT networking from the hypervisor.

  • Split-tunnel VPNs — the tunnel address is correct, but the physical-NIC address is not.

  • Multi-homed servers — a service-discovery system that hands out only one of several local addresses for the media path.

Before this feature existed, the workaround was to point ICE at a STUN server that observed the correct external mapping and rely on the srflx candidate. That still works, but adds a STUN dependency for the simplest case — a single static mapping known up front.

PJSIP 2.17 (#4618) adds manual host candidates: the application supplies extra host candidate addresses up front and ICE treats them as if they had been auto-discovered from a local interface.

How it works

Manual candidates are additive — they augment, not replace, the auto-detected host candidates. ICE will offer all of them as host candidates in the initial SDP and the peer will pair-test against each one as part of normal ICE connectivity checks.

Each manual address is processed as follows (pjnath/src/pjnath/ice_strans.c):

  • Address family must match the transport. Manual entries for IPv4 are added only on IPv4 STUN transports, and IPv6 entries only on IPv6 transports. Mixed-family entries are filtered out for the wrong-family transport.

  • Port is inherited from the media socket. Whatever port value you set on the manual address is overwritten with the port of the auto-detected base — i.e. the actual port the media socket is bound to. Leave the port at 0 when initialising the address.

  • Foundation is computed from the base address, matching how auto-detected hosts are foundationed, so pairing behaves the same.

  • Priority follows declaration order: manual hosts are inserted with descending local preference, so the first manual entry is preferred over the second, and so on.

  • Total host count is capped by pj::AccountNatConfig::iceMaxHostCands / pjsua_ice_config::ice_max_host_cands (default -1, i.e. PJ_ICE_ST_MAX_CAND, currently 64). Auto-detected and manual host candidates share this budget. PJSUA2 additionally raises PJ_ETOOMANY if the input vector exceeds the internal array size.

The feature does not change SIP-layer addressing. SIP requests and the SDP c= line still reflect the addresses chosen by the SIP transport (pjsua_transport_config::public_addr, etc.). Manual host candidates only affect the ICE candidate list inside the SDP.

PJSUA2 usage

Set pj::AccountNatConfig::iceManualHost to a vector of bare host-address strings. Numeric IPv4 and IPv6 literals are the common case; hostnames are also accepted (resolved through the platform resolver by pj_sockaddr_set_str_addr()), but since manual candidates exist precisely to pin a specific address, using a literal is normally clearer.

AccountConfig acfg;
// ... existing config ...
acfg.natConfig.iceEnabled       = true;
acfg.natConfig.iceManualHost.clear();
acfg.natConfig.iceManualHost.push_back("203.0.113.5");          // public IPv4
acfg.natConfig.iceManualHost.push_back("2001:db8::5");          // public IPv6

try {
    account.create(acfg);   // or account.modify(acfg);
} catch(Error& err) {
    // PJ_EINVAL if any string failed to parse,
    // PJ_ETOOMANY if vector exceeds the internal cap.
}

Notes:

PJSUA-LIB usage

Populate pjsua_ice_config::ice_manual_host (an array of pj_sockaddr) and set pjsua_ice_config::ice_manual_host_cnt.

pjsua_acc_config acc_cfg;
pjsua_acc_config_default(&acc_cfg);

acc_cfg.ice_cfg_use            = PJSUA_ICE_CONFIG_USE_CUSTOM;
acc_cfg.ice_cfg.enable_ice     = PJ_TRUE;

pj_str_t v4 = pj_str("203.0.113.5");
pj_str_t v6 = pj_str("2001:db8::5");
pj_sockaddr_set_str_addr(pj_AF_INET(),
                         &acc_cfg.ice_cfg.ice_manual_host[0], &v4);
pj_sockaddr_set_str_addr(pj_AF_INET6(),
                         &acc_cfg.ice_cfg.ice_manual_host[1], &v6);
acc_cfg.ice_cfg.ice_manual_host_cnt = 2;

pjsua_acc_id acc_id;
pjsua_acc_add(&acc_cfg, PJ_TRUE, &acc_id);

Manual host candidates are configured per account; the global pjsua_media_config does not expose ice_manual_host / ice_manual_host_cnt. To apply the same manual addresses to every account, set them on each account config.

A standalone PJNATH application configures the array on pj_ice_strans_stun_cfg::manual_host (with pj_ice_strans_stun_cfg::manual_host_cnt) inside pj_ice_strans_cfg, before calling pj_ice_strans_create().

pjsua CLI

There is no --ice-manual-host command-line option in the pjsua sample app at present. Applications that want to set manual hosts have to use the API.

When manual hosts are not enough

Manual host candidates only solve the ICE side of the address mismatch. If the SIP signalling path also goes through NAT, the addresses in the SIP request URI, Contact header, and SDP c= line still need their own handling — typically via pjsua_transport_config::public_addr for the SIP transport, and STUN or pjsua_acc_config::nat64_opt for SDP. See Getting around NAT (for media) for the broader NAT picture.

Manual host candidates also won’t help when the inbound address is not directly routable to the media socket — for example, when the container’s NAT does not forward UDP at all. In that case TURN relaying is still required; configure a TURN server in addition to the manual host. See TURN over TCP and TLS.

PJSUA-LIB equivalents

PJSUA2

PJSUA-LIB

pj::AccountNatConfig::iceManualHost (SocketAddressVector of address strings)

pjsua_ice_config::ice_manual_host / pjsua_ice_config::ice_manual_host_cnt (pj_sockaddr array)

pj::AccountNatConfig::iceMaxHostCands

pjsua_ice_config::ice_max_host_cands

(configured via pj::AccountNatConfig::iceManualHost)

Standalone PJNATH: pj_ice_strans_stun_cfg::manual_host / pj_ice_strans_stun_cfg::manual_host_cnt

References