ip_conntrack_sip模块用于为SIP协议建立所需的连接跟踪。支持指定最大8个监听端口,多个端口号使用逗号分隔。另外在加载模块时可指定的参数有:
# modprobe ip_conntrack_sip ports=5060 #define SIP_PORT 5060 #define SIP_TIMEOUT 3600 #define MAX_PORTS 8 static unsigned short ports[MAX_PORTS]; static unsigned int ports_c; module_param_array(ports, ushort, &ports_c, 0400); static unsigned int sip_timeout __read_mostly = SIP_TIMEOUT; module_param(sip_timeout, uint, 0600); static int sip_direct_signalling __read_mostly = 1; module_param(sip_direct_signalling, int, 0600); static int sip_direct_media __read_mostly = 1; module_param(sip_direct_media, int, 0600); static int sip_external_media __read_mostly = 0;
对于每个SIP端口,初始化四个helper结构,分别用于IPv4和IPv6的UDP和TCP协议,但是处理函数只有两个sip_help_udp和sip_help_tcp,可同时处理IPv4和IPv6协议。如果在加载此模块时,没有指定端口号,使用默认的SIP端口5060(SIP_PORT)。注册函数将helper链接到全局链表nf_ct_helper_hash。
static struct nf_conntrack_helper sip[MAX_PORTS * 4] __read_mostly; static int __init nf_conntrack_sip_init(void) { NF_CT_HELPER_BUILD_BUG_ON(sizeof(struct nf_ct_sip_master)); if (ports_c == 0) ports[ports_c++] = SIP_PORT; for (i = 0; i < ports_c; i++) { nf_ct_helper_init(&sip[4 * i], AF_INET, IPPROTO_UDP, HELPER_NAME, SIP_PORT, ports[i], i, sip_exp_policy, SIP_EXPECT_MAX, sip_help_udp, NULL, THIS_MODULE); nf_ct_helper_init(&sip[4 * i + 1], AF_INET, IPPROTO_TCP, HELPER_NAME, SIP_PORT, ports[i], i, sip_exp_policy, SIP_EXPECT_MAX, sip_help_tcp, NULL, THIS_MODULE); nf_ct_helper_init(&sip[4 * i + 2], AF_INET6, IPPROTO_UDP, HELPER_NAME, SIP_PORT, ports[i], i, sip_exp_policy, SIP_EXPECT_MAX, sip_help_udp, NULL, THIS_MODULE); nf_ct_helper_init(&sip[4 * i + 3], AF_INET6, IPPROTO_TCP, HELPER_NAME, SIP_PORT, ports[i], i, sip_exp_policy, SIP_EXPECT_MAX, sip_help_tcp, NULL, THIS_MODULE); } ret = nf_conntrack_helpers_register(sip, ports_c * 4);
如下测试配置。
# echo 1 > /proc/sys/net/ipv4/ip_forward # # modprobe nf_nat_sip # # iptables -t nat -A POSTROUTING -s 50.1.1.0/24 -j SNAT --to-source 192.168.1.127 # # iptables -t raw -A PREROUTING -p udp -m udp --dport 5060 -j CT --helper sip
以下处理函数,根据UDP头部长度,确定SIP数据的起始位置和长度。以及更新连接跟踪的超时时间。交由process_sip_msg处理。
static int sip_help_udp(struct sk_buff *skb, unsigned int protoff, struct nf_conn *ct, enum ip_conntrack_info ctinfo) { unsigned int dataoff, datalen; const char *dptr; /* No Data ? */ dataoff = protoff + sizeof(struct udphdr); if (dataoff >= skb->len) return NF_ACCEPT; nf_ct_refresh(ct, skb, sip_timeout * HZ); if (unlikely(skb_linearize(skb))) return NF_DROP; dptr = skb->data + dataoff; datalen = skb->len - dataoff; if (datalen < strlen("SIP/2.0 200")) return NF_ACCEPT; return process_sip_msg(skb, ct, protoff, dataoff, &dptr, &datalen);
对于TCP协议,不处理握手阶段的TCP报文。之后,根据TCP头部长度,确定SIP数据的起始位置和长度。以及更新连接跟踪的超时时间。
static int sip_help_tcp(struct sk_buff *skb, unsigned int protoff, struct nf_conn *ct, enum ip_conntrack_info ctinfo) { struct tcphdr *th, _tcph; const char *dptr, *end; s16 diff, tdiff = 0; int ret = NF_ACCEPT; if (ctinfo != IP_CT_ESTABLISHED && ctinfo != IP_CT_ESTABLISHED_REPLY) return NF_ACCEPT; /* No Data ? */ th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph); if (th == NULL) return NF_ACCEPT; dataoff = protoff + th->doff * 4; if (dataoff >= skb->len) return NF_ACCEPT; nf_ct_refresh(ct, skb, sip_timeout * HZ); if (unlikely(skb_linearize(skb))) return NF_DROP; dptr = skb->data + dataoff; datalen = skb->len - dataoff; if (datalen < strlen("SIP/2.0 200")) return NF_ACCEPT;
首先在报文数据中,搜索Content-Length字符串,其表示随后SDP数据的长度,如下示例报文:
INVITE sip:1001@192.168.1.109 SIP/2.0 Max-Forwards: 20 Via: SIP/2.0/TCP 50.1.1.2:56658;alias;rport;branch=z9hG4bK1365174431 From: <sip:1002@192.168.1.109>;tag=1136953115 Content-Type: application/sdp Content-Length: 496 v=0 o=yate 1642668860 1642668860 IN IP4 50.1.1.2 s=SIP Call c=IN IP4 50.1.1.2 t=0 0 m=audio 29898 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101
将其转换为10进制的长度值clen。随后,搜索SIP协议结束字符串(\r\n\r\n),其为SIP消息结尾,随后就是SDP消息了。
while (1) { if (ct_sip_get_header(ct, dptr, 0, datalen, SIP_HDR_CONTENT_LENGTH, &matchoff, &matchlen) <= 0) break; clen = simple_strtoul(dptr + matchoff, (char **)&end, 10); if (dptr + matchoff == end) break; term = false; for (; end + strlen("\r\n\r\n") <= dptr + datalen; end++) { if (end[0] == '\r' && end[1] == '\n' && end[2] == '\r' && end[3] == '\n') { term = true; break; } } if (!term) break;
SIP数据的结尾加上内容数据长度clen为一个完整的消息长度,注意一个报文中可能包含多个SIP消息。这里同UDP一样,交由process_sip_msg处理单个消息,其中如果修改了报文,参数msglen为新的长度值。
之后,偏移到下一条消息的位置,继续处理。剩余的数据长度datalen,需要减去已经处理的数据长度msglen,加上长度的变化值diff。
end += strlen("\r\n\r\n") + clen; msglen = origlen = end - dptr; if (msglen > datalen) return NF_ACCEPT; ret = process_sip_msg(skb, ct, protoff, dataoff, &dptr, &msglen); /* process_sip_* functions report why this packet is dropped */ if (ret != NF_ACCEPT) break; diff = msglen - origlen; tdiff += diff; dataoff += msglen; dptr += msglen; datalen = datalen + diff - msglen; }
最后,如果nf_nat_sip_hooks有值,例如加载了nf_nat_sip模块,执行其序号调整函数seq_adjust,调整TCP头部的序号值,tdiff为总的长度差值。
if (ret == NF_ACCEPT && ct->status & IPS_NAT_MASK) { const struct nf_nat_sip_hooks *hooks; hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks) hooks->seq_adjust(skb, protoff, tdiff); } return ret;
SIP回复消息统一由字符串("SIP/2.0 ")开头,据此来判定是请求还是回复类型的消息。完成之后由nf_nat_sip模块的hooks->msg函数来处理消息体。
static int process_sip_msg(struct sk_buff *skb, struct nf_conn *ct, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen) { const struct nf_nat_sip_hooks *hooks; int ret; if (strncasecmp(*dptr, "SIP/2.0 ", strlen("SIP/2.0 ")) != 0) ret = process_sip_request(skb, protoff, dataoff, dptr, datalen); else ret = process_sip_response(skb, protoff, dataoff, dptr, datalen); if (ret == NF_ACCEPT && ct->status & IPS_NAT_MASK) { hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks && !hooks->msg(skb, protoff, dataoff, dptr, datalen)) { nf_ct_helper_log(skb, ct, "cannot NAT SIP message"); ret = NF_DROP; } } return ret;
内核处理以下的6类SIP信令消息。
static const struct sip_handler sip_handlers[] = { SIP_HANDLER("INVITE", process_invite_request, process_invite_response), SIP_HANDLER("UPDATE", process_sdp, process_update_response), SIP_HANDLER("ACK", process_sdp, NULL), SIP_HANDLER("PRACK", process_sdp, process_prack_response), SIP_HANDLER("BYE", process_bye_request, NULL), SIP_HANDLER("REGISTER", process_register_request, process_register_response), };
如果Via字段中通告的源端口和连接跟踪记录的源端口不相等,表明请求端在不同的端口接收响应报文,记录下Via中的端口号,回复流量将会用到。此处是为了处理Cisco的一些IP电话,其发送请求使用一个源端口,但是固定在5060端口(通过Via字段中端口号表示)等待回复。
static int process_sip_request(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo); union nf_inet_addr addr; /* Many Cisco IP phones use a high source port for SIP requests, but * listen for the response on port 5060. If we are the local * router for one of these phones, save the port number from the * Via: header so that nf_nat_sip can redirect the responses to * the correct port. */ if (ct_sip_parse_header_uri(ct, *dptr, NULL, *datalen, SIP_HDR_VIA_UDP, NULL, &matchoff, &matchlen, &addr, &port) > 0 && port != ct->tuplehash[dir].tuple.src.u.udp.port && nf_inet_addr_cmp(&addr, &ct->tuplehash[dir].tuple.src.u3)) ct_sip_info->forced_dport = port;
对于SIP请求,开头的格式如:
INVITE sip:1001@192.168.1.109 Max-Forwards: 20 Via: SIP/2.0/TCP 50.1.1.2:56658;alias;rport;branch=z9hG4bK1365174431 From: <sip:1002@192.168.1.109>;tag=1136953115 To: <sip:1001@192.168.1.109> Call-ID: 1096881037@192.168.1.109 CSeq: 48 INVITE
这里使用Method字段(INVITE)与sip_handlers数组成员进行对比,确定是哪一类消息,调用相应消息的处理函数进行处理。另外,查找SIP_HDR_CSEQ字段(如 CSeq: 8 INVITE),获得序号。
for (i = 0; i < ARRAY_SIZE(sip_handlers); i++) { const struct sip_handler *handler; handler = &sip_handlers[i]; if (handler->request == NULL) continue; if (*datalen < handler->len + 2 || strncasecmp(*dptr, handler->method, handler->len)) continue; if ((*dptr)[handler->len] != ' ' || !isalpha((*dptr)[handler->len+1])) continue; if (ct_sip_get_header(ct, *dptr, 0, *datalen, SIP_HDR_CSEQ, &matchoff, &matchlen) <= 0) { nf_ct_helper_log(skb, ct, "cannot parse cseq"); return NF_DROP; } cseq = simple_strtoul(*dptr + matchoff, NULL, 10); if (!cseq && *(*dptr + matchoff) != '0') { nf_ct_helper_log(skb, ct, "cannot get cseq"); return NF_DROP; } return handler->request(skb, protoff, dataoff, dptr, datalen, cseq); } return NF_ACCEPT;
对于SIP响应消息,格式如:
SIP/2.0 200 OK Via: SIP/2.0/TCP 50.1.1.2:56658;received=50.1.1.2;alias;rport=56658;branch=z9hG4bK1365174431 Record-Route: <sip:192.168.1.109;transport=tcp;lr> From: <sip:1002@192.168.1.109>;tag=1136953115 To: <sip:1001@192.168.1.109>;tag=774899820 Call-ID: 1096881037@192.168.1.109 CSeq: 48 INVITE Server: YATE/6.1.0 Contact: <sip:1001@192.168.1.114:63181> Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO Content-Type: application/sdp Content-Length: 506
首先解析响应代码;其次,查找序号字符串,获得序号值。
static int process_sip_response(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); if (*datalen < strlen("SIP/2.0 200")) return NF_ACCEPT; code = simple_strtoul(*dptr + strlen("SIP/2.0 "), NULL, 10); if (!code) { nf_ct_helper_log(skb, ct, "cannot get code"); return NF_DROP; } if (ct_sip_get_header(ct, *dptr, 0, *datalen, SIP_HDR_CSEQ, &matchoff, &matchlen) <= 0) { nf_ct_helper_log(skb, ct, "cannot parse cseq"); return NF_DROP; } cseq = simple_strtoul(*dptr + matchoff, NULL, 10); if (!cseq && *(*dptr + matchoff) != '0') { nf_ct_helper_log(skb, ct, "cannot get cseq"); return NF_DROP; } matchend = matchoff + matchlen + 1;
对于SIP响应消息,通过序号之后的method字段来确定消息类型。在数组sip_handlers中找到相应的处理函数进行处理。
for (i = 0; i < ARRAY_SIZE(sip_handlers); i++) { const struct sip_handler *handler; handler = &sip_handlers[i]; if (handler->response == NULL) continue; if (*datalen < matchend + handler->len || strncasecmp(*dptr + matchend, handler->method, handler->len)) continue; return handler->response(skb, protoff, dataoff, dptr, datalen, cseq, code); } return NF_ACCEPT;
注册请求如下示例:
REGISTER sip:192.168.1.109 SIP/2.0 Contact: <sip:1001@192.168.1.114:5060> Expires: 600 To: <sip:1001@192.168.1.109> Via: SIP/2.0/UDP 192.168.1.114:5060;rport;branch=z9hG4bK912085676 From: <sip:1001@192.168.1.109>;tag=601050461 Call-ID: 2045540049@192.168.1.109 CSeq: 3 REGISTER User-Agent: YATE/6.1.0 Max-Forwards: 70 Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO Content-Length: 0
注册信令之后,在将来某个时刻可能接收到呼叫请求(如:INVITE),函数process_register_request提前为其创建永久expectation,当接收到注册成功响应消息时,将此expectation激活。
首先,取得Expires字段中指定的超时时长(如:600)。
static int process_register_request(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq) { struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo); /* Expected connections can not register again. */ if (ct->status & IPS_EXPECTED) return NF_ACCEPT; /* We must check the expiration time: a value of zero signals the * registrar to release the binding. We'll remove our expectation * when receiving the new bindings in the response, but we don't * want to create new ones. * * The expiration time may be contained in Expires: header, the * Contact: header parameters or the URI parameters. */ if (ct_sip_get_header(ct, *dptr, 0, *datalen, SIP_HDR_EXPIRES, &matchoff, &matchlen) > 0) expires = simple_strtoul(*dptr + matchoff, NULL, 10);
如下,SIP客户端下线时,发送Expires等于零的注册信令,解除绑定。
REGISTER sip:192.168.1.109 SIP/2.0 Contact: <sip:1001@192.168.1.114:5060> Expires: 0 To: <sip:1001@192.168.1.109> Call-ID: 282775376@192.168.1.109 Via: SIP/2.0/UDP 192.168.1.114:5060;rport;branch=z9hG4bK699650197 From: <sip:1001@192.168.1.109>;tag=1019243247 CSeq: 3 REGISTER
查找SIP_HDR_CONTACT字段,解析其中的地址和端口号字段,如果地址字段和连接跟踪的源地址不相同,表明客户端在为第三方注册,不支持这种情况。即注册消息使用一个源地址,而期待响应消息到另外一个地址的情况。
随后,解析Contact字段中的字符串(transport=),如果没有此字段,将使用连接跟踪记录的传输协议。最后,查找Contact中是否包含(expires=)字符串,获取超时值。超时值为零时,表明通知注册服务器取消绑定。此时不需要创建expectation会话。
ret = ct_sip_parse_header_uri(ct, *dptr, NULL, *datalen, SIP_HDR_CONTACT, NULL, &matchoff, &matchlen, &daddr, &port); if (ret < 0) { nf_ct_helper_log(skb, ct, "cannot parse contact"); return NF_DROP; } else if (ret == 0) return NF_ACCEPT; /* We don't support third-party registrations */ if (!nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.src.u3, &daddr)) return NF_ACCEPT; if (ct_sip_parse_transport(ct, *dptr, matchoff + matchlen, *datalen, &proto) == 0) return NF_ACCEPT; if (ct_sip_parse_numerical_param(ct, *dptr, matchoff + matchlen, *datalen, "expires=", NULL, NULL, &expires) < 0) { nf_ct_helper_log(skb, ct, "cannot parse expires"); return NF_DROP; } if (expires == 0) { ret = NF_ACCEPT; goto store_cseq; }
创建nf_conntrack_expect结构,如果设置了sip_direct_signalling(默认为1),源地址使用连接跟踪反方向的源地址,否则使用空地址;源端口为空;目的地址和目的端口号为在以上Contact中解析的地址和端口号。此expectation为之后的服务端主动发起的连接所需要,其帮助服务端绕过防火墙规则(使用related),如下:
# iptables -I FORWARD 1 -m state --state ESTABLISHED,RELATED -j ACCEPT
此expectation为了服务端响应报文创建,具有永久(PERMANENT)和非活动(INACTIVE)属性,当接收到响应报文之后,修改为活动(ACTIVE)。
exp = nf_ct_expect_alloc(ct); if (!exp) { nf_ct_helper_log(skb, ct, "cannot alloc expectation"); return NF_DROP; } saddr = NULL; if (sip_direct_signalling) saddr = &ct->tuplehash[!dir].tuple.src.u3; nf_ct_expect_init(exp, SIP_EXPECT_SIGNALLING, nf_ct_l3num(ct), saddr, &daddr, proto, NULL, &port); exp->timeout.expires = sip_timeout * HZ; exp->helper = nfct_help(ct)->helper; exp->flags = NF_CT_EXPECT_PERMANENT | NF_CT_EXPECT_INACTIVE;
如果nf_nat_ip模块加载,并且此连接需要进行NAT,交由其处理注册信令消息,此处expectation的源地址已经明确为服务器的地址,目的地址和目的端口由expect指向的函数(nf_nat_sip_expect)决定,之后,其将创建related关系。
否则,以上不成立,由函数nf_ct_expect_related创建related关系,将expectation连接到全局链表(nf_ct_expect_hash),以及当前连接跟踪的expectations链表。最后,保存注册消息的序号,以便在接收到响应时,进行匹配判断。
hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks && ct->status & IPS_NAT_MASK) ret = hooks->expect(skb, protoff, dataoff, dptr, datalen, exp, matchoff, matchlen); else { if (nf_ct_expect_related(exp, 0) != 0) { nf_ct_helper_log(skb, ct, "cannot add expectation"); ret = NF_DROP; } else ret = NF_ACCEPT; } nf_ct_expect_put(exp); store_cseq: if (ret == NF_ACCEPT) ct_sip_info->register_cseq = cseq;
在注册响应消息中,如果响应码为1XX,表明为临时消息,不进行处理;对于非2XX的响应码,发生错误,清空expectation。响应码2XX为成功类型消息码。
SIP/2.0 200 OK To: <sip:1001@192.168.1.109>;tag=117fc8cface42e6d25bf31f1b817dec2.09d1 Via: SIP/2.0/UDP 192.168.1.114:5060;received=192.168.1.114;rport=5060;branch=z9hG4bK912085676 From: <sip:1001@192.168.1.109>;tag=601050461 Call-ID: 2045540049@192.168.1.109 CSeq: 3 REGISTER Contact: <sip:1001@192.168.1.114:5060>;expires=600 Server: OpenSIPS (2.4.4 (x86_64/linux)) Content-Length: 0
之后,获取超时值(SIP_HDR_EXPIRES)。以上注册响应示例没有单独的Expires字段,其位于Contact字段。
static int process_register_response(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq, unsigned int code) { struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); unsigned int matchoff, matchlen, coff = 0; unsigned int expires = 0; int in_contact = 0, ret; /* According to RFC 3261, "UAs MUST NOT send a new registration until * they have received a final response from the registrar for the * previous one or the previous REGISTER request has timed out". * * However, some servers fail to detect retransmissions and send late * responses, so we store the sequence number of the last valid * request and compare it here. */ if (ct_sip_info->register_cseq != cseq) return NF_ACCEPT; if (code >= 100 && code <= 199) return NF_ACCEPT; if (code < 200 || code > 299) goto flush; if (ct_sip_get_header(ct, *dptr, 0, *datalen, SIP_HDR_EXPIRES, &matchoff, &matchlen) > 0) expires = simple_strtoul(*dptr + matchoff, NULL, 10);
解析Contact中的地址和端口信息,可能有多个地址端口对,如下:
Contact: <sip:1002@192.168.1.135:53663>;expires=491, <sip:1002@192.168.1.135:36252>;expires=600
如果解析到地址与连接跟踪同方向的目的地址不相同,不处理,这相当于当前客户端在为其它第三方设备做注册(在注册请求消息中同样检测了此种情况,直接放行不处理)。
查找Contact中的下一个地址字段(首次in_contact为零,之后为1,来做区分),解析到Contact末尾结束,跳出循环。接下来尝试查找Contact中的(expires=)字符串,获取超时值。不为零的情况下更新信令的expectation。
while (1) { unsigned int c_expires = expires; ret = ct_sip_parse_header_uri(ct, *dptr, &coff, *datalen, SIP_HDR_CONTACT, &in_contact, &matchoff, &matchlen, &addr, &port); if (ret < 0) { nf_ct_helper_log(skb, ct, "cannot parse contact"); return NF_DROP; } else if (ret == 0) break; /* We don't support third-party registrations */ if (!nf_inet_addr_cmp(&ct->tuplehash[dir].tuple.dst.u3, &addr)) continue; if (ct_sip_parse_transport(ct, *dptr, matchoff + matchlen, *datalen, &proto) == 0) continue; ret = ct_sip_parse_numerical_param(ct, *dptr, matchoff + matchlen, *datalen, "expires=", NULL, NULL, &c_expires); if (ret < 0) { nf_ct_helper_log(skb, ct, "cannot parse expires"); return NF_DROP; } if (c_expires == 0) break;
对于解除绑定的注册信令,在响应报文中没有Expires字段,默认c_expires为零,执行flush_expectations清除之前创建的信令类型的expectation。
SIP/2.0 200 OK To: <sip:1001@192.168.1.109>;tag=117fc8cface42e6d25bf31f1b817dec2.3cdd Call-ID: 282775376@192.168.1.109 Via: SIP/2.0/UDP 192.168.1.114:5060;received=192.168.1.114;rport=5060;branch=z9hG4bK699650197 From: <sip:1001@192.168.1.109>;tag=1019243247 CSeq: 3 REGISTER Server: OpenSIPS (2.4.4 (x86_64/linux)) Content-Length: 0
遍历连接跟踪的expectation链表,根据地址,端口号和协议找到匹配的expectation,将其NF_CT_EXPECT_INACTIVE属性去掉。
if (refresh_signalling_expectation(ct, &addr, proto, port, c_expires)) return NF_ACCEPT; } flush: flush_expectations(ct, false); return NF_ACCEPT;
收到Invite请求信令,如下示例:
INVITE sip:1002@192.168.1.109 SIP/2.0 Max-Forwards: 20 Via: SIP/2.0/UDP 192.168.1.114:5060;rport;branch=z9hG4bK190551602 From: <sip:1001@192.168.1.109>;tag=1003764480 To: <sip:1002@192.168.1.109> Call-ID: 1905338113@192.168.1.109 CSeq: 5 INVITE User-Agent: YATE/6.1.0 Contact: <sip:1001@192.168.1.114:5060> Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO Content-Type: application/sdp Content-Length: 506 v=0 o=yate 1643183047 1643183047 IN IP4 192.168.1.114 s=SIP Call c=IN IP4 192.168.1.114 t=0 0 m=audio 31518 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101 a=rtpmap:0 PCMU/8000
首先清空之前的媒体类型的expectation,之后由process_sdp函数处理。
static int process_invite_request(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); flush_expectations(ct, true); ret = process_sdp(skb, protoff, dataoff, dptr, datalen, cseq); if (ret == NF_ACCEPT) ct_sip_info->invite_cseq = cseq; return ret;
对于Invite响应信令,如果响应码为1XX或者2XX,处理也是主要由process_sdp完成;否则,为错误类型的响应码,清除媒体类型的expectation。
static int process_invite_response(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq, unsigned int code) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); if ((code >= 100 && code <= 199) || (code >= 200 && code <= 299)) return process_sdp(skb, protoff, dataoff, dptr, datalen, cseq); else if (ct_sip_info->invite_cseq == cseq) flush_expectations(ct, true); return NF_ACCEPT;
Update请求消息由函数process_sdp进行处理,Update回复消息由process_update_response处理,最终,也是调用process_sdp进行处理。
static int process_update_response(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq, unsigned int code) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); if ((code >= 100 && code <= 199) || (code >= 200 && code <= 299)) return process_sdp(skb, protoff, dataoff, dptr, datalen, cseq); else if (ct_sip_info->invite_cseq == cseq) flush_expectations(ct, true); return NF_ACCEPT;
临时ACK(PRovisional Acknowledgement)请求消息由函数process_sdp进行处理,PRACK回复消息由process_prack_response处理,最终,也是调用process_sdp进行处理。
static int process_prack_response(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq, unsigned int code) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); struct nf_ct_sip_master *ct_sip_info = nfct_help_data(ct); if ((code >= 100 && code <= 199) || (code >= 200 && code <= 299)) return process_sdp(skb, protoff, dataoff, dptr, datalen, cseq); else if (ct_sip_info->invite_cseq == cseq) flush_expectations(ct, true); return NF_ACCEPT;
收到Bye信令,如下:
BYE sip:1001@192.168.1.114:5060 SIP/2.0 Call-ID: 1905338113@192.168.1.109 From: <sip:1002@192.168.1.109>;tag=1394983491 To: <sip:1001@192.168.1.109>;tag=1003764480 P-RTP-Stat: PS=393,OS=62880,PR=402,OR=64320,PL=0 Via: SIP/2.0/UDP 192.168.1.109:5060;branch=z9hG4bK8444.76f83b16.0 Via: SIP/2.0/UDP 192.168.1.135:60088;received=192.168.1.135;rport=60088;branch=z9hG4bK840227390 CSeq: 62 BYE User-Agent: YATE/6.1.0 Max-Forwards: 69 Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO Content-Length: 0
清空媒体类型的expectation。
static int process_bye_request(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq) { enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); flush_expectations(ct, true); return NF_ACCEPT;
支持三种类型的媒体流,如下:audio、video和image。
static const struct sdp_media_type sdp_media_types[] = { SDP_MEDIA_TYPE("audio ", SIP_EXPECT_AUDIO), SDP_MEDIA_TYPE("video ", SIP_EXPECT_VIDEO), SDP_MEDIA_TYPE("image ", SIP_EXPECT_IMAGE), }; static const struct sdp_media_type *sdp_media_type(const char *dptr, unsigned int matchoff, unsigned int matchlen) { const struct sdp_media_type *t; unsigned int i; for (i = 0; i < ARRAY_SIZE(sdp_media_types); i++) { t = &sdp_media_types[i]; if (matchlen < t->len || strncmp(dptr + matchoff, t->name, t->len)) continue; return t; } return NULL;
连接跟踪解析以下四个SDP头部类型。
static const struct sip_header ct_sdp_hdrs_v4[] = { [SDP_HDR_VERSION] = SDP_HDR("v=", NULL, digits_len), [SDP_HDR_OWNER] = SDP_HDR("o=", "IN IP4 ", sdp_addr_len), [SDP_HDR_CONNECTION] = SDP_HDR("c=", "IN IP4 ", sdp_addr_len), [SDP_HDR_MEDIA] = SDP_HDR("m=", NULL, media_len), }; static const struct sip_header ct_sdp_hdrs_v6[] = { [SDP_HDR_VERSION] = SDP_HDR("v=", NULL, digits_len), [SDP_HDR_OWNER] = SDP_HDR("o=", "IN IP6 ", sdp_addr_len), [SDP_HDR_CONNECTION] = SDP_HDR("c=", "IN IP6 ", sdp_addr_len), [SDP_HDR_MEDIA] = SDP_HDR("m=", NULL, media_len), };
如下INVATE请求信令中的SDP数据,在结尾省略了部分字段:
INVITE sip:1002@192.168.1.109 SIP/2.0 Max-Forwards: 20 Via: SIP/2.0/UDP 192.168.1.114:5060;rport;branch=z9hG4bK190551602 From: <sip:1001@192.168.1.109>;tag=1003764480 To: <sip:1002@192.168.1.109> Call-ID: 1905338113@192.168.1.109 CSeq: 5 INVITE User-Agent: YATE/6.1.0 Contact: <sip:1001@192.168.1.114:5060> Allow: ACK, INVITE, BYE, CANCEL, OPTIONS, INFO Content-Type: application/sdp Content-Length: 506 v=0 o=yate 1643183047 1643183047 IN IP4 192.168.1.114 s=SIP Call c=IN IP4 192.168.1.114 t=0 0 m=audio 31518 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000
首先,搜索SDP_HDR_VERSION,即字符串(v=),找到SDP消息的开始位置。
static int process_sdp(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, unsigned int cseq) { struct nf_conn *ct = nf_ct_get(skb, &ctinfo); union nf_inet_addr caddr, maddr, rtp_addr; const struct nf_nat_sip_hooks *hooks; const struct sdp_media_type *t; int ret = NF_ACCEPT; hooks = rcu_dereference(nf_nat_sip_hooks); /* Find beginning of session description */ if (ct_sip_get_sdp_header(ct, *dptr, 0, *datalen, SDP_HDR_VERSION, SDP_HDR_UNSPEC, &matchoff, &matchlen) <= 0) return NF_ACCEPT; sdpoff = matchoff;
如下SDP的部分数据,接下来解析Connection信息。
v=0 o=yate 1643183047 1643183047 IN IP4 192.168.1.114 s=SIP Call c=IN IP4 192.168.1.114 t=0 0 m=audio 31518 RTP/AVP 0 8 3 11 98 97 102 103 104 105 106 101 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000
在找到(c=)字符串之后,还要继续搜索其后的("IN IP4 ")字符串(最后有空格),定位到地址字符串。如果没有找到(c=)字符串,找到SDP_HDR_MEDIA,即字符串(m=),也要结束搜索。
/* The connection information is contained in the session description * and/or once per media description. The first media description marks * the end of the session description. */ caddr_len = 0; if (ct_sip_parse_sdp_addr(ct, *dptr, sdpoff, *datalen, SDP_HDR_CONNECTION, SDP_HDR_MEDIA, &matchoff, &matchlen, &caddr) > 0) caddr_len = matchlen;
接下来,循环查找以上列出的三种媒体类型(示例报文中只有一种audio类型)。首先找到字符串(m=),将其后的类型字符串转换为数字表示,如audio对应SIP_EXPECT_AUDIO类别。
mediaoff = sdpoff; for (i = 0; i < ARRAY_SIZE(sdp_media_types); ) { if (ct_sip_get_sdp_header(ct, *dptr, mediaoff, *datalen, SDP_HDR_MEDIA, SDP_HDR_UNSPEC, &mediaoff, &medialen) <= 0) break; /* Get media type and port number. A media port value of zero * indicates an inactive stream. */ t = sdp_media_type(*dptr, mediaoff, medialen); if (!t) { mediaoff += medialen; continue; } mediaoff += t->len; medialen -= t->len;
接下来,解析此媒体使用的端口号,合法的端口号位于区间[1024,65535]。查找此媒体流有没有指定地址信息,优先使用此处的地址,其次使用Connection中解析到的地址。
port = simple_strtoul(*dptr + mediaoff, NULL, 10); if (port == 0) continue; if (port < 1024 || port > 65535) { nf_ct_helper_log(skb, ct, "wrong port %u", port); return NF_DROP; } /* The media description overrides the session description. */ maddr_len = 0; if (ct_sip_parse_sdp_addr(ct, *dptr, mediaoff, *datalen, SDP_HDR_CONNECTION, SDP_HDR_MEDIA, &matchoff, &matchlen, &maddr) > 0) { maddr_len = matchlen; memcpy(&rtp_addr, &maddr, sizeof(rtp_addr)); } else if (caddr_len) memcpy(&rtp_addr, &caddr, sizeof(rtp_addr)); else { nf_ct_helper_log(skb, ct, "cannot parse SDP message"); return NF_DROP; }
根据以上解析到的地址和端口,创建RTP和RTCP协议流量的expectation。
ret = set_expected_rtp_rtcp(skb, protoff, dataoff, dptr, datalen, &rtp_addr, htons(port), t->class, mediaoff, medialen); if (ret != NF_ACCEPT) { nf_ct_helper_log(skb, ct, "cannot add expectation for voice"); return ret; }
如果nf_nat_sip模块加载,并且连接跟踪需要执行NAT,由其sdp_addr函数处理(m=)字段中RTP地址的NAT替换,参数rtp_addr为新的转换后地址,其在以上函数set_expected_rtp_rtcp中得到更新。
/* Update media connection address if present */ if (maddr_len && hooks && ct->status & IPS_NAT_MASK) { ret = hooks->sdp_addr(skb, protoff, dataoff, dptr, datalen, mediaoff, SDP_HDR_CONNECTION, SDP_HDR_MEDIA, &rtp_addr); if (ret != NF_ACCEPT) { nf_ct_helper_log(skb, ct, "cannot mangle SDP"); return ret; } } i++; }
函数最后,更新会话信息,替换包括(o=)和(c=)两个字段中的地址信息。
/* Update session connection and owner addresses */ hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks && ct->status & IPS_NAT_MASK) ret = hooks->sdp_session(skb, protoff, dataoff, dptr, datalen, sdpoff, &rtp_addr); return ret;
以下建立媒体类型的期望连接,此连接为未来某个时刻由相反方向的端点发起,所以,其源和目的地址/端口号是与当前连接的源和目的地址/端口号相反的(NAT情况下还可能会不相等)。
如果通告的自身RTP媒体地址(daddr)与报文的发送源地址不相同,在sip_direct_media为真(默认)的情况下,不进行处理。否则,expectation的源地址设置为连接跟踪反方向的源地址。源地址设备将发起RTP连接到此通告的地址和端口,预先为期建立expectation。
注意,只有在sip_direct_media为真,并且当前信令通告的RTP地址与连接跟踪源地址相等时,才会为expectation的源地址赋值,其它情况下源地址为空(任意地址),这是考虑到其它情况下,RTP的源地址可能并不是连接跟踪中反方向的源地址,而是一个外部地址。
static int set_expected_rtp_rtcp(struct sk_buff *skb, unsigned int protoff, unsigned int dataoff, const char **dptr, unsigned int *datalen, union nf_inet_addr *daddr, __be16 port, enum sip_expectation_classes class, unsigned int mediaoff, unsigned int medialen) { struct nf_conntrack_expect *exp, *rtp_exp, *rtcp_exp; enum ip_conntrack_info ctinfo; struct nf_conn *ct = nf_ct_get(skb, &ctinfo); enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo); struct nf_conntrack_tuple tuple; int direct_rtp = 0, skip_expect = 0, ret = NF_DROP; saddr = NULL; if (sip_direct_media) { if (!nf_inet_addr_cmp(daddr, &ct->tuplehash[dir].tuple.src.u3)) return NF_ACCEPT; saddr = &ct->tuplehash[!dir].tuple.src.u3;
如果sip_direct_media为零,并且sip_external_media为真,检查外部RTP地址的路由可达性(sip_direct_media为真时,由于和信令地址相同不需要检查),如果路由存在,并且出口设备和信令的出口设备相同,不做处理。
} else if (sip_external_media) { struct net_device *dev = skb_dst(skb)->dev; struct net *net = dev_net(dev); struct flowi fl; struct dst_entry *dst = NULL; memset(&fl, 0, sizeof(fl)); switch (nf_ct_l3num(ct)) { case NFPROTO_IPV4: fl.u.ip4.daddr = daddr->ip; nf_ip_route(net, &dst, &fl, false); break; case NFPROTO_IPV6: fl.u.ip6.daddr = daddr->in6; nf_ip6_route(net, &dst, &fl, false); break; } /* Don't predict any conntracks when media endpoint is reachable * through the same interface as the signalling peer. */ if (dst) { bool external_media = (dst->dev == dev); dst_release(dst); if (external_media) return NF_ACCEPT; } }
根据五元组tuple,查看命名空间中是否已经存在此expectation,满足如下条件中的一个即跳出循环:
以下部分用于判断两个SIP客户端直接是否可建立直接的RTP流量,已经expectation是否已经存在,避免重复创建。
/* We need to check whether the registration exists before attempting * to register it since we can see the same media description multiple * times on different connections in case multiple endpoints receive * the same call. * RTP optimization: if we find a matching media channel expectation * and both the expectation and this connection are SNATed, we assume * both sides can reach each other directly and use the final * destination address from the expectation. We still need to keep * the NATed expectations for media that might arrive from the * outside, and additionally need to expect the direct RTP stream * in case it passes through us even without NAT. */ memset(&tuple, 0, sizeof(tuple)); if (saddr) tuple.src.u3 = *saddr; tuple.src.l3num = nf_ct_l3num(ct); tuple.dst.protonum = IPPROTO_UDP; tuple.dst.u3 = *daddr; tuple.dst.u.udp.port = port; do { exp = __nf_ct_expect_find(net, nf_ct_zone(ct), &tuple); if (!exp || exp->master == ct || nfct_help(exp->master)->helper != nfct_help(ct)->helper || exp->class != class) break;
运行到这里的条件是,expectation存在,并且其主连接跟踪使用的helper与当前连接是同一个,class也相等(断定由hooks->sdp_media创建)。比如helper使用的都是sip,class都是SIP_EXPECT_AUDIO,唯一的不同是expectation的主连接跟踪和当前连接跟踪不是同一个。这种情况下多个端点(连接)接收到相同的呼叫,expectation已经在第一个信令报文的时候创建。
此时,如果找到的expectation执行了NAT,即目的地址/端口号进行了转换(saved_addr/saved_proto保存NAT之前的原始目的地址/端口号,参见hooks->sdp_media),表明我们的原始tuple(未NAT)和一个NAT的expectation相同。如果我们当前的连接也是需要进行NAT的(IPS_NAT_MASK),将找到的expectation的原始目的地址/端口赋予tuple,继续查找,如果再次找到匹配的expectation,设置skip_expect标志,退出查找。
#if IS_ENABLED(CONFIG_NF_NAT) if (!direct_rtp && (!nf_inet_addr_cmp(&exp->saved_addr, &exp->tuple.dst.u3) || exp->saved_proto.udp.port != exp->tuple.dst.u.udp.port) && ct->status & IPS_NAT_MASK) { *daddr = exp->saved_addr; tuple.dst.u3 = exp->saved_addr; tuple.dst.u.udp.port = exp->saved_proto.udp.port; direct_rtp = 1; } else #endif skip_expect = 1; } while (!skip_expect);
RTP使用偶数端口号,RTCP使用紧邻的下一个奇数端口号。如果以上将direct_rtp设置为真,调用nf_nat_sip模块的sdp_port进行处理,替换报文中的媒体端口号。
base_port = ntohs(tuple.dst.u.udp.port) & ~1; rtp_port = htons(base_port); rtcp_port = htons(base_port + 1); if (direct_rtp) { hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks && !hooks->sdp_port(skb, protoff, dataoff, dptr, datalen, mediaoff, medialen, ntohs(rtp_port))) goto err1; } if (skip_expect) return NF_ACCEPT;
分别为RTP和RTCP创建新的expectation结构,源地址使用的是对端的地址或者空,目的地址/端口使用的是报文中(c=或者m=)中的地址/端口,或者在找到相同expectation时,目的地址/端口使用的是其NAT变换之前保存的地址/端口。
如果当前连接跟踪启用了NAT,并且direct_rtp为零,由函数sdp_media修正RTP和RTCP协议expectation中的目的地址和目的端口号,在启用NAT的情况下,对端看到的目的地址和端口是变化之后的,所以为使对端的RTP/RTCP流量通过,需要调整expectation的目的地址和端口号。
否则,未使能NAT,或者direct_rtp为真的情况下,直接将两个expectation链接到全局链表和连接跟踪上。
rtp_exp = nf_ct_expect_alloc(ct); if (rtp_exp == NULL) goto err1; nf_ct_expect_init(rtp_exp, class, nf_ct_l3num(ct), saddr, daddr, IPPROTO_UDP, NULL, &rtp_port); rtcp_exp = nf_ct_expect_alloc(ct); if (rtcp_exp == NULL) goto err2; nf_ct_expect_init(rtcp_exp, class, nf_ct_l3num(ct), saddr, daddr, IPPROTO_UDP, NULL, &rtcp_port); hooks = rcu_dereference(nf_nat_sip_hooks); if (hooks && ct->status & IPS_NAT_MASK && !direct_rtp) ret = hooks->sdp_media(skb, protoff, dataoff, dptr, datalen, rtp_exp, rtcp_exp, mediaoff, medialen, daddr); else { /* -EALREADY handling works around end-points that send * SDP messages with identical port but different media type, * we pretend expectation was set up. * It also works in the case that SDP messages are sent with * identical expect tuples but for different master conntracks. */ int errp = nf_ct_expect_related(rtp_exp, NF_CT_EXP_F_SKIP_MASTER); if (errp == 0 || errp == -EALREADY) { int errcp = nf_ct_expect_related(rtcp_exp, NF_CT_EXP_F_SKIP_MASTER); if (errcp == 0 || errcp == -EALREADY) ret = NF_ACCEPT; else if (errp == 0) nf_ct_unexpect_related(rtp_exp); } }
如下拓扑,两个客户端50.1.1.2和60.1.1.2,SIP服务器为192.168.1.109,设置sip_direct_media为零:
|--------------------------| | | 60.1.1.2 ----- | 60.1.1.1 | | 192.168.1.135 |-------- 192.168.1.109 50.1.1.2 ----- | 50.1.1.1 | | | |--------------------------|
例如客户端60.1.1.2发送INVITE呼叫60.1.1.2,其SDP中通告的RTP地址为:60.1.1.2:31600,内核建立以下的expectation(地址0.0.0.0匹配所有),开启NAT的话,创建EXP1,经过NAT转换,新的RTP地址为192.168.1.135:32778;关闭NAT的话,创建EXP2:
Proto srcaddr src_port - dstaddr dst_port EXP1: UDP 0.0.0.0 00000 - 192.168.1.135 32778 /* sip_direct_media = 0 */ saved_addr = 60.1.1.2 saved_proto.udp.port = 31600 或者 EXP2: UDP 0.0.0.0 00000 - 60.1.1.2 31600
SIP服务器192.168.1.109接收到INVITE信令之后,发送INVIE到客户端50.1.1.2,其SDP中携带的RTP地址为192.168.1.135:32778(NAT),或者60.1.1.2:31600(非NAT)。当内核接收到此INVITE信令之后,分以下情形处理。
情形1 - direct_rtp=0,skip_expect=0
搜索已有expectation链表,没有找到符合要求的expectation,标志位direct_rtp和skip_expect都为零,由hooks->sdp_media创建expectation。例如,以上60.1.1.2客户端发送的INVITE所创建的expectation。
情形2 - direct_rtp=0,skip_expect=1
接收到SIP服务器发送到客户端50.1.1.2的INVITE信令,根据以下tuple查找expectation,找到了符合要求的expectation,但是expectation没有开启NAT,或者当前连接没有开启NAT。此时,匹配到EXP2,无需再新建expectation,也不需要替换报文中媒体地址和端口。
Tuple1: UDP 0.0.0.0 00000 - 192.168.1.135 32778 /* NAT */ 或者 Tuple2: UDP 0.0.0.0 00000 - 60.1.1.2 31600 /* 非NAT */
情形3 - direct_rtp=1,skip_expect=0
匹配到EXP1,此expectation经过了NAT转换,并且当前连接也开启了NAT。expectation的原始目的地址和端口号保存在saved_addr和saved_proto中。此expectation的主连接为60.1.1.2发送SDP通告了自身的RTP地址60.1.1.2:31600,NAT将60.1.1.2:31600转换为了192.168.1.135:32778。
设置direct_rtp等于1,既然192.168.1.135:32778的最终目的地址为60.1.1.2:31660,那么现在就用EXP1的原始地址和端口,替换tuple中的目的地址和端口,如下,继续查找。
Tuple3: UDP 0.0.0.0 00000 - 60.1.1.2 31600 /* NAT */ Tuple: UDP 192.168.1.114 18600 - 50.1.1.2 30352 saved_addr = x.x.x.x saved_proto.udp.port = nnnnn EXP: UDP 192.168.1.114 18600 - 50.1.1.2 30352 saved_addr = 60.1.1.2 saved_proto.udp.port = 30118 New Tuple: UDP 192.168.1.114 18600 - 60.1.1.2 30118
没有找到符合新Tuple3要求的expectation,skip_expect为零。使用新Tuple3的目的地址和端口,替换报文中的媒体端口(媒体地址在随后的hooks->sdp_addr中替换),即通告最终的地址和端口给客户端50.1.1.2。并且,依据新tuple创建以下expectation。这样,客户端50.1.1.2的RTP流可以直接发送给60.1.1.2。
EXP3: UDP 0.0.0.0 00000 - 60.1.1.2 31600
情形4 - direct_rtp=1,skip_expect=1
使用新Tuple3再次找到了符合要求的expectation。表明需要的expectation都已经创建完成(EXP1和EXP3),不需要创建新的expectation。使用60.1.1.2:31600替换报文中的媒体端口(媒体地址在随后的hooks->sdp_addr中替换)。将RTP地址60.1.1.2:31600直接通告给客户端50.1.1.2,并建立了EXP3,这样,客户端50.1.1.2的RTP流可以直接发送给60.1.1.2。
反之,在INVITE回复消息两次经过NAT设备之后,将建立同样的expectation(使能NAT),使得60.1.1.2的RTP流可直接发送到50.1.1.2。
EXP4: UDP 0.0.0.0 00000 - 192.168.1.135 32798 /* sip_direct_media = 0 */ saved_addr = 50.1.1.2 saved_proto.udp.port = 19188 EXP5: UDP 0.0.0.0 00000 - 50.1.1.2 19188
内核版本 5.10