
简介面向SIP协议与VoIP开发者围绕eXosip2-3.6.0库实现客户端注册到服务器的完整代码包重点演示MD5摘要认证过程。包内共8个文件以C源程序为主包含SipServer与SipClient两个模块并配套Visual Studio工程文件dsp/dsw与7z依赖库eXosip2、md5免去二次收集依赖的繁琐整体仅222KB轻量易部署。已有1567人学习。通过示例代码可直观理解REGISTER请求构造、401质询响应、MD5凭据设置及注册有效期刷新机制掌握eXosip初始化、发送请求、处理响应与重试判断的完整链路可直接将工程框架复用到SIP客户端开发中为后续实现可靠VoIP音视频通信奠定基础。1. 注册到服务器为什么看着简单却在认证阶段反复翻车被凌晨两点的电话叫醒十有八九是监控平台的SIP接入出问题了。设备侧SIP注册状态刚亮绿灯平台侧列表里却看不到设备上线或者上线几分钟又掉线来回拉扯。所谓SIP注册流程就是终端话机、视频设备、软交换通过REGISTER方法向SIP服务器声明自己的地址和URI最终把自己注册到服务器上并维持在线状态。流程本身不长但认证方式、过期策略、NAT穿透三件事搅在一起让看似简单的注册成了SIP接入链路上翻车率最高的环节。这篇笔记按我自己的排错顺序展开先拆注册信令再调服务器参数然后讲海康平台这类严格校验方的对接配置最后落到抓包验证。适合正在做VoIP设备接入、监控平台联网对接的从业者新手能照着走熟手能核对参数边界。2. 先拆注册流程REGISTER信令的生命周期与认证头域2.1 注册不是“发一个包”而是一轮挑战-应答握手很多刚上手的人以为SIP注册就是发一条UDP消息过去服务器回200就完事。实际抓包会看到几乎所有服务器都会先回401或407迫使终端重新发第二封REGISTER。完整的注册序列长这样终端 - 服务器: REGISTER sip:sip.example.com SIP/2.0 (不带任何认证信息, Expires: 3600, Contact: 终端实际地址) 服务器 - 终端: 401 Unauthorized WWW-Authenticate: Digest realmsip.example.com, nonceNc1aB2, algorithmMD5, qopauth 终端 - 服务器: REGISTER sip:sip.example.com SIP/2.0 Authorization: Digest username1001, realmsip.example.com, nonceNc1aB2, urisip:sip.example.com, responseabcdef1234567890, algorithmMD5, qopauth, nc00000001, cnoncexyz CSeq: 2 REGISTER 服务器 - 终端: 200 OK Expires: 3600为什么第一封REGISTER不带认证也要发因为Digest认证是“挑战-应答”机制服务器需要先给一个nonce。nonce是服务器生成的随机串带时效防重放。如果服务器判断你提交的nonce、username、response都对才会回200并写入位置信息。有人嫌这一来一回浪费一次UDP往返想直接把认证信息塞进第一封REGISTER绝大多数服务器不会接受因为nonce还没生成。另一个很多人分不清的点是401和407的区别。401表示对请求资源的用户身份认证失败通常由注册服务器或业务服务器回407常见于经过SIP中继时的认证要求处理方式几乎一样只是头域从WWW-Authenticate换成Proxy-AuthenticateAuthorization换成Proxy-Authorization。调试时看到407别慌先看响应头里带的是哪种Challenge。还有一个细节我踩过坑第二封REGISTER的CSeq必须加11 REGISTER 变 2 REGISTERCall-ID保持和第一封一致否则服务器会认为这是两个无关事务照样不会给你200。很多自研终端在循环401的时候抓包一看CSeq永远都是1这就是典型的客户端自己把自己卡死的案例。2.2 Digest认证的计算链与三个最容易错的头域第二封REGISTER能不能通过校验核心在Authorization头里的response。它的计算链如下HA1 MD5(username:realm:password) HA2 MD5(method:uri) response MD5(HA1:nonce:HA2) // 无 qop response MD5(HA1:nonce:nc:cnonce:qop:HA2) // qopauth这不是随便算的任何一个输入和服务器端不一致结果就完全不同。我排错时优先核对三个地方。第一个是realm和nonce必须原样取服务器401响应里的值大小写都不能变。有些平台会在WWW-Authenticate里带realm34020000002000000001这个realm可能是平台ID而不是域名你按自己配置的域名去算response必错然后就是无休止的401循环。第二个是uri它必须等于REGISTER请求行里的Request-URI不能填From头里的URI。很多代码库写死了uriusernamedomain一旦服务器域和From域不一致就会抓狂。第三个是method必须是大写REGISTER不能因为收到401就把method改成别的。再就是qop。服务器返回的Challenge里如果带了qopauth你必须按带qop的公式算并额外提供nc和cnonce两个参数否则服务器报400。反过来如果服务器没带qop而你非要加很多老平台也认但少数平台会直接拒。这里没有银弹我的习惯是先查服务器返回的Challenge里有没有qop有就按有算没有就按没有算而不是写死。海康这类监控平台对接时多数用无qop的MD5但个别版本会带qopauth所以最好做成可配置。2.3 Expires、Contact与在线状态的维护注册并不是一次性的动作服务器记录的位置信息带有效期过期后终端必须重新注册。有效期通过Expires头或Contact头里的expires参数控制常见值是300、600、3600。服务器收到REGISTER后会检查这个值和自己的策略如果小于min_expires会回423响应并告诉你Min-Expires如果大于max_expires会被强制截断。Contact头里填的是终端实际可被呼叫的地址服务器后续的INVITE、OPTIONS都会发到这个地址。在纯内网环境里没问题但设备一旦在NAT后面Contact里往往是内网地址服务器直接把呼叫请求发过去是到不了的。常见做法是服务器用Via头里的received参数改写Contact记录请求实际来源。除此之外设备还要定期发OPTIONS或轻量REGISTER做保活维持NAT映射。判断设备是否在线不能只看有没有注册成功还要看有没有在周期内刷新。服务器一般把最近一次收到REGISTER或OPTIONS的时间作为在线判据超过1.5到2个注册周期就算掉线。3. 服务器侧注册配置把认证参数、过期策略和心跳窗口一次调对3.1 用Kamailio做注册服务器最小可用的认证与保存配置我平时搭建注册服务器首选Kamailio。它是纯SIP路由引擎把认证和位置保存解耦得很清楚出问题也好定位。最小可用的注册配置围绕auth、auth_db、registrar三个模块展开modparam(auth_db, db_url, mysql://sipuser:sippasslocalhost/sip) modparam(auth_db, password_column, ha1) modparam(auth, auth_checks_no_qop, 1) modparam(auth, nonce_expire, 300) modparam(registrar, default_expires, 3600) modparam(registrar, min_expires, 60) modparam(registrar, max_expires, 7200) modparam(registrar, max_contacts, 5)带认证的REGISTER路由逻辑一般长这样route[REGISTRAR] { if (!is_method(REGISTER)) return; if (!auth_check($fd, subscriber, 0)) { auth_challenge($fd, 0); exit; } if (!save(location)) return; }auth_check里的第三个参数0表示普通文本数据库存储如果你用HA1预计算列改成1也行但auth_db模块的password_column要对应。save(location)是真正把Contact写进位置表的一步失败时多半是表结构不对或权限不足。这里有个关键认知nonce_expire300表示nonce只活5分钟设备如果注册周期是3600秒刷新时nonce一定过期了于是服务器再回401终端重新完整认证一遍。这不是故障是防重放机制但从平台侧看会表现为每隔一小时闪断一次。如果不想让它太频繁把nonce_expire调到略大于最大Expires比如7500秒代价是nonce被重放的风险窗口稍微变大。内网环境我没那么在意公网环境建议维持较小的nonce_expire。min_expires60是另一个容易引发误会的参数。海康这类监控设备经常把注册周期写成30秒你这里如果设了60秒设备第一个REGISTER就会被423拒绝。正确做法是把min_expires调低到10秒左右或者去平台侧把注册周期改大别让服务器和平台在过期策略上打架。3.2 location表注册数据的落库与过期清理Kamailio的registrar模块会把注册信息写进location表。表结构一般按官方模板建核心字段就这些CREATE TABLE location ( username VARCHAR(64) NOT NULL, contact VARCHAR(255) NOT NULL, expires INT NOT NULL, q REAL DEFAULT 1.0, callid VARCHAR(255), user_agent VARCHAR(255), last_modified DATETIME, PRIMARY KEY (username, contact) );注意主键是(username, contact)而不是username因为一个用户可能有多条注册分机、手机App、桌面话机各一个Contact。如果你把username设成主键同一个用户第二次注册会把第一条覆盖掉这在通话场景会出大问题。password列最好存HA1而不是明文密码。auth_db的password_columnha1表示认证时直接用数据库里的ha1参与哈希计算即使数据库被拖走攻击者拿到的是哈希而不是明文。用户名和明文密码的对应表一旦泄露内网账号就全完了。过期清理也不能省。Kamailio本身会在收到新REGISTER时处理过期Contact但长期不活跃的用户记录会一直占着max_contacts。我一般用cron每5分钟跑一次清理SQLDELETE FROM location WHERE expires UNIX_TIMESTAMP();同时把appearance表的在线标志同步清掉。如果这块没做你会看到用户明明已经断网一周位置表里还留着记录平台侧依然显示在线。3.3 Asterisk和FreeSWITCH的注册参数对照不是每个团队都用Kamailio很多场景是把Asterisk或FreeSWITCH当作注册服务器兼媒体服务器。参数名不同但功能一一对应功能Kamailio参数Asterisk pjsip参数建议值认证来源auth_dbauth_typeuserpass无nonce有效期nonce_expireauth_nonce_lifetime3600或更高最短注册周期registrar min_expires无直接参数10-60最长注册周期registrar max_expiresmax_expires7200心跳探测dispatcher/optionsqualify_frequency60Asterisk的auth_nonce_lifetime默认300秒如果设备注册周期是3600秒同样会发生每小时闪断一次。我习惯把它调成比max_expires大一些。另外注册和业务分离是我一直推荐的做法。注册服务器只负责认证和位置表媒体与呼叫交给FreeSWITCH或Asterisk。这样做的好处是注册风暴或认证攻击不会直接压垮媒体服务器两边可以独立扩容。小规模部署一台机器跑两个进程也没问题但至少逻辑上要分开。4. 对接海康这类三方平台从平台的严格校验反推你的注册配置4.1 平台侧SIP注册和通用话机注册的差异海康平台SIP对接配置本质是让设备通过REGISTER注册到平台这个SIP服务器上。和普通话机注册最大的差异是平台不仅管注册还管后续的INVITE、心跳、媒体协商设备注册用的ID不是电话号码而是平台分配的20位编码。这个ID会出现在From、To、Contact等多个头域里任何一个写错都可能鉴权失败。我抓过一套典型的平台注册报文第一封REGISTER长这样REGISTER sip:34020000002000000001192.168.1.100:5060 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.64:5060;rport;branchz9hG4bK4d3f2a From: sip:3402000000132000000134020000002000000001;tag0987 To: sip:3402000000132000000134020000002000000001 Call-ID: 7a1b2c3d4e5f192.168.1.64 CSeq: 1 REGISTER Contact: sip:34020000001320000001192.168.1.64:5060 Expires: 3600 User-Agent: IP Camera Content-Length: 0注意这里有个容易搞反的地方REGISTER请求行里的域是平台的SIP服务器IDFrom和To里后面的也是平台域不是设备自己的IP。Contact里才是设备自己的实际地址。有些自研设备把From里的域填成设备IP平台直接不认。第二封REGISTER带认证头格式和通用SIP一样Authorization里的username用平台分配的20位编码realm取服务器返回的值。海康平台多数用MD5、无qop的摘要算法个别版本带qop。按第2章的方法算response即可。4.2 对接前先向平台方要齐的配置清单不要等上线了才去翻平台手册对接前把下面几项问清楚能省掉一大半调试时间平台SIP服务器地址和端口对应REGISTER请求行的域。平台返回的realm取值很多平台直接用平台ID别猜。设备接入编码和认证密码密码区分大小写别在配置里加空格。注册周期建议值平台一般接受300或3600海康设备默认可能偏小。是否提供公网映射如果设备在NAT后面平台是否支持rport和received改写。摘要算法细节是MD5还是MD5-sess是否强制qop。拿到这些信息后再写REGISTER事半功倍。如果平台方自己也说不清就抓包看401响应里的WWW-Authenticate头上面写着realm、algorithm、qop这就是最权威的答案。4.3 平台侧常见校验UA头、Header顺序、摘要算法监控平台对接时比通用SIP服务器更容易遇到平台侧的隐性校验。第一个是User-Agent有些平台会按UA头判定设备型号如果你用通用UA比如PJSIP平台可能回复403或直接丢弃请求。对接前先确认平台允许的UA列表或者直接抓包看平台上已经注册成功的设备用的什么UA。第二个是Header顺序。协议上顺序无关但有些平台实现得很粗糙解析器按固定偏移截取头域。我遇到过把Via放在第二位就解析失败的情况解决方式是先抓一个平台自带设备能成功的报文完全照抄头域顺序。第三个是摘要算法。个别平台在WWW-Authenticate里不声明algorithm默认MD5有的声明了但实际校验时又不用这就是纯玄学了。这时候唯一的办法是把两个REGISTER之间的所有头域逐行抄下来在本地用Digest计算器重算response比对不通过时试着去掉qop重算再试着改realm大小写一个一个排除。至少我遇过的平台最后都收敛到realm或qop这两个问题上。4.4 平台注册成功后还需要心跳维持在线状态平台侧在收到REGISTER之后会周期性查看设备是否在线。很多监控平台规定设备要每隔30到60秒发一次心跳常见是国标联网协议里的MESSAGE消息也可能就是OPTIONS平台收到后更新在线状态。如果你只注册一次就等着平台会在一个周期后把设备标记为离线。这个心跳和SIP注册的刷新不是一回事心跳消息可能是平台私有格式也可能就是OPTIONS。对接时把心跳间隔设为平台要求值的二分之一留出重试余量。海康平台对心跳间隔的要求比较严格超过时限就是在线刷新失败日志里会直接告诉你超时秒数。5. 注册到服务器的避坑实录四个高频故障的排查路径5.1 注册成功却反复重连Expires与min_expires打架现象设备侧显示注册成功但抓包看到REGISTER和200 OK的序列每隔几秒就重复一次平台列表里设备状态一直在上下跳动。原因设备把注册周期设成了一个很小的值有些设备默认10秒而服务器设置了min_expires60设备收到423响应后又立刻重发或者设备根本不看423里的Min-Expires依然按自己的小周期刷。还有一种情况是设备在自己的Expires到期前没收到200就急着发下一个REGISTER两个事务重叠服务器只处理了其中一个。解决把服务器min_expires调低到10秒同时在设备侧把注册周期改到300或600秒。如果是自研设备把状态机改成收到200后再进入刷新等待不要用绝对时间唤醒。5.2 401循环response算不对先查realm、uri和qop现象第二封REGISTER已经带了Authorization头服务器还是回401反复多次。原因response计算里的输入和服务器对不上。最常见的是realm没取服务器返回的值而是用自己配置文件里的域名其次是uri填错第三是qop算法没对齐。解决先在抓包里把401响应的WWW-Authenticate头完整抄出来取realm、nonce、algorithm、qop。然后找一个独立的Digest计算工具填入和抓包完全相同的值算出response。如果算出来和终端发的不一致问题在终端如果算出来一样服务器还拒再查服务器侧密码库里的密码和realm是否和终端一致。5.3 公网服务器收不到REGISTER响应NAT回程路由问题现象设备在内网服务器在公网。REGISTER消息发出去了抓包能看到出站请求但设备永远收不到401或200。原因这是NAT和SIP的老问题。REGISTER从内网设备一个随机端口出去NAT映射表建立后服务器回包如果发到另一个端口内网设备就收不到。更麻烦的是SIP的Contact头里写的是内网IP服务器按Contact回包直接把包发到内网地址去了。解决服务器开启rport和received处理。Kamailio里用nathelper模块对REGISTER的Via头做处理回包发到实际收到请求的源地址。同时让设备每隔30秒发一次OPTIONS或轻量REGISTER维持NAT映射的老化时间。如果设备在运营商级NAT后面还要考虑是否配置端口映射把5060映射到设备。5.4 假在线注册超时了服务器还留着位置记录现象设备被断电或断网平台侧仍然显示在线呼叫设备失败后才报离线。原因设备没有发BYE或REGISTER注销服务器只能等Expires超时才能清位置记录。如果注册周期是3600秒平台要等1小时才发现设备真死了。很多监控平台对在线的定义是注册成功那一刻之后全靠自己的心跳探测来修正探测间隔设得太长就出现假在线。解决服务器对每个注册用户做主动探测周期设为注册Expires的五分之一。比如注册周期300秒则每60秒发一次OPTIONS连续3次没回就强制把这个用户的注册记录置为过期并推送离线事件给平台。这样假在线的窗口最多只有180秒。我自己的服务器上还把主动探测的日志单开了一个文件这样排查谁掉线了不用再翻全量抓包。5.5 注册、掉线、再注册的快速排查清单遇到注册类问题我一般按这个顺序走tcpdump抓包tcpdump -i eth0 -s 0 -w reg.pcap -vvv port 5060保留完整信令。用sngrep或Wireshark看REGISTER、401、REGISTER、200四个关键包的时间戳和头域。检查服务器auth模块日志确认是密码错误还是nonce过期还是找不到用户。对比终端和设备两边的时间nonce有效期判断依赖时间一致。用sipp模拟一个标准REGISTER发给服务器确认服务器对标准终端的响应排除终端协议栈问题。6. 用抓包与脚本把注册链路做成可回归的验证套路6.1 拨测脚本验证REGISTER往返和鉴权算法每次改服务器配置后我都习惯用一个拨测脚本快速验证注册链路是否正常而不是手动发消息。核心逻辑是发第一封REGISTER解析401里的nonce计算response再发第二封REGISTER打印最终状态码。一个精简版长这样import socket, hashlib, re def digest(username, realm, password, nonce, uri, methodREGISTER): ha1 hashlib.md5(f{username}:{realm}:{password}.encode()).hexdigest() ha2 hashlib.md5(f{method}:{uri}.encode()).hexdigest() return hashlib.md5(f{ha1}:{nonce}:{ha2}.encode()).hexdigest() def build_register(uri, username, branch, cseq, auth_header): return (fREGISTER {uri} SIP/2.0\r\n fVia: SIP/2.0/UDP 127.0.0.1:5060;branch{branch}\r\n fFrom: sip:{username}127.0.0.1;tag1\r\n fTo: sip:{username}127.0.0.1\r\n fCall-ID: test01\r\n fCSeq: {cseq} REGISTER\r\n fContact: sip:{username}127.0.0.1:5060\r\n fExpires: 300\r\n f{auth_header} fContent-Length: 0\r\n\r\n) def reg(ip, port, username, password, uri): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(3) msg1 build_register(uri, username, z9hG4bK-1, 1) sock.sendto(msg1.encode(), (ip, port)) resp sock.recv(4096).decode() nonce re.search(rnonce([^]), resp) if not nonce: return resp.split(\r\n)[0] nonce nonce.group(1) realm re.search(rrealm([^]), resp).group(1) response digest(username, realm, password, nonce, uri) auth (fAuthorization: Digest username\{username}\, realm\{realm}\, fnonce\{nonce}\, uri\{uri}\, response\{response}\\r\n) msg2 build_register(uri, username, z9hG4bK-2, 2, auth) sock.sendto(msg2.encode(), (ip, port)) resp2 sock.recv(4096).decode() return resp2.split(\r\n)[0] print(reg(192.168.1.100, 5060, 34020000001320000001, secret, sip:34020000002000000001192.168.1.100))脚本里build_register封装了头域模板每次拼接一个CSeq递增的新REGISTERdigest函数实现了无qop的MD5摘要计算如果你的平台强制qopauth在函数里多加nc、cnonce两个参数即可。参数ip、port是服务器地址和端口username用平台分配的用户编码password是认证密码uri填写REGISTER请求行里的域。这个脚本只依赖标准库内网任何一台Linux机器都能跑。如果返回200 OK说明认证算法、realm、密码都对如果返回401把脚本里的uri或realm打印出来和抓包对比。6.2 把抓包基线存成习惯我现在的习惯是每次改完服务器或平台对接参数先跑一遍这个拨测脚本同时tcpdump抓包留存确认注册、刷新、注销三个动作都正常才收工。抓包文件命名带上日期和改动点比如reg-20250110-nonce-lifetime.pcap方便两周后出问题时有历史可查。SIP注册链路看起来简单但认证算法、过期策略、NAT三块叠加时没有基线就只能靠玄学。把每一次改动固化成一个可重复的验证动作比记住一堆参数更有用。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。