Eddie Wang

当你在穿山越岭的另一边, 我在孤独的路上没有尽头

DTLS协商失败导致无声问题

1. 简介 DTLS(Datagram Transport Layer Security,数据报传输层安全协议)是一种为基于数据报的应用(如UDP)提供安全通信的协议。它是TLS(传输层安全协议)的扩展,专门设计用于不可靠的传输协议(如UDP),以实现数据加密、身份认证和消息完整性保护。DTLS常用于VoIP、视频会议、WebRTC等实时通信场景。 DTLS在WebRTC音视频中是强制必须使用的,否则媒体协商阶段就会失败。 使用DTLS后,就算中间人从网络中抓包,抓到了RTP流,在播放RTP流时,里面也全是噪声,无法播放的。 今天要讲的是就是DTLS协商失败导致电话即使接通,也无声的问题。 2. 网络结构说明 UAS: sip server, OpenSIPS + RTPEngine组成 UAC: WebRTC 分机 F1. UAS ----> UAC: INVITE (SDP) F2. UAS <---- UAC: 180 F3. UAS <---- UAC: 200 Ok (SDP) F4. UAS ----> UAC: ACK 从SIP信令上看,被叫应答后,UAS和UAC之间的双向媒体流应该建立起来,但是实际上却无声。 3. ICE/STUN/DTLS ICE: WebRTC中的ICE(Interactive Connectivity Establishment,交互式连接建立)是一种网络协议,用于帮助WebRTC客户端在不同网络环境下建立点对点(P2P)连接。ICE的主要作用是解决NAT穿透和防火墙问题,使两个终端能够找到最佳的通信路径。 STUN: WebRTC中的STUN(Session Traversal Utilities for NAT,NAT会话穿越实用工具)是一种网络协议,主要用于帮助客户端发现自己的公网IP地址和端口。由于很多设备处于NAT(网络地址转换)或防火墙之后,直接通信会遇到障碍,STUN可以让客户端知道外部网络如何访问自己 在分机应答后,并在WebRTC发送本地媒体流前,还需要两个步骤协商完成UAC才会发送语音流 3.1 STUN协商 UAC收到来自UAS的SDP, 里面一般有如下内容 m=audio 54322 UDP/TLS/RTP/SAVPF 111 0 8 a=ice-ufrag:abcd a=ice-pwd:1234567890abcdef a=ice-options:trickle a=fingerprint:sha-256 12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF a=setup:actpass a=candidate:1 1 udp 2130706431 1.2.3.4 54322 typ host STUN是一个请求响应模型的协议。UAC将会向a=candidate里的1.2.3.4:54322 发送UDP消息 ...

2025-06-24 21:06:11 · 2 min · Eddie Wang

RTPEngine mr13版本, 特殊的fmtp参数会导致某些语音编码被移除

场景说明 如下图所示 Offer阶段,F1, F2 步骤里PCMU编码有个fmtp参数abc=no, 这个参数可能只对呼叫发起方有意义,对被叫方来说,只会被忽略。 Answer阶段, 例如被叫是个FreeSWITCH, 它不认识abc=no, 就直接忽略,然后应答编码是PCMU。但是rtpengine认为不带abc=no的参数,就认为这个PCMU的编码是不可能被选中的,然后就直接删掉了PCMU编码, 只保留了一个101编码 由于主叫收到101的编码,而没有语音的编码,所以主叫收到200 Ok后立马就发送了BYE // F1: send offer to rtpegnine m=audio 2000 RTP/AVP 0 8 101 a=rtpmap:0 PCMU/8000 a=fmtp:0 abc=no a=rtpmap:8 PCMA/8000 a=fmtp:8 abc=no a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 // F2: receive offer from rtpengine m=audio PORT RTP/AVP 0 8 101 c=IN IP4 203.0.113.1 a=rtpmap:0 PCMU/8000 a=fmtp:0 abc=no a=rtpmap:8 PCMA/8000 a=fmtp:8 abc=no a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 // F3: send answer to rtepngine m=audio 2002 RTP/AVP 0 101 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 // F4: expect receive answer from rtpengine m=audio PORT RTP/AVP 0 101 c=IN IP4 203.0.113.1 a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-15 问题原因 刚开始我以为是, https://github.com/sipwise/rtpengine/commit/9c00f30 这次commit引起的问题,我尝试注释代码,然后进行测试,发现可以解决问题。 ...

2025-06-18 20:04:52 · 1 min · Eddie Wang

VoIP Server 高可用架构设计

简介 VoIP高可用包含很多方面,主要包括: 接入层高可用:怎么保证某个接入点出现问题,不会影响到整个平台不可用? 核心层高可用:怎么保证路由选择的高可用,某个网关不可用,继续尝试其他网关? 存储层高可用:怎么保证不会丢失录音文件,怎么保证录音文件的完整性? 数据库层高可用:怎么保证注册信息能够在集群中同步? 接入层高可用 最简单的场景, 只有一个fs, 无论是ua还是gw都接入到fs1上 ua1 ----> fs1 ua2 ----> fs1 gw1 ----> fs1 gw2 ----> fs1 如果要实现高可用,最简单的做法就是再加一个fs2。但是加了fs2之后,ua和gw的接入策略就需要改变了。 为了保持客户端的接入策略不变,最简单的做法加上一个负载均衡器,然后由负载均衡器来做信令转发。 ua1 ----> op1 ----> fs1 ua2 ----> | ----> fs2 gw1 ----> | gw2 ----> | op1是单节点的,也会存在单点故障问题,因此引入op2。 ua1 ----> op1 ----> fs1 ua2 ----> op2 ----> fs2 gw1 ----> | gw2 ----> | 引入op2之后,有两个方案来保证高可用: 方案1, 主备: op1和op2用VIP来对外提供服务,op1和op2的VIP是一样的。实际只有一个在工作,另外一个处于备份状态。 一旦op1出现问题,op2接管VIP对外提供服务。 - 优点: 简单 - 缺点: - 兼容性:不是所有云平台都支持VIP - 成本:需要额外的硬件成本,而且还是闲置状态 - 负载:op1和op2都需要对外提供服务,但是实际上只有一个在工作,一个节点的处理能力有限,总会达到瓶颈 ...

2025-05-27 20:10:22 · 1 min · Eddie Wang

离谱的通话无声问题排查记录

简化网络模型 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这么久,碰到过很多疑难杂症。排查思路往往都是从软件或者网络策略层面去排查。 很少考虑到终端的固件和硬件设备上。 但是这次排查通话无声的问题,让我印象深刻的是,排查到最后才发现是硬件问题。 从声音的输出输出上,有不同的设备,例如有线耳机、蓝牙耳机、话机手柄、免提等。 排查这类问题,需要多方面考量。需要挖掘客户的实际使用场景,才能定位到真正的问题。 反思 客户的硬件话机,有两个接口。 手柄的接口和耳麦的接口是共用一套接口的。虽然接口上标注的很清楚,但是客户在接线的时候,没有注意到这点。 这要怎么避免呢? 骂客户是傻逼吗? 是不是在设计接口的时候,就应该使用不同形状的接口。 比如手柄接口是圆形,耳麦接口是方形。 这样就能避免这种问题了。 或者说,接口是通用的。无论是手柄还是耳麦,插上都能用呢。 还有一种方案,就是只保留一个接口,手柄和耳麦共用一套接口。两者是互斥关系。

2025-05-07 22:45:26 · 1 min · Eddie Wang

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映射的端口也是不同的。 ...

2025-04-30 17:36:58 · 1 min · Eddie Wang

读到无法解析的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 ...

2025-04-18 23:05:15 · 2 min · Eddie Wang

Default

2025-03-29 15:49:28 · 0 min · Eddie Wang

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/

2025-03-29 13:49:38 · 1 min · Eddie Wang

开发学习笔记

架构图 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消息处理 请求处理 响应处理。 这里可以看到,是先执行响应路由,再执行失败路由。 ...

2025-02-21 22:17:35 · 7 min · Eddie Wang

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

2025-02-17 22:57:11 · 1 min · Eddie Wang