离谱的通话无声问题排查记录
简化网络模型 A 硬件话机 S SIP 服务器/媒体服务器 B 客户手机 A <-SIP/RTP> S <-SIP/RTP> B 问题说明 通话接通后,双方听不到对方的声音。 呼叫流程 S 发起呼叫,A 接听后,bridge B 排查思路 检查S在INVITE A时,SDP里携带的c=行,是否是S的公网IP。检查后无问题 在A上抓包,发现有 S->A的RTP包,也有 A->S 的 RTP 包。 将第二步的抓包文件拿到wireshark中分析。发现S->A方向的包是正常的,能播放出声音;A->S方向虽然有RTP包,但是看波形是一条直线,非常奇怪。 S->A方向的RTP包,在wireshark中能播放出声音,理论上A应该能听到B的声音的。但是用户反馈听不到。 咨询用户,用户是如何接听电话的。用户反馈是通过话机手柄接听的电话 让后让客户使用免提的方式拨打接听电话,发现可以听到声音。 让客户拍照话机手柄插入话机的接口,发现手柄插入的接头插入了耳麦的接口。 让客户将手柄插入正确的手柄接口,问题解决。 延伸思考 这次问题排查,让我想起了之前看过的思科的排除语音质量问题 这篇文章把语音质量问题分为三类: 网络相关(包括网关(GW)和PSTN问题) 电话型号/固件相关 设备(例如头戴式耳机)相关 在步骤上: 步骤1.隔离语音质量问题的第一步是找出确切的用户体验并与他们交谈(无论是面对面还是通过电话),以获得对语音质量问题的准确描述。如果有大量用户报告问题,请与他们的样本(可能是5-10个)交谈,以获得对症状的准确描述。 但是实际情况下,我们有时候迫于形势,往往只从一两个客户那里获得只言片语的信息,然后就开始排查了。客户的只言片语如果带有误导性,那么排查的方向就错了。 步骤2.记录物理位置(例如站点A,2楼)、用户名(用户的电话)、目录号码(DN)、电话型号(例如8865)、电话固件(例如11.5.1)和遇到语音质量问题的电话的IP地址。创建一个按物理位置排序的电子表格。在开始排除故障时创建此电子表格需要30分钟(或更短时间),可节省数小时甚至数天的故障排除时间。 记录信息很重要,但是实际操作中,往往因为时间紧迫,而没有记录这些信息。或者这些信息记录在头脑中,但是排查到最后才发现,自己忘记了某些信息。 步骤3.创建电子表格后,查看电话列表,了解电话有哪些共同之处,以及它们与没有语音质量问题的其他电话有何不同。之后,您可以意识到所有有问题的电话都位于同一栋大楼和同一楼层,您可以意识到有问题的电话连接到最近升级的交换机,或者您可以看到所有有问题的电话都位于特定固件上。 总结 搞VoIP这么久,碰到过很多疑难杂症。排查思路往往都是从软件或者网络策略层面去排查。 很少考虑到终端的固件和硬件设备上。 但是这次排查通话无声的问题,让我印象深刻的是,排查到最后才发现是硬件问题。 从声音的输出输出上,有不同的设备,例如有线耳机、蓝牙耳机、话机手柄、免提等。 排查这类问题,需要多方面考量。需要挖掘客户的实际使用场景,才能定位到真正的问题。 反思 客户的硬件话机,有两个接口。 手柄的接口和耳麦的接口是共用一套接口的。虽然接口上标注的很清楚,但是客户在接线的时候,没有注意到这点。 这要怎么避免呢? 骂客户是傻逼吗? 是不是在设计接口的时候,就应该使用不同形状的接口。 比如手柄接口是圆形,耳麦接口是方形。 这样就能避免这种问题了。 或者说,接口是通用的。无论是手柄还是耳麦,插上都能用呢。 还有一种方案,就是只保留一个接口,手柄和耳麦共用一套接口。两者是互斥关系。
DNS域名解析对MicroSIP注册的影响分析
microSIP DNS域名解析影响分析 MicroSIP是一款流行的windows VoIP客户端,它允许用户通过互联网进行语音和视频通话。在使用MicroSIP时,DNS域名解析是确保正确连接到服务器的重要步骤之一。 DNS域名解析是将人类可读的域名(如www.example.com)转换为机器可读的IP地址的过程。这个过程通常由用户的网络服务提供商(ISP)或本地计算机上的DNS缓存完成。 域名解析可以将一个域名解析为多个IP地址,例如:[ip1, ip2, ip3], 但是每次解析返回的顺序往往是不确定的,这取决于DNS服务器的配置和负载均衡策略。 例如第一次解析的结果可能是[ip1, ip2, ip3],而第二次解析的结果可能是[ip2, ip3, ip1]。 MicroSIP客户端在注册时,会尝试连接到这些IP地址中的第一个。 当分机使用TCP方式注册时,如果其第一个IP地址无法连接,它会继续尝试下一个IP地址,直到成功或所有IP地址都失败为止 如果分机使用UDP方式注册,它只会用第一个IP地址尝试注册,如果失败,还是继续尝试第一个IP。 这种行为似乎有点傻,为什么一直要尝试第一个IP地址,而不是尝试下一个呢? MicroSIP底层用了pjsip库,而pjsip只有在用tcp/tls注册时, 才会自动尝试下一个IP。pjsip官网也给出了具体的解决方案,就是应用层主动调用 pjsua_acc_modify() 函数,手动修改账号配置,然后重新注册。 但是microSIP并没有这么做,而是直接用一个固定的IP地址去尝试注册。 Our DNS SRV failover support is only limited to TCP (or TLS) connect failure, which in this case pjsip will automatically retries the next server. https://docs.pjsip.org/en/latest/specific-guides/sip/dns_failover.html 所以,总体上说。在使用域名注册的情况下,当前的注册和上一次的注册可能发往不同的SIP服务器。 考虑如下场景: # t1 dns解析结果 ip1, ip2, ip3 REGISTER sip:ip1:5060 SIP/2.0 200 Ok # t2 dns解析结果 ip3, ip1, ip2 REGISTER sip:ip3:5060 SIP/2.0 200 ok 一般分机都在内网环境,出口的公网IP是不变的,但是t1和t2的注册由于目标IP不同,所以出口的NAT映射的端口也是不同的。 ...
读到无法解析的TCP包后, kamailio如何处理?
1int receive_msg(char *buf, unsigned int len, receive_info_t *rcv_info) { 2 if(parse_msg(buf, len, msg) != 0) { 3 errsipmsg = 1; 4 evp.data = (void *)msg; 5 6 // note: 这里尝试查找并执行nosip模块的 event_route[nosip:msg]事件路由 7 // 一般情况下,如果没有找到,那么ret的值是-1 8 // 那么这里的if内部不会执行 9 if((ret = sr_event_exec(SREV_RCV_NOSIP, &evp)) < NONSIP_MSG_DROP) { 10 LM_DBG("attempt of nonsip message processing failed\n"); 11 } else if(ret == NONSIP_MSG_DROP) { 12 // 这里也不会执行 13 LM_DBG("nonsip message processing completed\n"); 14 goto end; 15 } 16 } 17 18 // 由于在上面的判断里errsipmsg被设置成1,所以这里的if条件成立 19 if(errsipmsg == 1) { 20 // 打印报错信息,并执行核心错误处理 21 LOG(cfg_get(core, core_cfg, sip_parser_log), 22 "core parsing of SIP message failed (%s:%d/%d)\n", 23 ip_addr2a(&msg->rcv.src_ip), (int)msg->rcv.src_port, 24 (int)msg->rcv.proto); 25 sr_core_ert_run(msg, SR_CORE_ERT_RECEIVE_PARSE_ERROR); 26 27 // 跳转到error02标签,执行后续的清理工作 28 goto error02; 29 } 30 31// 跳转到error02标签,执行后续的清理工作 32error02: 33 free_sip_msg(msg); 34 pkg_free(msg); 35error00: 36 ksr_msg_env_reset(); 37 /* reset log prefix */ 38 log_prefix_set(NULL); 39 40 // 返回-1,表示出错 41 return -1; 42} 如果调用receive_msg返回负数,那么从调用栈向上查找receive_tcp_msg函数也会返回负数 int receive_tcp_msg(char *tcpbuf, unsigned int len,struct receive_info *rcv_info, struct tcp_connection *con) receive_tcp_msg函数返回负数,那么向上查找,tcp_read_req也会返回负数 int tcp_read_req(struct tcp_connection *con, int *bytes_read,rd_conn_flags_t *read_flags) tcp_read_req返回负数 inline static int handle_io(struct fd_map *fm, short events, int idx)在这个函数内部 if(unlikely(bytes < 0)) { LOG(cfg_get(core, core_cfg, corelog), "ERROR: tcp_read_req: error reading - c: %p r: %p (%d)\n", con, req, bytes); resp = CONN_ERROR; goto end_req; } resp = tcp_read_req(con, &n, &read_flags); if(unlikely(resp < 0)) { /* some error occurred, but on the new fd, not on the tcp * main fd, so keep the ret value */ if(unlikely(resp != CONN_EOF)) con->state = S_CONN_BAD; release_tcpconn(con, resp, tcpmain_sock); break; } 整个调用链条是这样的: handle_io -> tcp_read_req -> receive_tcp_msg -> receive_msg ...
Default
Hot Reload OpenSIPS vs Kamailio
用过nginx的都知道,改了nginx的配置文件,只需要执行nginx -s reload就可以让改动生效,而不用重启整个服务。 在kamailio和opensips中,也有类似的热重载功能。 在kamailio中,如果要热重载配置文件,只需要执行kamcmd app_jsdt.reload即可。 在opensips中,如果要热重载配置文件,只需要执行opensips-cli -x mi reload_routes即可。 理想很丰满,现实很骨感。 如果要只是修改路由模块,两者都可以做热重载。 如果要动态的新增一些模块,那就必须要重启。 Kamailio的实现方案 必须要用KEMI框架: cfg + [lua|js|python|ruby] 用这个框架,你写的路由脚本将包括两个部分 核心模块,全局配置,模块加载,模块配置,事件路由这部分内容还是写在cfg文件中。 请求路由、响应路由、分支路由、失败路由等这部分内容可以用lua、js、 python等来写。 在热重载的时候,实际上只有外部脚本会被重新加载,而核心模块是不会被重新加载的。 因为部分路由用其他脚本编写,必然涉及到性能比较。官方给出的结论是,在同等条件下,lua的性能是最好的。 https://www.kamailio.org/wikidocs/devel/config-engines/#interpreters-performances 但是实际生产环境如何,还不好说。 另外一点,就是并不是所有模块都实现了KEMI框架的接口,可能存在风险。 OpenSIPS的实现方案 OpenSIPS在3.0版本首次引入了热重载路由脚本的能力,脚本还是cfg,没有引入其他语言。 https://www.opensips.org/Documentation/Interface-CoreMI-3-0#toc8 结论 总体而言,目的是相同的,都是为了热重载路由。 kamailio的方案看似灵活,实则复杂。如果cfg本身就能做热重载,那么就没必要引入其他语言。 我更倾向使用OpenSIPS的方案 参考 https://blog.opensips.org/2019/04/04/no-downtime-for-opensips-3-0-restarts/
开发学习笔记
架构图 kamailio 1.x版本 kamailio 3.x版本。相比于1.x版本,两个核心模块移动到外部模块。 核心模块 The core includes: memory manager SIP message parser locking system DNS and transport layer management (UDP, TCP, TLS, SCTP) configuration file parser and interpreter stateless forwarding pseudo-variables and transformations engines RPC control interface API timer API The internal libraries include: some components from old Kamailio v1.5.x core database abstraction layers (DB API v1 and v2) management interface (MI) API statistics engine SIP消息处理 请求处理 响应处理。 这里可以看到,是先执行响应路由,再执行失败路由。 ...
kamailio 继承 HEP Server
目标 kamailio将所收到的SIP消息封装成HEP格式,然后已UDP发送给Hep Server 环境说明 kamailio版本 5.6 Hep server地址 192.168.0.100 kamailio脚本 listen 参考文档 https://www.kamailio.org/docs/modules/5.6.x/modules/siptrace.html https://github.com/sipcapture/homer/discussions/619 https://github.com/sipcapture/homer/wiki/Examples%3A-Kamailio
HEP 协议学习笔记
HEP简介 HEP协议目前叫做EEP(Extensible Encapsulation Protocol), 那之前的缩写HEP的H,我只能推测为homer。 这个协议的主要功能是对VoIP连路上的数据包,例如SIP进行封装,方便后续分析SIP信令图。 目前这个协议已经升级到V3版本,在这个pdfHEP3_Network_Protocol_Specification_REV_36中有详细的介绍。 今天我们主要看这个协议的V3版本的协议是如何实现的。 包头 packet-beta title HEP Packet 0-3: "版本号" 4-5: "总长度" 6-15: "数据段(变长)" HEPv3的包头是6个字节,主要分为三个部分 版本号:固定4个字节长度,是HEP3 总长度:固定2个字节长度,是包的总长度,这个总长度包括了包头的六个字节。所以HEP包的大小范围一般是6-65535之间。 数据段:数据段的长度不固定 注意事项:假如从传输层读到1000个字节的数据,在解析前6个字节是,发现总长度(total length)的子段是1200,那就说明本次读到的数据还不是一个完整的 数据段解析 数据段由固定6字节长度的头部和变长的payload部分。 packet-beta title HEP Packet 0-1: "vendor ID" 2-3: "type ID" 4-5: "length" 6-15: "payload" vendor ID: 固定2字节长度, 其实意义不大, type ID: 固定2字节长度,这个子段很重要,决定了payload的类型。可以理解为是一个对象的key, 然后把payload理解为value length: 固定2字节长度,范围是0-65535,这个字段是也是整个数据段的长度,也就是包括了6个字节的段头 payload: 长度是length的长度-6,表示数据长度 hep协议有个type ID的映射表 chunk type ID 类型 描述 0x0001 uint8 IP类型,0x02=IPv4, 0x0a=IPv6 0x0002 uint8 协议类型 0x06=TCP, 0x11=UDP, 可以参考IP协议号列表 其他的字段还有很多,可以参考HEP3_Network_Protocol_Specification_REV_36 ...
写日记的正经人 - 季羡林
在电影《邪不压正》中,关于写日记的经典对白是 "正经人谁写日记啊?" "是啊" "你写日记吗?" "我不写。你写日记吗?" "谁能把心里话写日记里?" "写出来的那能叫心里话?" "下贱!" 给我灌输一种感觉写日记的人往往是虚伪的。 然而,最近我看了季羡林先生两本书《牛棚杂忆》和《留得十年》,我不得不对季先生佩服的五体投地,眼角朦胧,鼻头发酸。 我在德国十年的日记,一天不缺,恐怕有一两百万字。–《留德十年》 做一件小事,坚持10年。 这是什么样动机,是因为毅力吗? 如果说做一件事让自己感觉很难,不想做,但是做了很长时间,可以称之为毅力。 我觉得除了毅力,更多的一种喜欢,一种习惯。一种把思想自然流露指尖,落笔成文的自然天性。 我不记得以前上学时,有没有度过季先生的。然而季先生的大名,还是略有耳闻的。 但是除了季先生的大名的,其他的事情,几乎不了解。 看了两本书书后,我不禁对季先生生有有一种心驰神往。 我不知道季先生原来是清朝末年的人。经历过清末、民国、二战、解放、文化大革命… 我不知道季先生原来去德国留过学,而且是是坐火车去的。 德国哥廷根的风景描写,让我脑海里呈现一种宫崎骏动画般画面。 我不知道原来季先生在德国还经历过二战。 我不知道季先生原来还在文化大革命中,数次被打倒。 季先生的文风朴实,感情自然流露。你能从他的文字中看到一个普通人懦弱与勇气、迷茫与坚持、欲望和克制。 我喜欢季先生的文风,也喜欢季先生的为人。 我觉得,归根结底,我喜欢真实,厌恶粉饰。
RFC768 UDP包学习笔记
包格式 UDP包头工占用8个字节, 其中源端口和目标端口各占2个字节,长度占2个字节,校验和占2个字节。 0 7 8 15 16 23 24 31 +--------+--------+--------+--------+ | Source | Destination | | Port | Port | +--------+--------+--------+--------+ | | | | Length | Checksum | +--------+--------+--------+--------+ | | data octets ... +---------------- ... User Datagram Header Format 字段 源端口,可选字段,默认为0,如果是有意义的其他端口,表示后续响应可以送回到该端口 目标端口,必须字段,用来关联目标主机上进程监听的端口 长度,长度是整个UDP包的长度,也就是包头 + 包的数据,包头固定8字节,那么length的最小长度就是8 校验和,用来验证传输过程中是否发生了错误。校验和的计算结果与源IP、目标IP、UDP的包长有关。 如果校验和失败,那么消息会被丢弃。 有些校验和计算的工作会被放置在网卡上,从而减少CPU的负载。当然如果在网卡上因为校验和的问题被网卡丢弃,上层应用是收不到不到UDP包的。但是用tcpdump可以抓到这种校验和有问题的包。 思考1: 为什么源端口可以设置为0? 答:有些UDP包,是不需要响应的,只需要发送出消息。 参考 https://datatracker.ietf.org/doc/html/rfc768 https://wdd.js.org/opensips/ch7/big-udp-msg/ https://wdd.js.org/network/udp-checksum-offload/