1、验证ICE支持能力
Agent收到初始offer以后,首先,它需要验证ICE的支持能力。对于每个在SDP中收到的媒体流来说,如果媒体流的每个构件的默认目的地地址出现在了候选地址属性中,agent将会根据RFC5245规范中ICE的处理流程来进行处理。如果我们进一步做具体理解的话,我们可以参考RTP的处理方式来说明。例如,使用RTP时,在c行和m=行的IP地址和端口出现在了候选地址属性参数中;使用RTCP时,RTCP值也会出现在候选地址属性中。如果前面所说的这个条件不成立,或者这些媒体构件没有出现在候选地址属性中的话,agent必须按照另外一个流程来处理SDP(RFC3264)值,而无需按照ICE的处理机制来处理。这个流程就是我们在前面文章中所介绍的关于SIP针对offer/answer交互模式来处理。读者可以查看历史文档来回顾笔者介绍的相关基础内容。如果按照RFC3264处理的话,读者也需要注意几个例外条件:
所有agent必须遵从RFC5245-10中的会话存活保持流程。
如果agent没有根据RFC5245 ICE的处理流程处理的话,是因为有a=候选地址属性,但是这些a=候选属性没有匹配媒体流的默认目的地地址,agent在其answer消息中必须包括一个a=icemismatch属性(无匹配)。
如果默认候选地址是一个从TURN服务器学习获得的转发地址,agent必须在TRUN服务器端创建一个授权许可,这个许可支持SDP中收到的,从对端的peer学习到的IP地址。大家需要注意,如果权限设置有问题的话,对端发过来的初始数据包将会丢失。
2、决定主控/被控制方角色
在上一篇文章中,笔者介绍了主控agent和被控方agent的角色。在会话中,每个agent需要充当一个角色来执行不同的操作流程。主控方负责选择最终候选配对和被控方进行通信,如果是全部署环境下的agent,这表示ICE使用挑选的候选配对对每个媒体流进行传输,并且,当agent需要时,可基于ICE的选择生成更新的offer消息。如果是轻量级的部署环境中的agnet,选定为主控方agent表示选择了候选地址配对,这个候选配对是基于offer和answer消息中的配对(在IPv4中仅有一对),并且,当agent需要时,生成一个更新的offer消息来反映这个选择。被控方agent被告知使用的候选配对来传输媒体流,并且针对这个单个信息不会生成一个更新offer。下面,笔者将讨论决定双方角色的规则和处理流程对双方的的影响。关于决定双方角色的规则和其流程的影响,事实上,这取决于双方agent所处的部署环境,这里有三种不同的部署环境需要讨论:双方都在全部署场景,双方各自在全部署场景/轻量级部署场景,双方都在轻量级部署场景。
双方都在全部署场景中,如果一个agent生成了一个offer消息,并且启动了ICE处理流程,这个agent必须扮演一个主控方agent的角色。另外一侧agent则必须扮演被控方角色。双方agent将会构建检查列表,运行ICE状态机和连接检查的流程。主控方将会根据全部署场景流程执行处理逻辑,挑选候选配,ICE根据这些候选配对提供进一步的选择,最后双方agent通过更新offer来更新或者结束ICE处理流程。当然,在实际部署场景中,不排除一些特殊使用环境,例如因为其他通信因素,双方的角色认定发生冲突。双方agnet错误地认为自己是主控方或者自己是被控方。因此,为了避免类似情况的发送,每个agent必须选择一个任意号码,RFC5245规范称之为tie-breaker,在连接检查中使用此数值检测修复这种情况,其取值范围一律发布在0和(2**64) - 1之间(它是一个64位的正整数)。
双方一方是全部署场景agent,另外一方是轻量级部署场景agent中,全部署场景的agent必须扮演主控方agent的角色,轻量级agent则必须扮演被控方agent的角色。全部署agent将会构建检查列表,运行ICE状态机和生成连接检查。主控方将会根据全部署场景流程执行处理逻辑,挑选候选配对,ICE根据这些候选配对提供进一步的选择。轻量级agent将会监听连接检查,接收响应检查消息,按照轻量级部署的结束ICE处理流程,最后对ICE进行结束处理。因为双方的角色不同,从某种程度来说,主控角色一般都是一直处于运行状态。因此,对轻量级部署agent来说,针对每个媒体流来说,ICE处理状态被认为是运行状态,所有ICE流程也是运行状态。
Agent双方都是轻量级部署的agent的话,如果一个agent生成了offer消息,并且启动了ICE处理流程,这个agent必须扮演一个主控方agent的角色。另外一侧agent则必须扮演被控方角色。这种环境中,双方从来都不会发送连接检查。准确地说,一旦双方的offer/answer交互模式完成以后,每个agent将执行ICE结束处理流程,它们无需经过连接检查的流程。和第一种场景中所描述的一样,同样的角色冲突问题也可能出现在双方都是轻量级部署agent的环境中。它们都可能认为自己是主控方agent或者被控方agent。这种情况下的处理方式和全场景部署中的角色决定的处理方式不同。轻量级部署环境中角色冲突时,双方agent通过在信令中承载的offer/answer交互,和交互消息中所支持的检测能力来确定双方角色。对轻量级部署agent来说,针对每个媒体流来说,ICE处理状态被认为是运行状态,所有ICE流程也是运行状态。
在会话中,一旦双方角色确定以后,除非ICE重新启动,否则,它们将一直持续充当各自的角色。补充说明,因为ICE重新启动以后,双方需要重新决定各自的角色,如果是全部署场景agent的话,它们需要重新对tie-breaker赋值计算。
3、候选地址采集
关于针对候选地址的采集处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解全部署场景中的场景流程和轻量级部署的要求等内容。根据RFC5245的推荐,提供方收到offer,早于对用户提醒之前马上执行采集流程。当agent启动时,这样的候选地址采集方式就可能开始。
4、候选地址优先级排序
关于针对候选地址的排序处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解全部署场景中的场景流程和轻量级部署的要求等内容。
5、默认候选地址选择
关于针对候选地址的默认候选地址选择的处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中已经非常详细地做出了说明。用户可以阅读此文章来了解全部署场景中的场景流程和轻量级部署的要求等内容。
6、SDP解码
关于针对SDP解码的处理流程,answerer应答方和offerer提供方的处理方式是完全一样的。笔者在前面的文章中针对全部署场景和轻量级部署场景的规定已经非常详细地做出了说明。用户可以阅读此文章来了解全部署场景中的场景流程和轻量级部署的要求等内容。
7、检查列表构建
构建检查列表是由全部署场景来实现的,如果是轻量级的部署场景,无需构建检查列表。因为offer/answer交互模式的使用,在媒体使用的过程中需要一个检查列表。为了对媒体流构建一个检查列表,agent需要经过几个必要的步骤来构建检查列表,agent需要经过的步骤是:构建候选地址配对,计算候选配对优先级和排序,优选配对,最后计算配对状态。接下来,笔者将分别讨论这四个主要的步骤。
首先,为了实现对媒体流的支持,agent选择自己本地候选地址和从对端peer收到的候选地址进行配对处理。这里,本地候选地址称之为LOCAL CANDIDATES,远端的候选地址称之为REMOTE CANDIDATES。选择过程中,agent同时还要考虑安全的问题,为了防止选择的地址被攻击,agent可以设置从offer或answer中接收候选地址的数量。关于STUN被攻击的可能性讨论,请读者参考RFC5245-18,后期文章中我们将讨论这个话题,现在不做讨论。如果本地候选地址和远端候选地址配对成功的话,它们必须具有相同的component ID,和同样的IP地址版本。当然,实际环境中,本地候选地址和远端候选地址也完全可能存在不匹配或者匹配不成功的可能,或者,远端候选地址和本地候选地址匹配不成功的可能。有时也可能发生这样的问题,例如,针对一个媒体流来说,agent没有包含候选地址来支持此媒体流的所有构件模块。如果是这样的情况的话,此媒体流的构件数量就会受到影响而减少,并且会认为这个数量等于双方agent所要求的构件最低数量,此最低数量值针对媒体流的所有component构件,并且来源于双方agent所提供的最大component ID(关于ID取值范围读者可参考前面的介绍和历史文档)。
除了上面所介绍的一些情况以外,还有几个比较特殊的情况和读者说明。在涉及到RTP/RTCP的使用场景中有可能发生这样的情况,一方agent提供了RTCP的候选地址,但是对端可能没有提供类似地址。有时,offer消息提供方可在同一端口多路复用RTP/RTCP数据,并且通过SDP属性中指示了这样的实现方式(多路复用RTP/RTCP参考RFC5761-5和RFC8035)。但是,如果answerer执行了多路复用RTP/RTCP的话,offerer则不知道answerer执行了这样的流程,offerer就会按照默认的设置方式使用各自独立的RTP和RTCP,这样处理的结果就会导致每个媒体流中在offer消息中包含两个components。answerer方执行了多路复用RTP/RTCP,对每个候选地址来说,它将仅包含单个component。因此,此component将是RTP/RTCP mux的合并值。如果此候选地址只有单个component的话,ICE结束执行候选地址配对。毫无疑问,如果配对中本地候选地址和远端候选地址都是默认候选地址时,这个候选配对被认为是默认的候选配对。
如果agent双方不是ICE感知的agent的话,媒体流构件使用此默认候选配对传输媒体流。读者可以参考以下示例来理解check list(检查列表)核心概念和与其他模块之间的关系。
![](http://www.ctiforum.com/uploadfile/2020/0413/20200413091146285.jpg)
check list列表和其他模块之间的关系汇总
构建候选地址配对完成以后,需要进行后续地址配对的优先级计算。优先级计算是根据以下格式来计算的。其中,G表示主控方agent提供的候选优先级,D表示被控制方提供的候选地址优先级。这里,G>D?1:0是一个表达式,如果G大于D,则取值为1,否则为0。
pair priority = 2 ^ 32 *MIN(G,D) + 2 *MAX(G,D) + (G > D?1:0)
关于以上优先级的计算,很多开源项目有类似的处理方式,读者可以参考一些开源项目来做进一步了解。以下一段代码是一个配对计算源代码示例,读者可以参考:
// PairPriority computes Pair Priority as in RFC 8445 func PairPriority(controlling, controlled int) int64 {
var (
g = int64(controlling)
d=int64(controlled) )
// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) v := (1<<32)*min(g, d) + 2*max(g, d)
if g > d {
v++ }
return v }
https://github.com/gortc/ice/blob/master/pair.go
一旦优先级被设定以后,agent就会按照顺序对候选地址配对进行排序。排序的规则按照优先级顺序递减的方式进行。如果两组候选地址配对有相同的优先级,它们两组配对的实现可以任意排序。
获得了候选地址配对排序以后,此优选候选地址会生成一个排序后的配对列表。ICE将会按照排序列表逐一进行连接检查。在每个检查流程将会涉及发送请求的流程,agent需要从本地候选地址发送检查请求到远端候选地址。这里注意,因为agent不能直接从反射地址发送请求,它仅能从base基准地址发送请求,所以agent需要通过排序后的配对列表来发送请求。对每个配对来说,如果本地候选地址是一个反射地址的话,base基准地址必须替换这个反射候选地址。一旦替换流程完成后,agent必须过滤或筛选此列表。过滤配对列表的流程通过移除配对的方式来实现。具体来说,如果它(需要移除的配对)的本地候选地址/远端候选地址和优先级列表中较高优先级的一对配对相同的话,则需要移除配对相同的较低优先级的配对。
另外,为了安全的考虑,防止STUN服务器被攻击,agent必须限制连接检查数量,在一定的数值设置环境中,agent的检查将会覆盖所有连接列表的地址,这个特定数值必须是可配置的数值。规范RFC5245推荐默认数值是100。候选配对列表一直保持低于100的限定设置,一些低优先级的候选配对将被强制丢弃。在可能的情况下,RFC5245推荐尽量使用比较低的设置限定,在实际生产环境中也可能看到一些用户针对配对检查设置了最大检查限定。要求支持可配置的限定设置也是为系统提供了一个工具,如果发现问题以后,可以通过此限定值来排查问题。
完成了候选配对地址的挑选以后,检查列表需要进行状态计算。每个候选配对支持了一个foundation和一个state(状态)。实际上,这里的foundation是合并了本地候选地址的foundation和远端的候选地址的fundation而生成的一个新的fundation。一旦开始计算foundation时,每个配对将会设定一个状态值,根据检查结果的不同,候选配对需要经过五个可能或潜在的状态计算(历史文章也有介绍这五个计算步骤)。
![](http://www.ctiforum.com/uploadfile/2020/0413/20200413091212404.jpg)
候选配对状态计算
ICE开始运行时,一个候选配对将迁移到以下任何一种状态。笔者再次重复一次每个状态的任务然后介绍具体流程处理:
- Frozen:处于锁定状态,等待检查
- Waiting:检查还没有启动,等待从check list中选择最高优先级的候选配对进行处理
- In-Progress:为这个配对发送check请求,事务在处理流程中
- Succeeded:配对检查成功,生成成功结果
- Failed:配对检查失败,既没有生成任何响应也没有生成任何还原响应
在检查列表中的每个候选配对的初始状态需要经过一个状态计算。其状态计算需要按序经过以下几个步骤:
- agent将会把每个检查列表中所有的候选配对设置为Frozen 封冻状态。
- agent将会为第一个媒体流查询检查列表(第一个媒体流出现在SDP offer和answer中的第一个m行中)。然后针对此媒体流做状态设定处理。对所有具有同样foundation的配对,agent将会设置一个比较低级别component ID的配对状态,设定这些配对进入等待状态。如果有多个类似这样的配对,agent则首先使用具有最高优先级的配对。
这里有两个特定的列表称谓需要读者注意。其中检查列表中的一部分配对会进入等待状态,另外一部分则进入到封冻状态。如果检查列表中至少有一对配对是在等待状态的,这样的列表称之为活动检查列表。如果检查列表中所有配对都是封冻状态的,这样的列表称之为封冻检查列表。
除了检查列表中的配对检查有状态存在,检查列表自己本身也关联一个状态,针对正在工作的媒体流,这个状态用来捕捉ICE检查的状态。检查列表具有三种状态:
- 运行状态:针对正在运行的媒体流,ICE检查状态也在运行中。
- 完成状态:在这个状态下,ICE检查已经生成了一个经过挑选的配对支持媒体流构件模块。接下来,ICE成功完成处理任务,开始发送媒体流。
- 失败状态:在这个状态下,针对此媒体流的支持,ICE检查还没有成功。
作为一个offer/answer交互的结果,检查列表首先构建的就会被ICE迁移到运行状态中。ICE处理流程覆盖所有的媒体流过程中,因此,ICE也和这个流程本身有一个关联绑定的状态。当ICE运行时,这个状态等于运行状态。当ICE处理流程完成后,这个状态将是完成状态,如果ICE处理流程失败的话,这个状态就是失败状态。关于以上几个状态之间切换的规则笔者将在定时检查的讨论中做更多说明。
8、定时检查
前面我们一直在讨论关于check的流程,check是由全部署场景生成的,所以,如果agent是轻量级的部署方式的话,可以跳过此内容讨论。Agent执行两种check,一种是ordinary checks,另外一种是triggered checks。两种check都是由一个定时器来控制,针对媒体流数据,定时器会周期性地触发check生成事件。
除了定时器以外,agent也会维护一个先进先出的队列,这个队列称之为triggered check队列,队列维护候选配对,如果有check的机会的话,这个队列将会发送配对进行检查。当定时器触发以后,agent将会从triggered checks队列顶部移除一个配对,对这个配对执行连接检查,最后把这对候选配对的状态设置为正在处理的状态(In-Progress)。如果在triggered checks队列中没有配对的话,agent将会发送ordinary checks。
一旦完成构建检测列表计算的流程,agent将会针对每个active check list设置一个定时器。每Ta*N秒触发一次定时器,这里的N是active check list的数量(初始阶段,至少有一个active check list)。Ta和RTO这两个定时器会在未来的讨论中加以介绍,这里不再展开讨论。Ta乘以N将允许整个check的吞吐量发布到所有active check list中。在实际部署环境中,这个定时器触发的频率可以低于上面的设置,同时,也应该考虑定时器传播的问题,尽量不要同时对每个媒体发布定时器。第一个定时器马上触发后,agent在这一瞬间(offer/answer交互模式已完成)执行连接检查,然后在Ta秒后执行下一个检查(因为在第一个定时器触发时只有一个active check list)。
如果定时器触发以后,agent没有triggered checks需要发送的话,它必须按照以下规则选择一个ordinary checks:
- 找到在等待状态的check list中最高优先级的配对(注意以下两种状态下配对处理流程)。
- 如果有这样的配对的话:首先,从本地候选配对发送一个STUN check请求到远端候选配对。此STUN check请求将会进行相关处理。关于STUN check请求的处理流程,笔者在后续文章中介绍。然后对候选配对的状态进行设置,设置其状态为In-Progress状态。
- 如果没有这样的配对的话:agent需要找到在封冻状态的check list中最高优先级的配对。如果有这样的配对的话,对此配对解除封冻状态,然后对此配对执行check流程,使其状态切换为In-Progress状态。如果没有这样的配对的话,结束针对此check list的定时器。
- 配对流程完成以后,需要保证其检查数据的完整性。如果要计算check消息的完整性,agent需要使用远端用户名称和密码来完成计算。远端用户名称和密码是agent学习远端peer发送的SDP中的用户名称和密码获得。
在接下来的章节中,笔者将继续分享初始应答接收的话题(验证ICE支持,决定角色等话题)。
参考资料:
https://www.rfc-editor.org/rfc/rfc5245
https://www.rfc-editor.org/rfc/rfc8445
https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/state
https://github.com/gortc/ice/blob/master/pair.go
关注微信公众号:asterisk-cn,获得有价值的Asterisk行业分享
Asterisk freepbx FreeSBC技术文档: www.freepbx.org.cn
融合通信/IPPBX商业解决方案:www.hiastar.com
如何使用FreeSBC,qq技术分享群:334023047
![](http://www.ctiforum.com/uploadfile/2020/0413/20200413091353887.jpg)