一、问题
在OnDo服务端和PJSIP客户端配置好IPv6,发现电话机可以向服务器注册成功,但使用话机A拨打话机B时,OnDo服务器返回500 (Server Internal Error) 错误。抓的包如下:
通过仔细对比与IPv4下INVITE的请求,没有发现明显差异。服务器的嫌疑比较大。
二、分析
要分析这个问题,首先需要定位服务器是何时发送500错误的,为此需要跟踪服务器的执行过程。
在这里我们使用WinDBG来调试跟踪,WinDBG是微软提供的在Windows上强大的调试工具,下载页面在这里:
http://msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx
如果页面过期,可以直接在MSDN中搜索 WinDBG 即可找到相关的页面。基本的调试命令这里也不解释了,文档非常详细,网络上相关的资料也非常多。
运行WinDBG,点击 File -> Attach to process ,我们要附加到OnDo Server的进程,然后才能进行调试。那么怎么找到这个进程?如果你对OnDo Server非常熟悉,知道它是一个运行于Java平台上的一个组件,那么你会非常容易找到那个合适的java.exe进程。如果不熟悉的话,我们可以这样做:
首先,打开Windows的控制台,输入这个命令:
wmic process list full
这个命令会详细列出正在运行的每个进程的详细信息,仔细找里面的一个(可以重定向到一个文件中,搜索ondo即可)
CommandLine="C:\Program Files\Java\jre7\bin\java" ... com.brekeke.ondo.sv ... Description=java.exe Handle=2368 Name=java.exe ...
在这里省略了一些大部分信息,只列出了其中几个属性,但对我们已经足够了。从CommandLine属性,我们知道我们没有看错人(进程);现在只需去找PID为2368,名称为java.exe的进程就可以了。
使用WinDBG附加到这个进程上。
然后,我们要设置断点,理想的断点是当OnDo产生500错误的时候(一定有会一个判断语句,从这个语句进入了这个分支),但这似乎并不现实,如果我们知道它如何产生的500错误,就不用如此费力的分析原因了。
所以,我们可以找到它发送500错误的时候。我们知道,在Windows平台上,网络通信最终使用的基本都是WinSock——当然也可以不是,但这种情况并不多见,我们先做这样的假设,OnDo使用的是WinSock,如果后面进行不下去了,我们再回头来分析其他情况,不过谢天谢地,这件事没有发生,我们的假设是正确的。使用WinSock,发送消息的函数并不多,就四个,下面列出了四个函数的声明:
int send( _In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags ); int sendto( _In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags, _In_ const struct sockaddr *to, _In_ int tolen ); int WSASend(...); // 参数省略,详见MSDN int WSASendTo(...); // 参数省略,详见MSDN
更详细的信息可以查阅MSDN,WinSock Functions:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms741394%28v=vs.85%29.aspx
从MSDN我们还可以知道,这些函数都在ws2_32.dll中,因此我们就可以很方便的设置断点了(实际上我只设置了两个,而只用到了一个):
0:018> bp ws2_32!send *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\WINDOWS\system32\WS2_32.dll - 0:018> bp ws2_32!sendto 0:018> bl 0 e 71a24c27 0001 (0001) 0:**** WS2_32!send 1 e 71a22f51 0001 (0001) 0:**** WS2_32!sendto
粗体部分就是我使用的命令。
现在我们可以打电话了。我们设置的断点,会截获所有调用send和sendto的地方,因此在调试过程中,我们可以看到,OnDo首先给客户端回复100 Trying 消息,然后尝试转发INVITE消息(并失败),然后再给客户端回复500 Server Internal Error 消息。这个过程很清晰,我们可以很明确的看到,转发INVITE消息的sendto调用,返回值是-1 (0xFFFFFFFF),也就是SOCKET_ERROR。
这个事情让人很疑惑,我的两个话机是在同一个ONT上的,IP地址是一样的,唯一不同的就是注册的号码和绑定的端口号,sendto显然不会涉及到注册的号码,那么为什么给同一个IP地址的两个端口发送数据会导致两种截然不同的结果呢?我们还记得两部话机的注册都是好的,500错误也可以收到,那么是什么导致这样的差异呢?对此,我们可以详细比较一下转发INVITE和发送100通知(或者500错误)的两次sendto调用的参数。
下图是发送100 Trying 消息时调用 sendto() 的堆栈(从esp寄存器看就可以了)。
下图是转发INVITE 消息时调用 sendto() 的堆栈。
这样看还不够明显。我们知道 sendto() 的 6 个参数,因此逐个对比一下:
参数 |
100 Trying |
INVITE |
Socket |
60 0d 00 00 |
60 0d 00 00 |
Buffer |
44 f1 54 03 |
44 f1 64 03 |
Buffer length |
81 01 00 00 |
89 04 00 00 |
Flags |
00 00 00 00 |
00 00 00 00 |
Socket address |
28 f1 54 03 |
28 f1 64 03 |
Socket address length |
1c 00 00 00 |
1c 00 00 00 |
第二、三、四个参数基本可以忽略。我们看到,两次发送,连套接字使用的都是相同的。似乎INVITE更加没有理由发送失败了。现在唯一不同就是发送的目的地 (struct sockaddr 结构) 了,我们再到那块内存中继续寻找线索。
这个内存在截图中已经体现出来了(就是在发送的buffer前面的28个字节)。这个结构的定义是这样的:
struct in6_addr { union { u_char Byte[16]; u_short Word[8]; } u; }; struct sockaddr_in6 { short sin6_family; // 2 u_short sin6_port; // 2 u_long sin6_flowinfo; // 4 struct in6_addr sin6_addr; // 16 u_long sin6_scope_id; // 4 };
应该明确一下,IPv4和IPv6使用的结构是不同的,这也是sendto() 最后一个参数的用途,我们这里自然只需关心IPv6的结构。为了更加明显的进行对比,请看下表:
成员 |
100 Trying |
INVITE |
sin6_family |
17 00 |
17 00 |
sin6_port |
13 d8 |
13 da |
sin6_flowinfo |
00 00 00 00 |
00 00 00 00 |
sin6_addr |
fe c0 00 00 00 00 00 00 c3 0c ab ff 13 6f 22 1c |
fe c0 00 00 00 00 00 00 c3 0c ab ff 13 6f 22 1c |
sin6_scope_id |
01 00 00 00 |
00 00 00 00 |
其中差异已经用红蓝两种颜色表示出来了。前面我说过,我的两个话机是在同一个ONT上,使用不同的端口注册的,所以他们共享同一个IP。从这里你可以明确的看出两个端口分别是5080(0x13d8)和5082(0x13da),因此这个差异是预料之中的。那么剩下的就只有 sin6_scope_id 了。为什么一个是1,另一个是0呢?其实首先应该问,是这个差异导致的 sendto() 失败进而服务器返回 500 错误吗?答案是肯定的。当我们人为的把这个0改为1的时候,奇迹(其实不是奇迹,而是我们预期的现象)出现了,INVITE发送成功,另一个话机响铃了。现在要问,为什么一个是1,另一个是0呢?我不知道。是的,我不知道。同样的配置,在Windows 7 系统上完全没有问题,而在 Windows XP 上就戏剧性的出现了上面的一幕。在OnDO的配置上,Configuration -> System -> Network,我们只设置了IPv6地址,而没有设置 scope 。
因此,OnDO在打算从这个 interface 发送 INVITE 请求时,使用了某种方法获取 scope ID,而显然它没有获取到(或者获取到了错误的值),后面的事情我们都知道了。因此,假如我们在这里明确指定 scope :
结果就正确了,所有的数据包都正常发送,电话可以通了。友情提示,这里的 scope 可以在命令行输入 ipconfig 查看,请不要猜测。
到这里,问题已经解决了。现在我依然很诧异OnDO在Windows XP和Windows 7系统的表现上的差异究竟来源于何处。
三、结论
归根结底,还是OnDO 服务器的配置问题,至少在 Windows XP 系统上,如果使用IPv6,需要把 scope 同时填写到地址上。这一点在Brekeke官方的Wiki上并没有体现(或许是他隐含的意思?),这就导致了我们兜了一个大圈。