概述
本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,
严禁用于任何商业用途。
msn: yfydz_no1@hotmail.com
来源: http://yfydz.cublog.cn
msn: yfydz_no1@hotmail.com
来源: http://yfydz.cublog.cn
1. 前言
SIP(Session Initiation Protocol)在RFC3261中定义的用于建立会话的文本协议,多用于VoIP等多
媒体应用中,其格式和HTTP类似,先有SIP头定义,然后是具体的数据。
目前linux2.6内核中已经正式将SIP跟踪和NAT处理纳入,说明该模块应该经过足够测试证明可用了。
以下Linux内核代码版本为2.6.19.2。
2. SIP基本信息格式
SIP协议本身只定义应用层数据,对于传输层协议是TCP还是UDP没有限制,只是定义了SIP服务端口是
5060。
以下使用RFC3665中提供的SIP应用实例来描述SIP过程,从中可知道对于NAT设备来说需要修改哪些内
容信息。
2.1 登记过程
Bob SIP Server
| |
| REGISTER F1 |
|------------------------------>|
| 401 Unauthorized F2 |
|<------------------------------|
| REGISTER F3 |
|------------------------------>|
| 200 OK F4 |
|<------------------------------|
| |
| |
| REGISTER F1 |
|------------------------------>|
| 401 Unauthorized F2 |
|<------------------------------|
| REGISTER F3 |
|------------------------------>|
| 200 OK F4 |
|<------------------------------|
| |
Message Details
F1 REGISTER Bob -> SIP Server
REGISTER sips:ss2.biloxi.example.com SIP/2.0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashds7
Max-Forwards: 70
From: Bob <sips:bob@biloxi.example.com>;tag=a73kszlfl
To: Bob <sips:bob@biloxi.example.com>
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 1 REGISTER
Contact: <sips:bob@client.biloxi.example.com>
Content-Length: 0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashds7
Max-Forwards: 70
From: Bob <sips:bob@biloxi.example.com>;tag=a73kszlfl
To: Bob <sips:bob@biloxi.example.com>
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 1 REGISTER
Contact: <sips:bob@client.biloxi.example.com>
Content-Length: 0
F2 401 Unauthorized SIP Server -> Bob
SIP/2.0 401 Unauthorized
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashds7
;received=192.0.2.201
From: Bob <sips:bob@biloxi.example.com>;tag=a73kszlfl
To: Bob <sips:bob@biloxi.example.com>;tag=1410948204
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 1 REGISTER
WWW-Authenticate: Digest realm="atlanta.example.com", qop="auth",
nonce="ea9c8e88df84f1cec4341ae6cbe5a359",
opaque="", stale=FALSE, algorithm=MD5
Content-Length: 0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashds7
;received=192.0.2.201
From: Bob <sips:bob@biloxi.example.com>;tag=a73kszlfl
To: Bob <sips:bob@biloxi.example.com>;tag=1410948204
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 1 REGISTER
WWW-Authenticate: Digest realm="atlanta.example.com", qop="auth",
nonce="ea9c8e88df84f1cec4341ae6cbe5a359",
opaque="", stale=FALSE, algorithm=MD5
Content-Length: 0
F3 REGISTER Bob -> SIP Server
REGISTER sips:ss2.biloxi.example.com SIP/2.0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashd92
Max-Forwards: 70
From: Bob <sips:bob@biloxi.example.com>;tag=ja743ks76zlflH
To: Bob <sips:bob@biloxi.example.com>
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 2 REGISTER
Contact: <sips:bob@client.biloxi.example.com>
Authorization: Digest username="bob", realm="atlanta.example.com"
nonce="ea9c8e88df84f1cec4341ae6cbe5a359", opaque="",
uri="sips:ss2.biloxi.example.com",
response="dfe56131d1958046689d83306477ecc"
Content-Length: 0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashd92
Max-Forwards: 70
From: Bob <sips:bob@biloxi.example.com>;tag=ja743ks76zlflH
To: Bob <sips:bob@biloxi.example.com>
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 2 REGISTER
Contact: <sips:bob@client.biloxi.example.com>
Authorization: Digest username="bob", realm="atlanta.example.com"
nonce="ea9c8e88df84f1cec4341ae6cbe5a359", opaque="",
uri="sips:ss2.biloxi.example.com",
response="dfe56131d1958046689d83306477ecc"
Content-Length: 0
F4 200 OK SIP Server -> Bob
SIP/2.0 200 OK
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashd92
;received=192.0.2.201
From: Bob <sips:bob@biloxi.example.com>;tag=ja743ks76zlflH
To: Bob <sips:bob@biloxi.example.com>;tag=37GkEhwl6
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 2 REGISTER
Contact: <sips:bob@client.biloxi.example.com>;expires=3600
Content-Length: 0
Via: SIP/2.0/TLS client.biloxi.example.com:5061;branch=z9hG4bKnashd92
;received=192.0.2.201
From: Bob <sips:bob@biloxi.example.com>;tag=ja743ks76zlflH
To: Bob <sips:bob@biloxi.example.com>;tag=37GkEhwl6
Call-ID: 1j9FpLxk3uxtm8tn@biloxi.example.com
CSeq: 2 REGISTER
Contact: <sips:bob@client.biloxi.example.com>;expires=3600
Content-Length: 0
由此可见,在“Via:”、“From:”、“To:”、“Call-ID:”、“Contact:”等字段中都有地址表示
的ID,对于大部分机器是没有域名的,只能由IP地址表示,因此NAT设备要能修改这些字段中的值。
2.2 SIP通信传输数据
SIP数据传输时使用SDP(Session Description Protocol, RFC4566)协议来描述数据通道信息:
Alice Bob
| |
| INVITE F1 |
|----------------------->|
| 180 Ringing F2 |
|<-----------------------|
| |
| 200 OK F3 |
|<-----------------------|
| ACK F4 |
|----------------------->|
| Both Way RTP Media |
|<======================>|
| |
| BYE F5 |
|<-----------------------|
| 200 OK F6 |
|----------------------->|
| |
F1 INVITE Alice -> Bob
INVITE sip:bob@biloxi.example.com SIP/2.0
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
Max-Forwards: 70
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:alice@client.atlanta.example.com;transport=tcp>
Content-Type: application/sdp
Content-Length: 151
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
Max-Forwards: 70
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:alice@client.atlanta.example.com;transport=tcp>
Content-Type: application/sdp
Content-Length: 151
v=0
o=alice 2890844526 2890844526 IN IP4 client.atlanta.example.com
s=-
c=IN IP4 192.0.2.101
t=0 0
m=audio 49172 RTP/AVP 0
a=rtpmap:0 PCMU/8000
o=alice 2890844526 2890844526 IN IP4 client.atlanta.example.com
s=-
c=IN IP4 192.0.2.101
t=0 0
m=audio 49172 RTP/AVP 0
a=rtpmap:0 PCMU/8000
F2 180 Ringing Bob -> Alice
SIP/2.0 180 Ringing
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
Content-Length: 0
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
Content-Length: 0
F3 200 OK Bob -> Alice
SIP/2.0 200 OK
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
Content-Type: application/sdp
Content-Length: 147
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bf9
;received=192.0.2.101
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 INVITE
Contact: <sip:bob@client.biloxi.example.com;transport=tcp>
Content-Type: application/sdp
Content-Length: 147
v=0
o=bob 2890844527 2890844527 IN IP4 client.biloxi.example.com
s=-
c=IN IP4 192.0.2.201
t=0 0
m=audio 3456 RTP/AVP 0
a=rtpmap:0 PCMU/8000
o=bob 2890844527 2890844527 IN IP4 client.biloxi.example.com
s=-
c=IN IP4 192.0.2.201
t=0 0
m=audio 3456 RTP/AVP 0
a=rtpmap:0 PCMU/8000
F4 ACK Alice -> Bob
ACK sip:bob@client.biloxi.example.com SIP/2.0
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bd5
Max-Forwards: 70
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 ACK
Content-Length: 0
Via: SIP/2.0/TCP client.atlanta.example.com:5060;branch=z9hG4bK74bd5
Max-Forwards: 70
From: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
To: Bob <sip:bob@biloxi.example.com>;tag=8321234356
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 ACK
Content-Length: 0
/* RTP streams are established between Alice and Bob */
/* Bob Hangs Up with Alice. Note that the CSeq is NOT 2, since
Alice and Bob maintain their own independent CSeq counts.
(The INVITE was request 1 generated by Alice, and the BYE is
request 1 generated by Bob) */
Alice and Bob maintain their own independent CSeq counts.
(The INVITE was request 1 generated by Alice, and the BYE is
request 1 generated by Bob) */
F5 BYE Bob -> Alice
BYE sip:alice@client.atlanta.example.com SIP/2.0
Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
Max-Forwards: 70
From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 BYE
Content-Length: 0
Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
Max-Forwards: 70
From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 BYE
Content-Length: 0
F6 200 OK Alice -> Bob
SIP/2.0 200 OK
Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
;received=192.0.2.201
From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 BYE
Content-Length: 0
Via: SIP/2.0/TCP client.biloxi.example.com:5060;branch=z9hG4bKnashds7
;received=192.0.2.201
From: Bob <sip:bob@biloxi.example.com>;tag=8321234356
To: Alice <sip:alice@atlanta.example.com>;tag=9fxced76sl
Call-ID: 3848276298220188511@atlanta.example.com
CSeq: 1 BYE
Content-Length: 0
可见,在SDP定义数据中,“o=”、“c=”中有地址信息,“m=”中有媒体通信用的端口信息,这些
都需要NAT设备修改,如果修改后SDP数据长度发生变化,则应该修改SIP头中的“Content-Length:”
字段的值。
3. SIP跟踪
SIP跟踪处理文件为net/ipv4/netfilter/ip_conntrack.sip.c, 头文件为
include/linux/netfilter_ipv4/ip_conntrack_sip.h.
3.1 初始化
static int __init init(void)
{
int i, ret;
char *tmpname;
if (ports_c == 0)
ports[ports_c++] = SIP_PORT;
ports[ports_c++] = SIP_PORT;
for (i = 0; i < ports_c; i++) {
// 以下定义SIP的ip_conntrack_helper结构参数
/* Create helper structure */
memset(&sip[i], 0, sizeof(struct ip_conntrack_helper));
// 只处理使用UDP协议的SIP
// 使用UDP协议简化很多处理,如TCP序列号跟踪等
sip[i].tuple.dst.protonum = IPPROTO_UDP;
// 跟踪端口,缺省5060
sip[i].tuple.src.u.udp.port = htons(ports[i]);
// tuple掩码
sip[i].mask.src.u.udp.port = htons(0xFFFF);
sip[i].mask.dst.protonum = 0xFF;
// 最大的并发子连接数为2个
sip[i].max_expected = 2;
// 3分钟的子连接超时
sip[i].timeout = 3 * 60; /* 3 minutes */
sip[i].me = THIS_MODULE;
// 跟踪帮助函数
sip[i].help = sip_help;
// 以下定义SIP的ip_conntrack_helper结构参数
/* Create helper structure */
memset(&sip[i], 0, sizeof(struct ip_conntrack_helper));
// 只处理使用UDP协议的SIP
// 使用UDP协议简化很多处理,如TCP序列号跟踪等
sip[i].tuple.dst.protonum = IPPROTO_UDP;
// 跟踪端口,缺省5060
sip[i].tuple.src.u.udp.port = htons(ports[i]);
// tuple掩码
sip[i].mask.src.u.udp.port = htons(0xFFFF);
sip[i].mask.dst.protonum = 0xFF;
// 最大的并发子连接数为2个
sip[i].max_expected = 2;
// 3分钟的子连接超时
sip[i].timeout = 3 * 60; /* 3 minutes */
sip[i].me = THIS_MODULE;
// 跟踪帮助函数
sip[i].help = sip_help;
// helper的名字
tmpname = &sip_names[i][0];
if (ports[i] == SIP_PORT)
sprintf(tmpname, "sip");
else
sprintf(tmpname, "sip-%d", i);
sip[i].name = tmpname;
tmpname = &sip_names[i][0];
if (ports[i] == SIP_PORT)
sprintf(tmpname, "sip");
else
sprintf(tmpname, "sip-%d", i);
sip[i].name = tmpname;
DEBUGP("port #%d: %d/n", i, ports[i]);
// 登记跟踪函数
ret = ip_conntrack_helper_register(&sip[i]);
if (ret) {
printk("ERROR registering helper for port %d/n",
ports[i]);
fini();
return ret;
}
}
return 0;
}
ret = ip_conntrack_helper_register(&sip[i]);
if (ret) {
printk("ERROR registering helper for port %d/n",
ports[i]);
fini();
return ret;
}
}
return 0;
}
3.2 sip_help
static int sip_help(struct sk_buff **pskb,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
unsigned int dataoff, datalen;
const char *dptr;
int ret = NF_ACCEPT;
int matchoff, matchlen;
__be32 ipaddr;
u_int16_t port;
/* No Data ? */
// dataoff为ip头加UDP头长度
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
// dataoff为ip头加UDP头长度
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
if (dataoff >= (*pskb)->len) {
// dataoff大于等于整个IP包数据长度, 没应用数据
DEBUGP("skb->len = %u/n", (*pskb)->len);
return NF_ACCEPT;
}
// dataoff大于等于整个IP包数据长度, 没应用数据
DEBUGP("skb->len = %u/n", (*pskb)->len);
return NF_ACCEPT;
}
// 更新一下该连接的超时, 用的是sip专门定义的超时值而不是标准的UDP超时(30秒)
// 缺省3600秒
ip_ct_refresh(ct, *pskb, sip_timeout * HZ);
// 缺省3600秒
ip_ct_refresh(ct, *pskb, sip_timeout * HZ);
// 如果这个包是非线性的,不处理,只处理线性包
// 其实可以用skb_make_writable处理一下即可继续解析
if (!skb_is_nonlinear(*pskb))
// 从应用层数据开始解析
dptr = (*pskb)->data + dataoff;
else {
DEBUGP("Copy of skbuff not supported yet./n");
goto out;
}
// 其实可以用skb_make_writable处理一下即可继续解析
if (!skb_is_nonlinear(*pskb))
// 从应用层数据开始解析
dptr = (*pskb)->data + dataoff;
else {
DEBUGP("Copy of skbuff not supported yet./n");
goto out;
}
if (ip_nat_sip_hook) {
// 如果定义了SIP NAT, 修改SIP头中信息
if (!ip_nat_sip_hook(pskb, ctinfo, ct, &dptr)) {
ret = NF_DROP;
goto out;
}
}
// 如果定义了SIP NAT, 修改SIP头中信息
if (!ip_nat_sip_hook(pskb, ctinfo, ct, &dptr)) {
ret = NF_DROP;
goto out;
}
}
/* After this point NAT, could have mangled skb, so
we need to recalculate payload lenght. */
// 数据长度
datalen = (*pskb)->len - dataoff;
we need to recalculate payload lenght. */
// 数据长度
datalen = (*pskb)->len - dataoff;
// 以下重点检查和修改SDP部分的信息
// 合法的最小长度检查
if (datalen < (sizeof("SIP/2.0 200") - 1))
goto out;
if (datalen < (sizeof("SIP/2.0 200") - 1))
goto out;
/* RTP info only in some SDP pkts */
// SDP定义的RTP信息只在处理发起方的INVITE包和相应方的200信息
// 其他的都不处理
if (memcmp(dptr, "INVITE", sizeof("INVITE") - 1) != 0 &&
memcmp(dptr, "SIP/2.0 200", sizeof("SIP/2.0 200") - 1) != 0) {
goto out;
}
/* Get ip and port address from SDP packet. */
// 查找SDP中的"c="信息, matchoff为地址相对起点的偏移
if (ct_sip_get_info(dptr, datalen, &matchoff, &matchlen,
&ct_sip_hdrs[POS_CONNECTION]) > 0) {
// SDP定义的RTP信息只在处理发起方的INVITE包和相应方的200信息
// 其他的都不处理
if (memcmp(dptr, "INVITE", sizeof("INVITE") - 1) != 0 &&
memcmp(dptr, "SIP/2.0 200", sizeof("SIP/2.0 200") - 1) != 0) {
goto out;
}
/* Get ip and port address from SDP packet. */
// 查找SDP中的"c="信息, matchoff为地址相对起点的偏移
if (ct_sip_get_info(dptr, datalen, &matchoff, &matchlen,
&ct_sip_hdrs[POS_CONNECTION]) > 0) {
/* We'll drop only if there are parse problems. */
// 解析"c="中的地址信息, 失败则丢包
if (parse_ipaddr(dptr + matchoff, NULL, &ipaddr,
dptr + datalen) < 0) {
ret = NF_DROP;
goto out;
}
// 查找SDP中的"m="信息, matchoff为端口相对起点的偏移
if (ct_sip_get_info(dptr, datalen, &matchoff, &matchlen,
&ct_sip_hdrs[POS_MEDIA]) > 0) {
// 获取端口
port = simple_strtoul(dptr + matchoff, NULL, 10);
// 端口不可能是特权端口,只能是普通端口
if (port < 1024) {
ret = NF_DROP;
goto out;
}
// 建立期待的子连接信息
ret = set_expected_rtp(pskb, ct, ctinfo,
ipaddr, port, dptr);
}
}
out:
return ret;
}
// 解析"c="中的地址信息, 失败则丢包
if (parse_ipaddr(dptr + matchoff, NULL, &ipaddr,
dptr + datalen) < 0) {
ret = NF_DROP;
goto out;
}
// 查找SDP中的"m="信息, matchoff为端口相对起点的偏移
if (ct_sip_get_info(dptr, datalen, &matchoff, &matchlen,
&ct_sip_hdrs[POS_MEDIA]) > 0) {
// 获取端口
port = simple_strtoul(dptr + matchoff, NULL, 10);
// 端口不可能是特权端口,只能是普通端口
if (port < 1024) {
ret = NF_DROP;
goto out;
}
// 建立期待的子连接信息
ret = set_expected_rtp(pskb, ct, ctinfo,
ipaddr, port, dptr);
}
}
out:
return ret;
}
3.3 ct_sip_get_info
该函数在sip数据中查找指定的模式,获取模式的偏移和长度
/* Returns 0 if not found, -1 error parsing. */
// dptr为缓冲区起点, dlen为缓冲区总长
// matchoff和matchlen作为成功时的返回值, 记录查找模式的偏移是长度信息
// hnfo为要查找的模式信息指针
int ct_sip_get_info(const char *dptr, size_t dlen,
unsigned int *matchoff,
unsigned int *matchlen,
struct sip_header_nfo *hnfo)
{
const char *limit, *aux, *k = dptr;
int shift = 0;
// dptr为缓冲区起点, dlen为缓冲区总长
// matchoff和matchlen作为成功时的返回值, 记录查找模式的偏移是长度信息
// hnfo为要查找的模式信息指针
int ct_sip_get_info(const char *dptr, size_t dlen,
unsigned int *matchoff,
unsigned int *matchlen,
struct sip_header_nfo *hnfo)
{
const char *limit, *aux, *k = dptr;
int shift = 0;
// 查找结束点
limit = dptr + (dlen - hnfo->lnlen);
limit = dptr + (dlen - hnfo->lnlen);
while (dptr <= limit) {
// 线性查找两个模式: lname和sname, lname是全称, sname是缩写名称, 两种都合法
// 注意用的是大小写敏感的strncmp, 似乎用strnicmp更好一些
if ((strncmp(dptr, hnfo->lname, hnfo->lnlen) != 0) &&
(strncmp(dptr, hnfo->sname, hnfo->snlen) != 0)) {
dptr++;
continue;
}
// 找到模式
// 在当前行中查找ln_str标志信息,如"UDP", "sip:"等, 这就是个普通字符串查找函数
aux = ct_sip_search(hnfo->ln_str, dptr, hnfo->ln_strlen,
ct_sip_lnlen(dptr, limit));
if (!aux) {
// 没有标志, 出错
DEBUGP("'%s' not found in '%s'./n", hnfo->ln_str,
hnfo->lname);
return -1;
}
// aux跳过标志长度
aux += hnfo->ln_strlen;
// 线性查找两个模式: lname和sname, lname是全称, sname是缩写名称, 两种都合法
// 注意用的是大小写敏感的strncmp, 似乎用strnicmp更好一些
if ((strncmp(dptr, hnfo->lname, hnfo->lnlen) != 0) &&
(strncmp(dptr, hnfo->sname, hnfo->snlen) != 0)) {
dptr++;
continue;
}
// 找到模式
// 在当前行中查找ln_str标志信息,如"UDP", "sip:"等, 这就是个普通字符串查找函数
aux = ct_sip_search(hnfo->ln_str, dptr, hnfo->ln_strlen,
ct_sip_lnlen(dptr, limit));
if (!aux) {
// 没有标志, 出错
DEBUGP("'%s' not found in '%s'./n", hnfo->ln_str,
hnfo->lname);
return -1;
}
// aux跳过标志长度
aux += hnfo->ln_strlen;
// 计算匹配的模式长度, shift是从aux到实际模式地址的偏移
*matchlen = hnfo->match_len(aux, limit, &shift);
// 如果为匹配长度为0出错
if (!*matchlen)
return -1;
// 模式相对数据头的偏移, 跳过了标志本身
*matchoff = (aux - k) + shift;
*matchlen = hnfo->match_len(aux, limit, &shift);
// 如果为匹配长度为0出错
if (!*matchlen)
return -1;
// 模式相对数据头的偏移, 跳过了标志本身
*matchoff = (aux - k) + shift;
DEBUGP("%s match succeeded! - len: %u/n", hnfo->lname,
*matchlen);
return 1;
}
DEBUGP("%s header not found./n", hnfo->lname);
return 0;
}
*matchlen);
return 1;
}
DEBUGP("%s header not found./n", hnfo->lname);
return 0;
}
可查找的数据模式定义如下:
// 用于查找SIP头中的“Via:”、“Contact:”、“Content-Length:”
// SDP头中的“m=”、“v=”、“o=”、“c=”等
struct sip_header_nfo ct_sip_hdrs[] = {
{ /* Via header */
.lname = "Via:",
.lnlen = sizeof("Via:") - 1,
.sname = "/r/nv:",
.snlen = sizeof("/r/nv:") - 1, /* rfc3261 "/r/n" */
.ln_str = "UDP ",
.ln_strlen = sizeof("UDP ") - 1,
.match_len = epaddr_len,
},
{ /* Contact header */
.lname = "Contact:",
.lnlen = sizeof("Contact:") - 1,
.sname = "/r/nm:",
.snlen = sizeof("/r/nm:") - 1,
.ln_str = "sip:",
.ln_strlen = sizeof("sip:") - 1,
.match_len = skp_epaddr_len
},
{ /* Content length header */
.lname = "Content-Length:",
.lnlen = sizeof("Content-Length:") - 1,
.sname = "/r/nl:",
.snlen = sizeof("/r/nl:") - 1,
.ln_str = ":",
.ln_strlen = sizeof(":") - 1,
.match_len = skp_digits_len
},
{ /* SDP media info */
.lname = "/nm=",
.lnlen = sizeof("/nm=") - 1,
.sname = "/rm=",
.snlen = sizeof("/rm=") - 1,
.ln_str = "audio ",
.ln_strlen = sizeof("audio ") - 1,
.match_len = digits_len
},
{ /* SDP owner address*/
.lname = "/no=",
.lnlen = sizeof("/no=") - 1,
.sname = "/ro=",
.snlen = sizeof("/ro=") - 1,
.ln_str = "IN IP4 ",
.ln_strlen = sizeof("IN IP4 ") - 1,
.match_len = epaddr_len
},
{ /* SDP connection info */
.lname = "/nc=",
.lnlen = sizeof("/nc=") - 1,
.sname = "/rc=",
.snlen = sizeof("/rc=") - 1,
.ln_str = "IN IP4 ",
.ln_strlen = sizeof("IN IP4 ") - 1,
.match_len = epaddr_len
},
{ /* Requests headers */
.lname = "sip:",
.lnlen = sizeof("sip:") - 1,
.sname = "sip:",
.snlen = sizeof("sip:") - 1, /* yes, i know.. ;) */
.ln_str = "@",
.ln_strlen = sizeof("@") - 1,
.match_len = epaddr_len
},
{ /* SDP version header */
.lname = "/nv=",
.lnlen = sizeof("/nv=") - 1,
.sname = "/rv=",
.snlen = sizeof("/rv=") - 1,
.ln_str = "=",
.ln_strlen = sizeof("=") - 1,
.match_len = digits_len
}
};
3.4 建立期待连接信息
static int set_expected_rtp(struct sk_buff **pskb,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo,
__be32 ipaddr, u_int16_t port,
const char *dptr)
{
struct ip_conntrack_expect *exp;
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
int ret;
// 分配expect连接空间
exp = ip_conntrack_expect_alloc(ct);
if (exp == NULL)
return NF_DROP;
// 期待连接的端口地址信息, 解析出的地址端口用于目的方
exp->tuple.src.ip = ct->tuplehash[!dir].tuple.src.ip;
exp->tuple.src.u.udp.port = 0;
exp->tuple.dst.ip = ipaddr;
exp->tuple.dst.u.udp.port = htons(port);
exp->tuple.dst.protonum = IPPROTO_UDP;
// 掩码部分地址
exp->mask.src.ip = htonl(0xFFFFFFFF);
exp->mask.src.u.udp.port = 0;
exp->mask.dst.ip = htonl(0xFFFFFFFF);
exp->mask.dst.u.udp.port = htons(0xFFFF);
exp->mask.dst.protonum = 0xFF;
exp->mask.src.ip = htonl(0xFFFFFFFF);
exp->mask.src.u.udp.port = 0;
exp->mask.dst.ip = htonl(0xFFFFFFFF);
exp->mask.dst.u.udp.port = htons(0xFFFF);
exp->mask.dst.protonum = 0xFF;
exp->expectfn = NULL;
exp->flags = 0;
exp->flags = 0;
if (ip_nat_sdp_hook)
// 如果定义了SDP的NAT处理,修改SDP数据中的信息,并建立期待连接
ret = ip_nat_sdp_hook(pskb, ctinfo, exp, dptr);
else {
// 建立期待连接
if (ip_conntrack_expect_related(exp) != 0)
ret = NF_DROP;
else
ret = NF_ACCEPT;
}
ip_conntrack_expect_put(exp);
// 如果定义了SDP的NAT处理,修改SDP数据中的信息,并建立期待连接
ret = ip_nat_sdp_hook(pskb, ctinfo, exp, dptr);
else {
// 建立期待连接
if (ip_conntrack_expect_related(exp) != 0)
ret = NF_DROP;
else
ret = NF_ACCEPT;
}
ip_conntrack_expect_put(exp);
return ret;
}
}
4. SIP的NAT处理
nat处理函数为net/ipv4/netfilter/ip_nat_sip.c, 包括两个函数, 分别处理SIP和SDP数据
4.1 ip_nat_sip (ip_nat_sip_hook)
// 只修改SIP头中的数据, 不建立期待连接
// 函数返回0表示失败, 非0表示成功
// 最多的情况下需要修改两个字段的信息
static unsigned int ip_nat_sip(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack *ct,
const char **dptr)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
char buffer[sizeof("nnn.nnn.nnn.nnn:nnnnn")];
unsigned int bufflen, dataoff;
__be32 ip;
__be16 port;
// 函数返回0表示失败, 非0表示成功
// 最多的情况下需要修改两个字段的信息
static unsigned int ip_nat_sip(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack *ct,
const char **dptr)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
char buffer[sizeof("nnn.nnn.nnn.nnn:nnnnn")];
unsigned int bufflen, dataoff;
__be32 ip;
__be16 port;
// 重新计算应用层数据的起点
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
// 找到NAT转换后的地址和端口数据, 也就是相反方向的目的地址端口
ip = ct->tuplehash[!dir].tuple.dst.ip;
port = ct->tuplehash[!dir].tuple.dst.u.udp.port;
bufflen = sprintf(buffer, "%u.%u.%u.%u:%u", NIPQUAD(ip), ntohs(port));
ip = ct->tuplehash[!dir].tuple.dst.ip;
port = ct->tuplehash[!dir].tuple.dst.u.udp.port;
bufflen = sprintf(buffer, "%u.%u.%u.%u:%u", NIPQUAD(ip), ntohs(port));
/* short packet ? */
// 异常短包, 返回
// 不过应该提前点操作, 这样就不用计算ip, port和bufflen了
if (((*pskb)->len - dataoff) < (sizeof("SIP/2.0") - 1))
return 0;
// 异常短包, 返回
// 不过应该提前点操作, 这样就不用计算ip, port和bufflen了
if (((*pskb)->len - dataoff) < (sizeof("SIP/2.0") - 1))
return 0;
/* Basic rules: requests and responses. */
if (memcmp(*dptr, "SIP/2.0", sizeof("SIP/2.0") - 1) == 0) {
// 数据以"SIP/2.0"开头, 是SIP回应数据
const char *aux;
if (memcmp(*dptr, "SIP/2.0", sizeof("SIP/2.0") - 1) == 0) {
// 数据以"SIP/2.0"开头, 是SIP回应数据
const char *aux;
// 此处为什么不用dir判断呢?
if ((ctinfo) < IP_CT_IS_REPLY) {
// 正方向数据, 发起方->响应方,修改"Contact: "字段中的地址端口数据, 用buffer中的数据替代
mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen,
&ct_sip_hdrs[POS_CONTACT]);
return 1;
}
if ((ctinfo) < IP_CT_IS_REPLY) {
// 正方向数据, 发起方->响应方,修改"Contact: "字段中的地址端口数据, 用buffer中的数据替代
mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen,
&ct_sip_hdrs[POS_CONTACT]);
return 1;
}
// 反方向数据, 响应方->发起方
// 修改"Via: "字段中的地址端口数据, 用buffer中的数据替代
// 返回0表示失败
if (!mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_VIA]))
return 0;
// 修改"Via: "字段中的地址端口数据, 用buffer中的数据替代
// 返回0表示失败
if (!mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_VIA]))
return 0;
/* This search should ignore case, but later.. */
// 查找"CSeq:"字符串的位置
aux = ct_sip_search("CSeq:", *dptr, sizeof("CSeq:") - 1,
(*pskb)->len - dataoff);
if (!aux)
return 0;
// 查找"CSeq:"字符串的位置
aux = ct_sip_search("CSeq:", *dptr, sizeof("CSeq:") - 1,
(*pskb)->len - dataoff);
if (!aux)
return 0;
// 如果在"CSeq:"字段行中没有"REGISTER",
if (!ct_sip_search("REGISTER", aux, sizeof("REGISTER"),
ct_sip_lnlen(aux, *dptr + (*pskb)->len - dataoff)))
return 1;
if (!ct_sip_search("REGISTER", aux, sizeof("REGISTER"),
ct_sip_lnlen(aux, *dptr + (*pskb)->len - dataoff)))
return 1;
// 修改"Contact: "字段中的地址端口数据, 用buffer中的数据替代
return mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen,
&ct_sip_hdrs[POS_CONTACT]);
}
return mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen,
&ct_sip_hdrs[POS_CONTACT]);
}
// 运行到这里说明数据不是以"SIP/2.0"开头的, 是SIP请求方数据
// 此处为什么不用dir判断呢?
if ((ctinfo) < IP_CT_IS_REPLY) {
// 正方向数据, 发起方->响应方,修改"Via: "字段中的地址端口数据, 用buffer中的数据替代
if (!mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_VIA]))
return 0;
// 此处为什么不用dir判断呢?
if ((ctinfo) < IP_CT_IS_REPLY) {
// 正方向数据, 发起方->响应方,修改"Via: "字段中的地址端口数据, 用buffer中的数据替代
if (!mangle_sip_packet(pskb, ctinfo, ct, dptr,
(*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_VIA]))
return 0;
/* Mangle Contact if exists only. - watch udp_nat_mangle()! */
// 修改"Contact: "字段中的地址端口数据, 用buffer中的数据替代
mangle_sip_packet(pskb, ctinfo, ct, dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_CONTACT]);
return 1;
}
// 修改的是反方向数据
/* This mangle requests headers. */
// 修改"sip:"中的数据
return mangle_sip_packet(pskb, ctinfo, ct, dptr,
ct_sip_lnlen(*dptr,
*dptr + (*pskb)->len - dataoff),
buffer, bufflen, &ct_sip_hdrs[POS_REQ_HEADER]);
}
// 修改"Contact: "字段中的地址端口数据, 用buffer中的数据替代
mangle_sip_packet(pskb, ctinfo, ct, dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_CONTACT]);
return 1;
}
// 修改的是反方向数据
/* This mangle requests headers. */
// 修改"sip:"中的数据
return mangle_sip_packet(pskb, ctinfo, ct, dptr,
ct_sip_lnlen(*dptr,
*dptr + (*pskb)->len - dataoff),
buffer, bufflen, &ct_sip_hdrs[POS_REQ_HEADER]);
}
// 修改SIP字段
// 查找由hnfo定义的数据, 然后用buffer中的数据替代
static unsigned int mangle_sip_packet(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack *ct,
const char **dptr, size_t dlen,
char *buffer, int bufflen,
struct sip_header_nfo *hnfo)
{
unsigned int matchlen, matchoff;
// 查找hnfo定义的数据, 获取偏移地址matchoff和长度matchlen
if (ct_sip_get_info(*dptr, dlen, &matchoff, &matchlen, hnfo) <= 0)
return 0;
if (ct_sip_get_info(*dptr, dlen, &matchoff, &matchlen, hnfo) <= 0)
return 0;
// 修改数据内容, 用buffer内容替代matchoff出的数据
if (!ip_nat_mangle_udp_packet(pskb, ct, ctinfo,
matchoff, matchlen, buffer, bufflen))
return 0;
if (!ip_nat_mangle_udp_packet(pskb, ct, ctinfo,
matchoff, matchlen, buffer, bufflen))
return 0;
/* We need to reload this. Thanks Patrick. */
// 重新定位应用层数据起始地址
*dptr = (*pskb)->data + (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
return 1;
}
// 重新定位应用层数据起始地址
*dptr = (*pskb)->data + (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
return 1;
}
4.2 ip_nat_sdp(ip_nat_sdp_hook)
修改SDP数据, 并根据SDP中的参数建立期待子连接参数,函数返回NF_ACCEPT或NF_DROP
/* So, this packet has hit the connection tracking matching code.
Mangle it, and change the expectation to match the new version. */
static unsigned int ip_nat_sdp(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack_expect *exp,
const char *dptr)
{
struct ip_conntrack *ct = exp->master;
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
__be32 newip;
u_int16_t port;
DEBUGP("ip_nat_sdp():/n");
/* Connection will come from reply */
// NAT修改后的地址
newip = ct->tuplehash[!dir].tuple.dst.ip;
// NAT修改后的地址
newip = ct->tuplehash[!dir].tuple.dst.ip;
// 期待连接的参数
exp->tuple.dst.ip = newip;
exp->saved_proto.udp.port = exp->tuple.dst.u.udp.port;
exp->dir = !dir;
exp->tuple.dst.ip = newip;
exp->saved_proto.udp.port = exp->tuple.dst.u.udp.port;
exp->dir = !dir;
/* When you see the packet, we need to NAT it the same as the
this one. */
// 期待处理函数, 用于建立子连接的nat信息
exp->expectfn = ip_nat_follow_master;
this one. */
// 期待处理函数, 用于建立子连接的nat信息
exp->expectfn = ip_nat_follow_master;
/* Try to get same port: if not, try to change it. */
// 查找一个可用的空闲端口代替原来的端口值
for (port = ntohs(exp->saved_proto.udp.port); port != 0; port++) {
exp->tuple.dst.u.udp.port = htons(port);
if (ip_conntrack_expect_related(exp) == 0)
break;
}
// 没可用端口了, 丢包
if (port == 0)
return NF_DROP;
// 查找一个可用的空闲端口代替原来的端口值
for (port = ntohs(exp->saved_proto.udp.port); port != 0; port++) {
exp->tuple.dst.u.udp.port = htons(port);
if (ip_conntrack_expect_related(exp) == 0)
break;
}
// 没可用端口了, 丢包
if (port == 0)
return NF_DROP;
// 修改SDP数据
if (!mangle_sdp(pskb, ctinfo, ct, newip, port, dptr)) {
ip_conntrack_unexpect_related(exp);
return NF_DROP;
}
return NF_ACCEPT;
}
if (!mangle_sdp(pskb, ctinfo, ct, newip, port, dptr)) {
ip_conntrack_unexpect_related(exp);
return NF_DROP;
}
return NF_ACCEPT;
}
// 修改SDP数据
static unsigned int mangle_sdp(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack *ct,
// 新地址,端口值
__be32 newip, u_int16_t port,
const char *dptr)
{
char buffer[sizeof("nnn.nnn.nnn.nnn")];
unsigned int dataoff, bufflen;
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
/* Mangle owner and contact info. */
// 新地址字符串
bufflen = sprintf(buffer, "%u.%u.%u.%u", NIPQUAD(newip));
// 修改"o="字段中的地址
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_OWNER]))
return 0;
// 修改"c="字段中的地址信息
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_CONNECTION]))
return 0;
// 新地址字符串
bufflen = sprintf(buffer, "%u.%u.%u.%u", NIPQUAD(newip));
// 修改"o="字段中的地址
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_OWNER]))
return 0;
// 修改"c="字段中的地址信息
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_CONNECTION]))
return 0;
/* Mangle media port. */
// 新端口字符串
bufflen = sprintf(buffer, "%u", port);
// 修改"m="字段中的端口信息
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_MEDIA]))
return 0;
// 最后修改"Content-Length: "字段中的内容长度值, 因为修改了上述SDP数据后长度
// 可能会发生变化
return mangle_content_len(pskb, ctinfo, ct, dptr);
}
// 新端口字符串
bufflen = sprintf(buffer, "%u", port);
// 修改"m="字段中的端口信息
if (!mangle_sip_packet(pskb, ctinfo, ct, &dptr, (*pskb)->len - dataoff,
buffer, bufflen, &ct_sip_hdrs[POS_MEDIA]))
return 0;
// 最后修改"Content-Length: "字段中的内容长度值, 因为修改了上述SDP数据后长度
// 可能会发生变化
return mangle_content_len(pskb, ctinfo, ct, dptr);
}
// 修改"Content-Length: "字段中的内容长度值
static int mangle_content_len(struct sk_buff **pskb,
enum ip_conntrack_info ctinfo,
struct ip_conntrack *ct,
const char *dptr)
{
unsigned int dataoff, matchoff, matchlen;
char buffer[sizeof("65536")];
int bufflen;
dataoff = (*pskb)->nh.iph->ihl*4 + sizeof(struct udphdr);
/* Get actual SDP lenght */
// 找"v="字符串位置,这是SDP数据的起始点
if (ct_sip_get_info(dptr, (*pskb)->len - dataoff, &matchoff,
&matchlen, &ct_sip_hdrs[POS_SDP_HEADER]) > 0) {
// 找"v="字符串位置,这是SDP数据的起始点
if (ct_sip_get_info(dptr, (*pskb)->len - dataoff, &matchoff,
&matchlen, &ct_sip_hdrs[POS_SDP_HEADER]) > 0) {
/* since ct_sip_get_info() give us a pointer passing 'v='
we need to add 2 bytes in this count. */
// 目前SDP数据的真实长度, 最后+2是因为要加回"v="这两个字符长度
int c_len = (*pskb)->len - dataoff - matchoff + 2;
we need to add 2 bytes in this count. */
// 目前SDP数据的真实长度, 最后+2是因为要加回"v="这两个字符长度
int c_len = (*pskb)->len - dataoff - matchoff + 2;
/* Now, update SDP lenght */
// 找"Content-Length: "字段位置,获取数据长度
if (ct_sip_get_info(dptr, (*pskb)->len - dataoff, &matchoff,
&matchlen, &ct_sip_hdrs[POS_CONTENT]) > 0) {
// 新长度字符串
bufflen = sprintf(buffer, "%u", c_len);
// 更新长度数据
return ip_nat_mangle_udp_packet(pskb, ct, ctinfo,
matchoff, matchlen,
buffer, bufflen);
}
}
return 0;
}
// 找"Content-Length: "字段位置,获取数据长度
if (ct_sip_get_info(dptr, (*pskb)->len - dataoff, &matchoff,
&matchlen, &ct_sip_hdrs[POS_CONTENT]) > 0) {
// 新长度字符串
bufflen = sprintf(buffer, "%u", c_len);
// 更新长度数据
return ip_nat_mangle_udp_packet(pskb, ct, ctinfo,
matchoff, matchlen,
buffer, bufflen);
}
}
return 0;
}
5. 结论
Linux内核中的SIP跟踪和NAT模块基本上比较完善地解决了SIP处理, 不过只是针对UDP协议的, 如果
是TCP实现的SIP则无效。 在编程中,定义了struct sip_header_nfo结构来描述要查找的各种类型数
据的信息,实现对象化编程,而且使程序更简洁。不过查找数据应该要支持大小写无关方式查找数据
,扫描数据次数也比较多,最多会扫描4次。
最后
以上就是善良音响为你收集整理的LInux下的SIP协议跟踪的全部内容,希望文章能够帮你解决LInux下的SIP协议跟踪所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复