PJSIP 是非常精致的SIP Client 协议栈, 其层次结构清晰的API设计, 良好的跨平台属性, 在业内有很高的声誉, 有不少SIP商业应用基于它开发. 可是PJSIP早期版本从未对IPV6有很好的支持. Apple 在今年5月时发布声明, 要求6月1号以后所有的iOS应用都必须包含对IPV6-ONLY网络的支持. 这个对基于PJSIP开发的iOS应用来说可谓是当头一棒. 很多人呼吁要求PJSIP来快速支持IPV6, PJSIP的开发小组也迅速的将IPV6支持列入计划, 在7月份发布了版本 2.5.5, 宣布了对IPV6的支持. 这个新版本的发布声明如下:
PJSIP version 2.5.5 is released with the main focus on:
IPv6 support in PJNATH
IPv6 support in PJLIB-UTIL DNS SRV and DNS AAAA resolution
IPv6 support for hostname resolution in PJSIP and PJSUA-LIB
PJSIP 的这个版本刚发布, 我们就投入了极大地兴趣对其进行研究, 这次的更新确实解决了IPV6,以及IPVT NAT的一些BUG, 从其实现逻辑来分析, 它在纯IPV6网络应该可以正常工作(这里的纯IPV6网络是指从client到server, 其所有的路由节点都是IPV6设备). 但是这个却无法满足目前的Internet网络对其需求!
IPV6标准虽然已经推出多年, 但是由于众所周知的原因, 目前的Internet网络的主流还是IPV4, 现在我们能看到的IPV6网络, 大多是架设在IPV4网络上的一些子网, 通过IPV6 to IPV4的NAT跟外部的因特网进行通信. 所以目前应用要支持的IPV6用户案例, 基本上会是: app运行于IPV6子网, 服务器位于IPV4的云端, 连接的路由类型是IPV4/IPV6的混合类型. 非常遗憾的是 PJSIP 是无法支持这种类型的网络的.
其根源在于SIP自身的复杂性!
简单的网络应用, 通过域名解析建立连接, 并不关心服务器的真实IP, 但是SIP协议则不然,在SIP终端与服务器建立连接时, 除了第一次是通过域名解析, 以后的通信都会利用保存下来的SIP Proxy的Public 地址, 减少域名解析的次数,来达到优化之目的. 但是在上文所述的混合网络中, Server的地址是IPV4的, 由于本地地址为IPV6, PJSIP所建立的信令连接是IPV6类型的, 这会导致: SIP Message生成, 然后交给信令通道进行传输, 但是此Message却在到达网络前就被信令通道给丢弃了!
这个是PJSIP无法在混合网络上建立SIP的根本原因. 好了, 理解了这一点, 我们可以来具体分析PJSIP实现的瑕疵, 或者说Bug, 解决的思路也很简单, 因为PSJIP Client是处于IPV6网络, 外部是IPV4网络, 所以只需要将外部的IPV4地址转译成内部可以支持的IPV6地址, 问题即可迎刃而解. 转译的思路如下:
+-------------------+--------------+----------------------------+ | Well-Known Prefix | IPv4 address | IPv4-Embedded IPv6 address | +-------------------+--------------+----------------------------+ | 64:ff9b::/96 | 192.0.2.33 | 64:ff9b::192.0.2.33 | +-------------------+--------------+----------------------------+ Table 2: Text Representation of IPv4-Embedded IPv6 Addresses Using the Well-Known Prefix
OK, 理解了原理, 我们就可以来谈一下具体的BUG应该如何解决, PJSIP的问题一共有3处:
1. Server Resolve
其解决思路就是:当发现自身的信令通道为IPV6类型, Server地址为IPV4是, 用上述方法将Server地址转译为IPV6
2. Register
这里是PJSIP的BUG, 通常的SIP 注册会有2次, 第一次是普通的注册,不带鉴权信息, 收到Server Challenge 之后会发起第二次带鉴权信息的注册, 其实这里的2次注册不仅仅是为了鉴权, 第一次注册的response上, server可以告诉SIP Client它的public address, 所以以后 client 发出的SIP message, 其contact 可以带上自己的内网和外网地址. PJSIP在这里的实现有漏洞!
pjsua_acc.c
if (status == PJ_SUCCESS) {
/* Compare the addresses as sockaddr according to the ticket above,
* but only if they have the same family (ipv4 vs ipv4, or
* ipv6 vs ipv6)
*/
matched = (contact_addr.addr.sa_family != recv_addr.addr.sa_family) ||
(uri->port == rport &&
pj_sockaddr_cmp(&contact_addr, &recv_addr)==0);
} else {
/* Compare the addresses as string, as before */
matched = (uri->port == rport &&
pj_stricmp(&uri->host, via_addr)==0);
}
if (matched) {
/* Address doesn't change */
pj_pool_release(pool);
return PJ_FALSE;
}
当自己的内网IP family跟外网 IP family不同, 竟然被判为matched! 从而取消了第二次注册, 导致无法注册成功! 其修改也很简单,简单用pj_sockaddr_cmp判决matched 即可.
3 Establish Data Transport.
这里的修改思路跟1类似, 因为外部是IPV4网络,所以收到的SDP的对端地址也是IPV4类型, 从而引发与多媒体通道的连接类型冲突, 导致无法建立数据连接, 只要把外部的地址改为IPV6, 就可以正常建立媒体数据通道了。 需要修改stream_info.c和vid_stream_info.c
到此, 在IPV6网络上使用PJSIP 建立SIP CALL应该非常完美了.
虽然上文说述的bug改动不难, 但是由于我们对PJSIP并未了如指掌, 逐个排查, 确定问题症结, 也是绞尽脑汁。 行文至此, 精华已竭, 希望对读者诸君有所帮助。
本文由@Sandfox 和本人协作完成.
Reference: