目录
前言
1、NAT问题简介
2、解决终端NAT问题
2.1 使用终端的外网地址通信
2.2 保活终端NAT路由映射
3、 媒体NAT处理
小结
前言
跨NAT通信在VOIP通信中常会出现问题,由其原生基于UDP传输导致。虽然现在也有基于websocket这样的基于TCP的传输方式,但整体上还是基于UDP的。而VOIP的很重要的应用场景是企业应用,所以很多时候通信终端都是处于内网之中,这也使得解决NAT问题更加重要。
1、NAT问题简介
NAT(Network Address Translation,网络地址转换),是一种实现内网中的主机与因特网上的主机通信的技术,该技术使用普遍,大到企业内网和小到日常的路由器等。企业内网通常通过一个NAT网关进行协议转换并建立内网与外网的连接会话,并由NAT网关负责连接会话的管理。如图
网关会对长时间没有通信的会话进行清理,回收IP和端口资源。对于基于TCP的服务而言,与外网服务保持长连接,似乎并没有太多通信的问题,因为TCP基于连接且有状态的。但对于无状态的UDP而言,服务端需要知道终端的外网地址,否则服务端的消息将无法返回给内网中的客户端。由于语音通信普遍使用基于UDP的SIP协议进行通信,往往使得NAT穿越成为问题。
2、解决终端NAT问题
终端的问题主要是两个,一个是内网中的终端只知道自己的内网地址,二是需要维持NAT地址的映射关系,不能让NAT网关清掉了。
2.1 使用终端的外网地址通信
内网中的终端是不知道自己的外网地址的,所以发起SIP REGISTER或其他消息的时候,Contact头域带的是终端的内网地址,如:
REGISTER sip:192.168.253.128:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.253.1:6090;branch=z9hG4bK-d87543-e4034a3570717061-1--d87543-;rport
Max-Forwards: 70
Contact: <sip:10086@192.168.253.1:6090;rinstance=76a144f7248850e7>
To: "10086"<sip:10086@192.168.253.128:5060>
From: "10086"<sip:10086@192.168.253.128:5060>;tag=39775242
Call-ID: c32d6d4b7004dc02@Rmlubi1QQw..
CSeq: 4 REGISTER
Expires: 3600
...
OpenSIPs默认是会试用Contact头域的地址作为终端的通信地址,所以这会导致终端收不到OpenSIPs返回的请求响应消息,当然也收不到呼叫请求。
要解决这个问题,就需要让OpenSIPs获取终端的外网地址,并将Contact中的URI地址替换为终端的外网地址并保存起来(Contact信息默认存在MySQL 数据库location表中),后续通信都以保存的外网地址通信。这个功能主要由OpenSIPs的nat_traversal模块提供,只需要在OpenSIPS路由脚本中加一下配置就可以实现:
loadmodule "nat_traversal.so" #加载提供支持的模块
loadmodule "nathelper.so"
route{
...
if (is_method("REGISTER")){
...
if (client_nat_test("11")) {
fix_contact();
}
fix_nated_register();
if (!save("location")){
sl_reply_error();
exit;
}
exit;
}
...
}
这样终端的注册信息中就保存了外网地址,解决了NAT通信问题。
主要函数解析:
client_nat_test()函数:用于检查SIP是否包含私有地址,参数有0x01、0x02、0x04、0x08,或这四个值中的多个按位或得到的值,示例中的参数11是0x01|0x02|0x08得到的值(0x04值的意义请参考文档),分别表示检查Contact头域地址是否是私有地址、检查Via头域地址和接收消息的终端实际IP地址是否一样,已经检查Contact头域地址是否和接收到消息的终端实际IP地址是否一样,如果检查满足任意一个就进行fix_contact操作。私有地址判断遵循RFC 1918和RFC 6598。
fix_contact()函数:使用实际收到消息的源IP和Port替换SIP消息Contact头域中的IP和Port,后续保存Dialog时就保存了实际的IP地址,用于后续消息交互,也就解决了终端的NAT通信问题。
fix_nated_register()函数:该函数由nathelper模块提供,主要解析源地址并在200 OK响应消息中的Contact头域中添加received字段,将终端的外网地址返回给终端。主要用于REGISTER请求,让终端得到自己的外网地址(如果终端需要用的话)。
此外,使用OpenSIPs模块导出的函数时,需要注意使用范围,fix_contact()和client_nat_test()导出函数能在REQUEST_ROUTE, ONREPLY_ROUTE, BRANCH_ROUTE这几个路由块中使用。而fix_nated_register()函数只能在 REQUEST_ROUTE块中使用。关于路由有疑问可以参考《OpenSIPS实战(三):路由脚本介绍与实战》
2.2 保活NAT路由映射
就像前面说的,网关会对长时间没有通信的会话进行清理,回收IP和端口资源,所以终端建立的NAT路由表需要进行保活。一般的SIP终端都有定期发送探测包,如我使用的eyeBeam和linePhone注册OpenSIPs成功后都会定期向OpenSIPs发送探测包,这样就能达到保活终端的NAT路由映射的功能。下面是OpenSIPs打印的收到探测包的日志和抓包抓到的探测包:
# OpenSIPs日志打印收到探测包
Jan 27 00:11:16 [3497] DBG:core:udp_read_req: probing packet received len = 4
Jan 27 00:11:17 [3496] DBG:core:udp_read_req: probing packet received len = 4
# ngrep抓到探测包#
U 192.168.253.1:6090 -> 192.168.253.128:5060 #3712
.
.
..............
#
U 192.168.253.1:5060 -> 192.168.253.128:5060 #3713
.
.
..............
但是,全由客户端主动发起探测有时候会比较被动,比如某个不标准的终端不会定期发起探测或探测间隔太长等。所以如果想要服务端掌握主动权的话,就需要由服务端主动发起探测请求。OpenSIPs同样支持这样的功能,只需在路由脚本中增加如下配置:
loadmodule "nat_traversal.so" #加载提供支持的模块
modparam("nat_traversal", "keepalive_interval", 90)
route{
...
if (($rm=="REGISTER" || ($rm=="INVITE" && !has_totag()) ) && client_nat_test("3"))
{
#设置会话定时器,定期发送保活消息
nat_keepalive();
}
...
}
主要函数解析:
nat_keepalive()函数:该函数会为该Dialog添加定时器,定时发送保活消息。默认的发送的是NOTIFY消息,可以通过模块参数修改,包括保活时间间隔设置。
下面是OpenSIPs发起的保活消息:
# OpenSIPs发起的定期保活的SIP NOTIFY消息
NOTIFY sip:192.168.253.1:6090 SIP/2.0
Via: SIP/2.0/UDP 192.168.253.128:5060;branch=z9hG4bK2384259
From: sip:keepalive@192.168.253.128;tag=32140b8
To: sip:192.168.253.1:6090
Call-ID: 4916e47f-57f4f97e-48@192.168.253.128
CSeq: 1 NOTIFY
Event: keep-alive
Content-Length: 0
SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.253.128:5060;branch=z9hG4bK2384259
Contact: <sip:192.168.253.1:6090>
To: <sip:192.168.253.1:6090>;tag=d52a6c49
From: <sip:keepalive@192.168.253.128>;tag=32140b8
Call-ID: 4916e47f-57f4f97e-48@192.168.253.128
CSeq: 1 NOTIFY
User-Agent: eyeBeam release 3015c stamp 27107
Content-Length: 0
这样,OpenSIPs主动保活的功能就实现完成了。
3、媒体NAT处理
前两小节讲的都是信令的NAT处理,现在来看下媒体流的NAT处理,这里讲的还是以OpenSIPs作为信令服务器而不是兼顾媒体服务而言的,也就是只涉及SDP协议的处理。
OpenSIPs媒体NAT处理不是很常见,一般终端NAT处理好了就没有什么问题了。但也遇到过,主要是在搞基于websocket的web电话的时候遇到过,SDP保存的地址使用的是内网地址。
OpenSIPs的nathelper模块导出函数fix_nated_sdp提供对SDP中的媒体IP进行修改的支持。
下面是修改实例,如果是基于websocket的请求,就修改SDP中的媒体地址和源地址为信令请求的源地址(实际的外网地址):
loadmodule "nathelper.so" #加载提供支持的模块
route{
...
if (is_method("INVITE")) {
...
if ( proto==WS)
{
fix_nated_sdp(“10”, , );
}
t_on_reply("modify_sdp");
}
...
}
onreply_route[modify_sdp] {
if(status=="200") {
fix_nated_sdp(“10”, , );
}
}
增加fix_nated_sdp函数处理后,主叫的SDP媒体地址(c=)和源(o=)中的地址将会被替换为消息的源地址后,再用于呼叫被叫。如果被叫的SDP也需要做修改,可以在ONREPLY_ROUTE脚本段中,判断被叫返回的200 OK,并增加fix_nated_sdp()函数进行转换,正如示例中做的那样。
fix_nated_sdp(flags [, ip_address [, sdp_fields]])函数解析:
flags必选参数,该值可以是以下值或以下值的按位或得到的值:
0x01 --在SDP中增加“a=direction:active”行;
0x02 --使用消息的源地址或者ip_address参数指定的Ip地址重写SDP中媒体IP地址(“c=”)。
0x04--在SDP中增加”a=nortpproxy:yes”行。
0x08 --使用消息的源地址或者ip_address参数指定的Ip地址重写SDP中源IP地址(“o=”)。
0x10 --强制重写空的媒体IP和空的源IP地址。如果没有此标志,空IP将保持不变。
ip_address可选参数,指定用来重写SDP的IP,如果不指定该参数,默认使用信令消息的源地址。
sdp_fields可选参数,指定要附加到SDP消息后的sdp字段,每个sdp字段前面必须有“\r\n”
小结
从全文可以看出,OpenSIPs解决NAT通信问题非常简单,解决终端NAT问题并不难。如果这样还不能满足需求,可以参阅文档,寻找其他可能的实现。再不济可以考虑修改NAT相关模块的实现,提供新的函数实现,总是可以做到的。
(全文完)
更多查看官方文档
https://opensips.org/html/docs/modules/2.3.x/nat_traversal.html#idp5604720
https://opensips.org/html/docs/modules/2.4.x/nathelper.html#idp5641936