最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

互联网自诞生以来,其用户群一直保持着持续上升的趋势,并且随着内容不断增加,从文本到视频的需求也要求更实时的传输,流量增长更为明显。根据思科视觉网络指数™(VNI)的发布,这种趋势成为必然,并且这种需求趋势不会很快改变。在2020年,该报告预计IP流量将是2015年的三倍。此外、预计将有超过10亿的新互联网用户加入全球互联网社区,从2015年的30亿增长到2020年的41亿。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

此图例以及以下图例均来自于互联网资源

互联网的野蛮生长需要对其基础设施以及协议和其基础设施所支持的协议和应用的做一些调整。用户经常遇到的是网络页面浏览,考虑到网页如何网页上的信息越来越多,越来越繁杂、如动态对象或简单的更多数据。绝大部分的网络页面主要由TCP和HTTP协议控制或支持。但是,HTTP是在90年代初设计的协议,它并不能预测到今天不断发展的流量的传输,特别是基于大数据环境下的网络传输。为了克服一些传输问题,在技术层面,技术人员引入了以缓存或代理服务器为形式的中间缓存机制或代理服务器形式来以减少网页加载时间。

随着手机互联网的发展,另外,各种移动终端的出现和APP的出现,要求更快的网络访问速度。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

并且,因为网络安全的考虑,HTTP2和TLS-1.3已经开始普及,网络连接的握手机制和安全机制也需要进行优化和提升。技术层面来说,需要新的传输机制是针对互联网,大数据发展的一个必然的需求。QUIC横空出世。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

关于QUIC的背景说明

Google作为互联网巨头,在网络技术领域一直扮演着领头羊的作用。针对TCP,HTTP目前的传输效果和一些诟病,谷歌发布了新的传输技术架构QUIC- A UDP-Based Multiplexed and Secure Transpor,参考RFC9000 详解。首先说明,TCP(传输控制协议)和QUIC(快速UDP互联网连接)都是用于在互联网上传输数据的协议。它们有以下主要区别:

1. 基础协议:TCP基于IP协议,是一种面向连接的、可靠的、基于字节流的传输层协议。QUIC则基于UDP协议,是一种更快、更可靠的传输协议,旨在解决TCP的一些缺陷和限制。

2. 连接建立:TCP使用三次握手建立连接,这会导致连接建立延迟。QUIC可以在第一次握手时建立连接,并且包括了TLS-13,从而减少了连接建立的时间。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

3. 加密和安全性:TCP本身不包含加密功能,需要额外的安全协议(如TLS)来确保数据传输的安全。QUIC内置了加密功能,提供了与TLS类似的安全性,同时减少了额外的握手过程。

4. 流控制和拥塞控制:TCP使用单一的流控制和拥塞控制机制,导致一个流上的丢包会影响其他流的传输(称为“队头阻塞”)。QUIC使用多流控制,可以独立处理每个流的丢包和拥塞,避免了队列的头阻塞问题。

5. 传输速度:QUIC由于减少了连接建立时间、避免了队头阻塞等问题,通常具有比TCP更快的传输速度,尤其在高延迟和丢包率的网络环境下表现更为优越。

6. 兼容性:因为TCP作为互联网的基石协议,必须具有广泛的兼容性。QUIC作为较新的协议,尽管得到了许多公司(如Google)的推广和支持,但在某些网络环境和设备上可能还不被完全支持。很多设备和其他应用都没有针对QUIC进行支持,所以距离完全普及还有一定的距离。不过,一些厂家已经开始了这方面的技术储备。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

微软的开源QUIC工具,用户可以使用这个支持包进行开发测试。另外,NGINX也声明开始在新版本中支持QUIC+HTTP/3。

MsQUIC操作面板数据显示如下:

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

一些研究人员也对两个协议之间的执行性能进行了对比,Késsia Nepomuceno发表的论文对TCP和QUIC传输的时延,cache,RTT,丢包等影响要素进行了对比。从实际测试效果来看,QUIC具有更好的效果。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

关于TCP和QUIC的网络抖,时延和带宽和丢包的测试效果如下:

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论
最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论
最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

必须说明的是,针对QUIC的安全问题也需要读者注意。毕竟,QUIC虽然基于TLS-1.3,安全性相对比较好。但是,在应用层面仍然存在很多需要优化的地方。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

以上和读者分享的是关于TCP和QUIC的一些差异和关于QUIC的发展背景介绍。笔者这里不准备和大家花费太多时间讨论QUIC协议具体内容,读者可以查阅RFC9000。我们这里计划进一步和读者分享关于QUIC在具体应用层的使用。虽然,QUIC还是处于刚刚起步阶段,很多应用场景已经考虑支持QUIC,包括最近几年比较受欢迎的WebRTC和IP语音核心协议-SIP。

虽然针对SIP over quic传输还没有完全开始应用普及,但是一些架构思路已经发布,核心初稿内容已经完成, 2023年4月份发布了最新的初稿。我们在接下来的章节中将重点介绍SIP-over-QUIC: Session Initiation Protocol over QUIC Transport,帮助读者对此技术细节有一个比较完整的认识和以及对此传输方式有一个充分准备,特别是针对SIP 帧格式数据,错误响应和控制数据等参数的不同需要加以关注。关于其草案内容,访问参考资料链接获得。以下是关于SIP-over-QUIC的草案概览,主要解释了关于QUIC的介绍,概览,关于SIP over QUIC的语法语义定义,头字段,连接存活机制,和SIP兼容性所涉及的事务和dialog处理,流媒体映射和使用,SIP 帧定义格式,错误处理和扩展使用等章节。

1. 简介

会话初始协议(SIP)[RFC3261]被大量广泛地应用于IP语音网络环境中来支持电话服务业务和视频会议,实时媒体处理,也包括和WebRTC的集成。

关于SIP协议的完整详解,读者可以访问:www.sip.org.cn 获得中文完整的SIP协议详解。这里仅做简单概述。

[SIP2.0]使用以空格分隔的文本字段来传输SIP消息,其格式与HTTP/1.1 [HTTP1.1]相似,并可选择通过TLS加密( “SIPS”)传输。本文所定义的SIP-over-QUIC使用通过QUIC流传输的二进制框架层,并使用QUIC传输连接所提供的强制性TLS加密的保护。

作者说明:未来的可选扩展可能引入在QUIC数据报中携带SIP消息的能力[QUIC-DATAGRAMS]。

1.2. 定义

在任何比较正式的规范文档中,一般都会约定某些专业术语。本文档中使用了以下术语:

中止:连接或数据流的突然终止,可能是由于错误状况造成的。

端点:连接的客户端或服务器。

连接:两个端点之间使用QUIC作为传输协议的传输层连接。

连接错误:一个影响整个SIP-over-QUIC连接的错误。

:SIP-over-QUIC流中最小的通信单位,由一个头和一个根据帧类型结构的可变长度的字节序列组成。

本文档和[QUIC-TRANSPORT]中都有称为 “帧 “的协议元素。在引用[QUIC-TRANSPORT]中的帧时,帧的名称将在前面加上 “QUIC”。例如,”QUIC CONNECTION_CLOSE帧”。

peer-对等实体:是一个端点。当讨论一个特定的端点时,”对等实体 “指的是和本地连接的远端实体。

接收方:一个正在接收帧的端点。发送方:一个正在传输帧的端点。

SIP/2.0:RFC3261 [SIP2.0]中描述的SIP/2.0规范。参考www.sip.org.cn 中文版本

SIP-over-QUIC连接:一个QUIC连接,其中协商的应用协议是SIP-over-QUIC。

数据流:由QUIC传输提供的双向或单向的字节流。SIP-over-QUIC连接中的所有流都可以被认为是 “SIP-over-QUIC流”,但在SIP-over-QUIC中定义了多种流类型。

数据流错误:单个流上的应用级错误。

传输客户端:发起SIP-over-QUIC连接的终端,例如SIP终端,B2BUA等。传输服务器:接受SIP-over-QUIC连接的终端。

关于客户端和服务器端的定义,读者可以通过RFC9000的定义来了解具体涵义。在RFC9000它们分别定义为:

Client: The endpoint that initiates a QUIC connection. Server: The endpoint that accepts a QUIC connection.https://datatracker.ietf.org/doc/html/rfc9000

其他术语,例如 “呼叫”、”对话”、”头”、”头字段”、”头字段值”、”发起方”、”被邀请者”、”消息”、”方法”、”代理服务器”、”请求”、”(SIP)交易”、”用户代理客户端 “和 “用户代理服务器 “在[SIP2.0]的第6节中定义。

2. SIP-over-QUIC协议概述

SIP-over-QUIC通过QUIC传输协议为SIP语义提供了一个传输方式,并且依赖于[HTTP3]的内部框架层构建。一旦用户代理客户端获悉用户代理服务器支持SIP-over-QUIC,它就会开启一个QUIC连接。QUIC为上面的SIP-over-QUIC事务层提供协议协商、基于流的复用和流控制服务。如[QUIC-TRANSPORT]第2节所述,SIP事务在QUIC流中被复用。SIP-over-QUIC事务中的每个请求和响应消息对都要耗费一个单个QUIC流。在每个QUIC流中,SIP-over-QUIC通信的基本单位是第7节中描述的帧。每个帧类型都有不同的用途。HEADERS和DATA帧构成了[RFC3264]中描述的offer/answer事务模型的基础,并在第3.2节中描述。读者一定要了解关于offer/answer model的处理才能进一步了解协商机制。

关于offer/answer 模式的讨论,读者可以参考:

在SIP规范中,一些头字段可以通过使用缩写版本进行压缩。在SIP-over-QUIC中,所有请求和响应头字段都使用[QPACK]进行压缩传输,其中头字段可以被映射为索引值,或者字面值可以使用Huffman表中的编码点进行编码。[QPACK]为其索引值使用两个表:静态表是用常见的SIP头字段和值预定义的,动态表可用于编码SIP-over-QUIC连接中经常使用的头字段以减少重复。由于[QPACK]的静态表设计为和[HTTP3]一起使用,本规范用附录B中的静态表取代了[QPACK]附录A中定义的默认静态表。完整内容链接见参考资料链接。

2.1. QUIC传输

SIP-over-QUIC依赖于QUIC版本1作为基础传输。其他 QUIC 传输版本与 SIP-over-QUIC 的使用可能由未来的规范来定义。QUIC连接的建立如[RFC9000]中所述。在连接建立期间,通过在 TLS 握手中选择 ALPN 令牌 “sips/quic “来表示支持 SIP-over-QUIC。对其他应用层协议的支持可能会在同一握手中提供。

作者说明:本文档的初衷是为了定义SIP的下一个主要版本,即SIPv3。因此,未来的ALPN令牌可以是sips/3或s3(类似于h2/h3)。QUIC版本1使用TLS版本1.3或更高版本作为其握手协议。目前,很多网络环境中,对于TLS-1.3版本支持正在部署中,可能一些应用仍然使用1.2版本。

SIP-over-QUIC用户代理必须支持一种机制,在TLS握手期间向服务器指示目标主机。如果目标用户代理服务器是由域名[RFC8499规范]识别的,客户必须发送服务器名称指示([SNI])TLS扩展,除非使用其他机制来指示目标主机。

SIP-over-QUIC消息是在可靠的QUIC流中传输的;因此,本文定义的SIP-over-QUIC协议是一个可靠的SIP传输。因此,使用 SIP-over-QUIC传输的客户和服务器事务处理必须遵循 [SIP2.0] 中定义的可靠传输的程序和定时器值。

2.2. 连接复用

SIP-over-QUIC连接在多个请求-响应事务中是持久的。一个SIP-over-QUIC连接也可以由多个并发的对话共享,每个对话都由To和From头的Call-ID头和tag=参数单独识别。

3. 通过QUIC传输表达SIP语义

3.1. QUIC客户端和服务器本规范引入了两个新术语:”传输客户端 “表示启动QUIC传输连接以交换SIP-over-QUIC消息的主机,而 “传输服务器 “则是其对等实体。这些定义术语与SIP的用户代理客户端(UAC)和用户代理服务器(UAS)的概念是有相关性的:传输客户端和服务器在SIP-over-QUIC连接中可以承担任何一个角色。这是需要特别注意的地方。

3.2. SIP事务帧数据格式

SIP-over-QUIC事务从UAC在请求流上发送的请求消息开始,这个请求流是一个双向的QUIC流;具体细节见第5节说明。如果提出请求的用户代理客户机正在从传输客户机端点访问传输连接,那么请求流是由客户机发起的双向QUIC流。如果提出请求的用户代理客户端从传输服务器端点访问传输连接,那么请求流将在服务器发起的双向QUIC流中传输。

每个SIP-over-QUIC事务都独占使用一个请求流。每个请求流只提出一个请求。UAS在与请求相同的流上发送零个或多个临时响应,然后是一个或多个最终响应。关于临时响应和最终响应的描述,见[SIP2.0]的第7.2节。

在一个给定的请求流中,收到多个请求时,此数据必须被视为畸形的数据。

一个SIP消息(请求或响应)由以下部分组成:报头部分,包括消息控制数据,在SIP-over-QUIC中作为单个HEADERS帧发送、如果出现消息体的话,可以选择消息正文,在SIP-over-QUIC中作为一系列数据帧发送。头信息在[SIP2.0]的第7.3节中描述。消息体在[SIP2.0]的第7.4节中描述。如果收到无效的帧序列必须被视为SIP_FRAME_UNEXPECTED类型的连接错误。特别是,在任何HEADERS帧之前,收到的DATA帧被认为是无效的。其他帧类型,特别是未知的帧类型,可能被允许,但要遵守它们自己的规则,具体细节参考第9章节。HEADERS帧可能引用对QPACK动态表的更新。虽然这些更新不是消息交换的直接部分,但在消息被销毁之前,它们必须被接收和处理。在发送请求后,UAC必须关闭发送流(见[QUIC-TRANSPORT]第3.4节)。在发送完最后一个最终响应后,UAS必须关闭流以进行发送。这时,QUIC流是完全处于关闭状态。当一个流被关闭时,这表示SIP-over-QUIC事务的完成。如果一个流在没有足够的请求提供完整响应的情况下终止了事务,UAS应该启用错误代码SIP_REQUEST_INCOMPLETE中止该流。

3.2.1. 请求的取消和拒绝

一旦请求流被打开,任何一个端点都可以根据[SIP2.0]第9章节中列出的的原因来取消这个请求。取消被分为渐进方式和突然终止的两种情况。端点可以通过使用RESET_STREAM帧重置流并使用STOP_SENDING帧中止接收该流上的后续数据,来突然取消任何请求,具体实现方式参考[QUIC-TRANSPORT]第2.4节中所述。如果响应不再有任何存在的价值,UAC应该通过使用CANCEL帧以渐进方式取消请求。例如,当UAC试图在多个端点联系单个用户时,并且UAC已经从一个端点收到它满意的最终响应时,UAC就开源采用渐进式方式取消其他的请求。UAC可以使用错误代码SIP_REQUEST_CANCELLED来突然取消请求。在收到这个错误代码后,如果没有进行任何进一步处理的话,UAS可能会使用错误代码SIP_REQUEST_REJECTED突然终止响应。UAC一定不能使用SIP_REQUEST_REJECTED错误代码,除非相应的UAS已经用这个错误代码请求关闭请求流。一个收到CANCEL请求(非帧格式数据)的UAS必须立即以405 Method Not Allowed错误响应该请求,如[SIP2.0]第21.4.6节中所述。用户代理服务器收到一个尚未打开的流的CANCEL帧时,必须被视为SIP_CANCEL_FRAME_CLOSED类型的连接错误。

作者说明:这是因为CSeq头已被删除,所以不能使用CANCEL请求方法。如果UAS不能或选择不能响应时,UAS就会取消请求。UAS的取消总是突然方式来取消。当UAS在不执行任何应用处理的情况下突然取消一个请求时,该请求被认为是 “拒绝”。在这种情况下,UAS应该以错误代码SIP_REQUEST_REJECTED中止其响应流。(在这种情况下,”处理 “意味着请求流中的一些数据被传递给上一层的应用软件,这些软件可能因此而采执行了其他的流程处理)。UAC可以把被UAS拒绝的请求视为未被发送的请求,并且可以在后续重新发送该请求。对于部分或全部处理的请求,UAS不得使用SIP_REQUEST_REJECTED错误代码进行处理。当UAS在部分处理后放弃一个响应时,它应该用错误代码SIP_REQUEST_CANCELLED中止其响应流。

3.2.2. 畸形的请求和响应

畸形的请求或响应是指在语法上有效的SIP-over-QUIC帧序列,但由于以下原因而导致其无效:

  • 一个无效的 SIP-over-QUIC 帧序列,如 DATA 帧在 HEADERS 帧之前、
  • 在一个HEADERS帧中存在被禁止的头域或伪头域、
  • 在HEADERS帧中没有强制性的伪头字段、
  • 一个HEADERS帧中的伪头字段的无效值、
  • 在一个HEADERS帧中,在标题字段之后的伪标题字段、
  • 在一个HEADERS帧中包含大写的标题字段名称、
  • 在HEADERS帧中的字段名或值中包含无效的字符。

一个已定义的请求或响应包含Content-Length头字段(见[SIP2.0]第18.3节)时,如果Content-Length头字段的值不等于收到的DATA帧长度的总和,则此请求或者响应将视为畸形数据。处理SIP-over-QUIC请求或响应消息的中介代理(如代理服务器)不得转发畸形的请求或响应。检测到的畸形请求或响应必须被视为SIP_MESSAGE_ERROR类型的流错误。UAS可能会响应一个畸形的请求,在关闭或重设流之前指示其错误。UAC一定不能接受畸形的响应。

3.3. SIP头字段

SIP消息携带的元数据是一系列称为 “SIP头字段 “的key-value对;见[SIP2.0]的第7.3节。关于已注册的SIP头字段的清单,请参见https://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml#sip-parameters-2 “会话初始协议(SIP)参数–头字段注册”。

3.3.1. SIP-over-QUIC报头压缩

[SIP2.0]第 7.3.3 节中描述的 SIP 头字段的缩写形式不得用于 SIP-over-QUIC。作为替代,头字段(包括头部分中的控制数据)使用[QPACK]编解码器进行压缩和解压缩。SIP-over-QUIC实现方式可以使用SETTINGS_MAX_FIELD_SECTION_SIZE参数,这个参数对它将接受的单个SIP消息的编码字段的最大size进行一个限定。与HTTP不同的是,在SIP中没有关于对头字段block size过大对应的响应代码。这表示说,如果头字段的block size 过大的话,没有相应的错误响应码来做回复。如果用户代理收到的编码字段大于它所承诺的可接受的字段,它必须将其视为SIP_HEADER_TOO_LARGE类型的流错误,并丢弃该响应。[QPACK]第4.2节描述了编码器和解码器流的两个单向流类型的定义。当与SIP-over-QUIC一起使用时,这些流类型的值是完全相同的,具体细节参考5.2章节。在[QPACK]附录A中定义的静态表是为HTTP设计的,因此它包含的头域对SIP端点来说没有太大意义。

本草案的附录B定义了一个必须由SIP-over-QUIC传输客户端和服务器使用的替代静态表。为了限定解码器对QPACK动态表的内存要求,解码器限制了编码器允许设置的动态表容量的最大值,具体细节参考[QPACK]第3.2.3节的规定。动态表的容量大小由解码器发送的SETTINGS_QPACK_MAX_TABLE_CAPACITY参数的值决定。可以通过将此值设置为零来禁用动态表的使用。如果两个端点都禁止使用动态表,那么端点之间就不应该打开编码器和解码器流。当使用动态表时,QPACK解码器可能会遇到一个已解码的字段,这个字段引用了一个未收到的动态表入口值,因为QUIC不能确保不同数据流中的数据接收顺序。在这种情况下,如[QPACK]第2.1.2节所述,该流被认为是 “阻塞 “的数据流。HTTP/3设置参数SETTINGS_QPACK_BLOCKED_STREAMS被复用为SIP-over-QUIC参数,通过接收方设置来决定最大流数量,这个数量是允许 “阻塞 “的,等待定动态表更新处理的数量。如果解码器遇到比它承诺支持的更大的阻塞流,它必须将其视为SIP_HEADER_DECOMPRESSION_FAILED类型的连接错误。可以通过发送Huffman编码的描述而不是更新QPACK动态表来避免流阻塞。

3.3.2. SIP控制数据

SIP-over-QUIC使用了一系列的伪头字段,字段名以:字符(ASCII 0x3a)开头。这些伪头字段传输消息控制数据,它取代了[SIP2.0]第7.1节中描述的Request-Line。这也是和RFC2161不同的一点。伪头字段不是SIP头字段。这个概念读者一定要搞清楚。终端不能生成在本文档中定义的伪头字段以外的字段。但是,通过一个扩展字段可以协商限定的修改;具体存在参考第9章节。伪头字段只在它们被定义的context中有效。针对请求定义的伪头字段决不能出现在响应中;针对响应定义的伪头字段也决不能出现在请求中。伪头字段决不能出现在消息的尾部。终端必须将包含未定义或无效的伪头字段的请求或响应视为畸形的请求或者响应。所有的伪头字段都必须出现在头部,并且在常规头字段之前。任何包含伪头字段的请求或响应,它们如果出现在常规头字段之后的,它们必须被视为畸形数据。

3.3.2.1. 请求伪头字段

以下是为请求定义的伪头字段:

  1. “:method”:包含SIP method。参见第6节,了解SIP-over-QUIC对SIP method的具体使用。
  2. “:request-uri”:包含 [SIP2.0] 第19.1节中描述的 SIPS URI。

所有SIP-over-QUIC请求必须包括:method和:request-uri伪头域的一个值。在SIP 2.0中的Request-Line中的SIP-Version要素被省略,所有SIP-over-QUIC请求都间接包含 “2.0 “协议版本

省略任何强制性伪头字段或包含这些伪头字段的无效值的SIP请求是畸形请求。

3.3.2.2. 响应伪头字段

对于响应,定义了一个单一的”:status “伪头字段,携带SIP状态码,见[SIP2.0]的第7.2节。所有SIP-over-QUIC响应都必须包括”:status “伪头字段的一个准确值。[SIP2.0]第7.2节中Status-Line结构的SIP-Version和Reason-Phrase元素被省略,所有SIP-over-QUIC响应都隐含有一个协议版本 “2.0”。如果需要说明,例如提供收到的状态码是用户可读字符串,可以从[SIP2.0]第21节中列出的状态码附带的原因值列表中推定出的Reason-Phrase获得支持。

3.3.3. Via传输参数

SIP消息中的Via头字段带有一个传输协议标识。本文定义了 “QUIC “这个值,用于通过QUIC传输的SIP-over-QUIC请求。这个参数的最新ABNF语法结构(Augmented Backus-Naur Form)[RFC5234]如下:

transport  =/  "QUIC"

一个完整的Via: 头的示例如下:

Via: SIP/2.0/QUIC wxp6O3dffes2.example.com;branch=z9hG4bKXG1gNkhgOiNR

3.3.4. SIP URI传输参数

本规范草案定义了值 “quic “作为SIP-over-QUIC URI[RFC3986]的传输参数值,其中用于发送SIP消息的传输机制将是QUIC传输,扩展了[SIP2.0]第19.1.1节中定义的参数名称。

该参数的更新ABNF语法如下:

transport-param =/ "transport=" "quic"

3.3.5. 事务序列号

SIP-over-QUIC终端一定不能使用CSeq头域(见[SIP2.0]第20.16节)。SIP-over-QUIC事务的正确顺序是由第5节中描述的QUIC流标识符推断出来的。中间代理服务器将SIP-over-QUIC消息转发到通过其他传输方式执行的SIP会话代理负责定义这些消息的CSeq头字段中携带的值,并将这些值映射到相反方向的相应的SIP-over-QUIC请求流。这里的中间代理相当于一个转义网关服务器或者SBC。

3.4. 连接存活

SIP-over-QUIC端点实体之间可以通过定期发送PING帧来保持其QUIC连接的活动和端口的开放状态,以延迟QUIC空闲超时,具体执行方式参考[QUIC-TRANSPORT]第10.1.2条所述。

4. 与早期的SIP版本兼容

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

在上面的例子中,发起方、被邀请方和 “代理A “的代理服务器它们都支持SIP-over-QUIC,但被称为 “代理B “的代理服务器只支持通过TCP/TLS和UDP的SIP/2.0。当代理服务器A试图连接到代理B时,它可能已经知道代理服务器B缺少对SIP-over-QUIC的支持,或者DNS SRV记录[RFC2782]或DNS服务绑定记录[DNS-SVCB]中的记录获知该服务器只支持通过TCP的SIPS服务来表示目前代理服务器支持的是SIP/2.0。关于安全降级的处理,参考RFC3261 

http://www.sip.org.cn/wp-content/uploads/2023/02/SIP-rfc3261-cn-james-zhu-v1.html#[300,%22XYZ%22,72,620.1,null]

如果代理服务器B只支持通过UDP的未加密SIP,那么代理服务器A一定不能通过未加密的协议转发安全SIP-over-QUIC的消息,因为这可能导致安全降级攻击。相反,如果不能通过代理B以外的方式联系到指定的被邀请者,那么代理A必须向该事务的发起者返回502 bad gateway的响应。在初始INVITE事务结束后,邀请方与被邀请人启动直接通信时,如果出现以下情况,应使用SIP-over-QUIC:

  • SIPS URI的DNS SRV记录表明被邀请者支持SIPS over UDP,或
  • SIPS URI的DNS服务绑定记录表明第2.1节所述的sips/quic ALPN标记,或
  • Contact: 头部字段表示一个携带 “quic “传输参数的SIPS URI,如第3.3.4节所述。

4.1. 事务处理

在[SIP2.0]中,与某个SIP事务相关的消息是通过请求和响应中的Via: header字段的分支参数来确定的。在SIP-over-QUIC中,使用用于传输事务的QUIC流来跟踪单个事务。SIP-over-QUIC终端可以省略这一参数。对于在SIP-over-QUIC和在其他传输协议上运行的SIP会话之间转换的中间代理,这些节点应该插入丢失的branch参数。为了避免泄露QUIC传输连接的细节,这些转换的branch参数一定不能是用于承载特定事务的流 ID 的明文文本表示,也不能是其它表达方式,这些方式可用于推断在特定 QUIC传输连接中使用的流ID的。

4.2. Dialog在[SIP2.0]中,dialog是通过使用Call-ID: 头字段和To: 和From: 头字段的tag=参数来跟踪的。目前的文档草案中没有引入任何其它的手段来跟踪dialog,因此Call-ID:和tag=值必须继续使用在SIP-over-QUIC对doalog的跟踪。

5. 流的映射和使用

QUIC流为该流上的字节数据提供了可靠和有序的传输,但不保证与其他流上的字节有关的传输顺序。QUIC流层的语义对SIP帧结构层是不透明的。传输层对收到的流数据进行缓冲和排序,向应用程序提供一个可靠的字节流。

虽然,QUIC允许在一个流中,数据传输可以不按顺序传输,但SIP-over-QUIC没有使用这一功能。QUIC流可以是单向传输,只从发起方到接收者传输数据,或者也可以是双向的,在两个方向上进行传输数据。在本规范中,双向流被用来传输SIP-over-QUIC请求和响应消息;单向流仅用于控制SIP-over-QUIC会话自己。双向流确保响应可以随时与响应对应的请求相关联。这些流被称为请求流。

最初SIP协议时,这个协议是基于不可靠的传输上运行,如UDP。因为QUIC保证了传输的可靠性,SIP/2.0的一些功能就不需要了或者可以省略。用户代理不能在请求或响应中发送CSeq头域,因为这些消息已经与QUIC流相关。在转发消息时将SIP-over-QUIC转换为其他传输协议的中介代理负责将CSeq头域的映射交给各个事务。关于CSeg的处理,因为涉及了比较复杂的事务流程,这里的定义如何处理仍然对用户来说是一个很大的挑战。因此,草拟发布作者也希望得到其它用户的反馈意见。

作者说明:作者请大家反馈与CSeq头有关的MUST NOT是否可以放宽到SHOULD NOT,或者是否可能存在作者没有发现的有效用例,如果有有效的场景用例,那么表示这个限定应该进一步放宽。如果使用[QPACK]动态表,那么[QPACK]第4.2节中描述的单向编码器和解码器流将在SIP-over-QUIC连接中运行。

5.1. 双向流

双向QUIC流被用于SIP请求和响应。这些流被称为请求流。

5.2. 单向流

SIP-over-QUIC使用单向的QUIC流。一个给定的的单向流的目的是通过一个流的类型来表示的,它在流的初始处,以一个可变长度的整数发送。流类型决定这个整数之后的数据格式和结构。

Unidirectional Stream Header {
  Stream Type (i),
}

Figure 2: Unidirectional Stream Header本规范草案中定义了一种流类型:控制流。另外,由[QPACK]第4.2节定义的HTTP/3流类型在SIP-over-QUIC中被映射为相同的值(0x2用于编码器流,0x3用于解码器流)。

此外,本文件预留了0x4的流类型值,以便将来作为媒体流使用。

每个SIP-over-QUIC用户代理必须为SIP-over-QUIC控制流创建至少一个单向的流。如果使用QPACK动态表,那么每个端点将各自开放两个额外的单向流。其他扩展可能需要更多的单向流。因此,两个端点发送的传输参数必须允许对等实体创建至少三个单向流。这些传输参数还应该为每个单向流提供至少1,024字节的流量控制开销。

如果数据流头字段指示了接收方不支持的流类型,接收方必须中止读取该流,丢弃传入的数据,并且执行后续处理,并以SIP_STREAM_CREATION_ERROR错误代码重置该流。接收方不得认为未知的流类型是任何类型的连接错误。

因为某些流类型会影响连接状态,接收方的用户代理不应该在读取流类型之前丢弃来自传入的单向流的数据。实现部署场景应该等待接收一个SETTINGS帧,这个帧数据描述他们的对等用户代理在发送该类型的流之前支持哪些流类型。

在获悉对等用户代理是否支持流类型之前,实现部署方式可以发送不修改的现有协议组件的状态或语义的流类型,但一定不要发送所支持的流类型(如QPACK)。除非另有规定,否则发送方可以关闭或重置一个单向流。接收者必须允许单向流在接收单向流头之前被关闭或重置。

5.2.1. 控制流

控制流是由0x00的流类型表示的。这个流上的数据由SIP-over-QUIC帧组成,参考第7节的定义。每个SIP-over-QUIC用户代理必须在连接开始时启动一个控制流,并将其SETTINGS帧作为该流的第一个帧发送。

如果控制流的第一帧是任何其他帧类型,这必须被视为SIP_MISSING_SETTINGS类型的连接错误。每个用户代理只允许一个控制流存在;如果收到第二个流宣称是控制流的话,将被视为SIP_STREAM_CREATION_ERROR类型的连接错误。控制流一定不能由发送方来关闭,并且,接收方不能请求发送方关闭控制流。如果任意节点时间关闭了控制流,则必须被视为SIP_CLOSED_CRITICAL_STREAM类型的连接错误。

关于连接错误说明将在第8章节中进一步介绍。因为控制流的数据内容是用来管理其他流的行为执行的,用户代理应该提供足够的流量控制开销,以确保对等实体的控制流不被阻塞。

6. SIP methods

在[SIP2.0]中描述的REGISTER、INVITE、ACK和BYE method继续在SIP-over-QUIC中运行,和它们在其他传输协议上运行的SIP中相同。

在SIP-over-QUIC中不得使用CANCEL method。如果需要取消SIP-over-QUIC请求,应该使用CANCEL帧,或者使用QUIC RESET_STREAM帧重置该请求的流([QUIC-TRANSPORT]第19.4条)。请注意,当时正在传输过程中的的SIP-over-QUIC消息仍然可能在对等体收到和处理取消之前到达流上。

作者说明:我没有对所有的SIP/2.0扩展及其对本文的适用性做全面的学习,所以我邀请大家对任何其他可能有问题的方法进行反馈。扩展处理仍然需要优化。

7. SIP帧数据层

SIP-over-QUIC帧在QUIC流中传输,如第5节所述。SIP-over-QUIC定义了一个单一的流类型:请求流。本节描述了SIP-over-QUIC的帧格式,概述见表1。

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

注意,与QUIC帧不同,SIP-over-QUIC帧可以支持在多个QUIC或UDP包持续。

7.1. 帧数据结构

所有的帧数据结构都有以下格式:

SIP-over-QUIC Frame Format {
  Type (i),
  Length (i),
  Frame Payload (...)
}

Figure 3: SIP-over-QUIC 帧格式

一个帧包括以下字段:

  • 类型:一个变量长度的整数,用于识别帧的类型。
  • 长度:一个变量长度的整数,描述帧有效载荷的字节长度。
  • 帧有效载荷:一个有效载荷,其语义由类型字段决定。

每个帧的有效载荷必须在描述中包含确切的字段。一个帧有效载荷在包含了已确定的字段之后,包含了其它字节,在确定的字段结束之前被终止的,那么,必须被视为SIP_FRAME_ERROR类型的连接错误。当一个流按照正常流程终止时,如果流上的最后一帧被截取,那么必须被视为SIP_FRAME_ERROR类型的连接错误来处理。突然终止的流可以在一帧的任何一点上被重置。

7.2. 帧定义

7.2.1. 数据DATA帧(type=0x00)只在请求流中发送。在数据字段中携带的帧有效载荷传递了与一个SIP-over-QUIC请求或响应消息相关的任意、可变长度的字节序列。

数据帧必须与一个SIP请求或响应相关联。

DATA Frame {
  Type (i) = 0x00,
  Length (i),
  Data (..)
}

Figure 4: DATA Frame

7.2.2. HEADERS/头

HEADERS帧(type=0x01)只在请求流中发送。它们被用来携带与SIP请求或响应消息相关的SIP头字段集合,如第3.3节所述。在编码字段部分携带的有效载荷,有效载荷是用[QPACK]编码的。

HEADERS Frame {
  Type (i) = 0x01,
  Length (i),
  Encoded Field Section (..)
}

Figure 5: HEADERS Frame

7.2.3. CANCELCANCEL帧

(type=0x02)只在控制流上发送,并通知接收方,其对等用户代理不想对相关双向流ID携带的消息做任何进一步处理。如果接收方已经完成了对此消息的处理,并且发送了响应,而且关闭了流的发送端,接收方必须丢弃这个帧。

CANCEL Frame {
  Type (i) = 0x02,
  Length (i),
  Stream ID (i)
}

Figure 6: CANCEL Frame发送方决不能用一个尚未被其对等实者确认的流ID发送这个帧。用户代理如果收到一个带有尚未打开的流ID的CANCEL帧,必须以SIP_CANCEL_STREAM_CLOSED错误类型的连接错误来响应。

7.2.4. SETTINGS

SETTINGS帧(type=0x04)只在控制流中发送。它传输涉及SIP-over-QUIC用户代理通信方式的配置参数,如对等行为的偏好和限定。这些参数总是适用于整个SIP-over-QUIC连接,而不会适用于单一的事务。一个SETTINGS帧必须作为每个对等用户代理的每个控制流的第一帧来发送,它不能在后续流程中发送。如果SIP-over-QUIC用户代理在控制流或任何其他流上收到第二个SETTINGS帧,用户代理必须以SIP_FRAME_UNEXPECTED类型的连接错误来响应。

SETTINGS参数不是协商的;它们描述了发送用户代理的特性,可以由接收用户代理使用。但是,可以通过使用SETTINGS来建议协商:每个用户代理使用SETTINGS来声明一组自己所支持的值。每个用户代理结合这两组值,得出将使用哪种选择值。注意,SETTINGS并没有提供一种机制来确定这些选择值的生效时间。其实,通过建议的支持数据来声明自己的支持能力,在SIP协议中也同样执行类似的流程,不过协商机制不同,在SIP协议和SDP的处理过程中进行双方的协商。

两个用户代理可以声明支持同一参数的不同值。相同的参数在SETTINGS帧中不得出现一次以上。接收方可以将重复的此设置标识符视为SIP_SETTINGS_ERROR类型的连接错误。一个SETTINGS帧的有效载荷由零个或多个参数组成。每个参数由一个参数标识符和一个值组成,都被编码为QUIC变长整数。

Parameter {
  Identifier (i),
  Value (i)
}

SETTINGS Frame {
  Type (i) = 0x04,
  Length (i),
  Parameter (..) ...
}

Figure 7SETTINGS Frame

部署方式必须丢弃任何带有它不理解的标识符的参数。

7.2.4.1. 定义的SETTINGS参数

下列参数是在SIP-over-QUIC中定义的:

SETTINGS_QPACK_MAX_TABLE_CAPACITY(0x01):

默认值为零。使用方法见第3.3章节。

SETTINGS_MAX_FIELD_SECTION_SIZE (0x06):

默认值为无限。使用方法见第3.3章节。

SETTINGS_QPACK_BLOCKED_STREAMS (0x07):

默认值为零。使用方法见第3.3章节。

8. 错误处理

当一个请求不能成功完成时,或者如果底层QUIC流有问题,QUIC允许应用协议突然重置该流并回复一个原因值(见[QUIC-TRANSPORT]第2.4节)。这种执行方式被称为 “流错误”。SIP-over-QUIC部署方式可以决定关闭一个QUIC流并且传递一个错误的类型。关于这类错误的错误解码在第8.1节中定义。流错误与表示错误状态的SIP状态码不同。流错误表示发送方没有传输或处理完整的请求或响应消息,而SIP状态代码表示接收方成功接收和处理请求的结果。

如果需要终止整个连接,QUIC同样提供了沟通理由的机制(见[QUIC-TRANSPORT]第5.3节)。这被称为 “连接错误”。与流错误类似,SIP-over-QUIC实现可以终止QUIC连接,并使用第8.1节中的错误代码回复原因值。

虽然这里被称为 “流错误”,但这并不一定表示作为一个整体,部署方式或连接上有问题。如果SIP响应的结果不再是用户代理客户端所需要的,流也可能被重置,参考第3.2.1节。第9节规定了扩展可以不经过协商来定义新的错误代码。一个未知的错误代码使用或在乱码中的已知的错误代码,必须被视为等同于SIP_NO_ERROR的错误。

8.1. SIP-over-QUIC错误代码

以下错误代码是在突然终止数据流、中止读取数据流或立即关闭SIP-over-QUIC连接时定义使用的错误码:

SIP_NO_ERROR (0x0300):无错误。当连接或数据流需要关闭,但没有错误信号时,就会使用这个信号。

SIP_GENERAL_PROTOCOL_ERROR (0x0301):对方用不同方式违反了协议要求,这种方式不匹配更具体的错误代码,或者终端拒绝使用更具体的错误代码。

SIP_INTERNAL_ERROR (0x0302):在SIP-over-QUIC堆栈中产生了一个内部错误。

SIP_STREAM_CREATION_ERROR (0x0303):端点侦测到它的对等实体创建了一个它不接受的流。

SIP_CLOSED_CRITICAL_STREAM (0x0304):SIP-over-QUIC连接所需的一个流被关闭或重置。

SIP_FRAME_ERROR (0x0305):收到一个不符合结构规范或数据size无效的帧。

SIP_FRAME_UNEXPECTED (0x0306):收到一个在当前状态或当前流上不支持的帧。

SIP_CANCEL_FRAME_CLOSED (0x0307):收到一个引用了未知流ID的CANCEL帧。

SIP_SETTINGS_ERROR (0x0309):终端在SETTINGS帧的有效载荷中检测到一个错误。

SIP_MISSING_SETTINGS (0x030a):在控制流的开始阶段没有收到SETTINGS帧。

SIP_REQUEST_INCOMPLETE (0x030d):终端的数据流在没有包含一个完整的请求的情况下就已经终止了。未完成接收请求流程。

SIP_REQUEST_REJECTED (0x030b):服务器拒绝了一个请求,没有进行任何应用处理。

SIP_REQUEST_CANCELLED (0x030c):请求或其响应被取消。

SIP_MESSAGE_ERROR (0x030e):SIP消息是畸形的,不能被处理。

SIP_HEADER_COMPRESSION_FAILED (0x0310):QPACK解码器未能解释一个已编码的字段部分,无法继续解码该字段。

SIP_HEADER_TOO_LARGE (0x0311):收到的编码字段部分大于接收方先以前允许的接受范围。见第3.3节。

9. 对SIP-over-QUIC的扩展

SIP-over-QUIC允许对协议进行扩展。在本节所述的限制范围内,协议扩展可以用来提供额外的服务或改变协议的任何方面。扩展仅在单个SIP-over-QUIC连接的范围内有效。

此扩展只适用于本文档草案中定义的协议元素。这不影响现有的扩展SIP的选项,如定义新的方法、状态码或头域。

允许扩展使用新的帧类型(第7.2节)、新的设置(第7.2.4.1节)、新的错误代码(第8.1节),或新的流类型(第5节)。

RFC编辑说明:为帧类型、设置、错误代码和流类型建立注册表。

实现部署方式必须忽略所有可扩展协议元素中的未知或不支持的值。这意味着这些扩展点中的任何一个都可以安全地被扩展使用,而无需事先配置或协商。但是,如果一个已知的帧类型被要求置于一个特定的位置,例如SETTINGS帧(见第5.2.1节),如果一个未知的帧类型不满足该要求,应该被视为一个错误。

某些扩展协议,例如,可能修改现有协议组件语义的扩展必须在使用前进行协商。例如,允许在双向QUIC流上复用其他协议(如媒体传输协议)的扩展,在对端用户代理发出可以接受的明确确认信令之前,不得复用这些协议。

本文档草案没有要求协商使用任何扩展的具体方法,但是,为了此目的,建议了一个参数(第7.2.4.1节)。如果两个对等用户代理都设置了一个表示愿意使用该扩展的值,那么该扩展就可以被使用。如果以这种方式使用参数,必须以这样的方式来定义默认值,即如果省略设置,则扩展被禁用。

10. 未来媒体会话的传输

作者注:本章节旨在促进讨论如何将SIP-over-QUIC建立和使用的QUIC传输连接的机制也可以应用于媒体流的传输。不属于文档草案的正式内容。

本规范的后续版本可能包括支持在与SIP-over-QUIC相同的QUIC传输连接中携带媒体会话,其目的是使用SDP offer/Answer 模式来协商。

目前已知的一些技术架构来尝试通过定义通过QUIC传输媒体,如[QRT]、[RTP-over-QUIC]、[QuicR-Arch]、[RUSH]和[Warp]。关于 RTP-over-QUIC, 更多细节用户可以参考:Media Over QUIC – Use Cases and Requirements for Media Transport Protocol Design。在针对媒体传输的创建中,目前比较受欢迎的包括:媒体互动(游戏,远程桌面,音视频会议),混合互动流媒体直播,按需媒体等场景。

游戏应用中的媒体属性:

最新SIP-over-QUIC: Session Initiation Protocol over QUIC和RTP传输概论

对于以QUIC数据包携带的媒体,用户代理不能建议使用该机制发送媒体,除非其对等实体通过[QUIC-DATAGRAMS]第3节中描述的max_datagram_frame_size参数表明其支持接收数据包。

在QUIC流中携带媒体的情况下,如果媒体流使用单向流传输,那么就需要定义新的流类型。本文档草案为此预留了流类型值0x04,参考第5.2节。在某些不太可能发生的场景中,如果媒体流使用双向流传输,则需要扩展流类型机制以支持双向流,因为本规范目前假设SIP-over-QUIC消息独享双向流。

10.1. QUIC传输会话中的RTP传输

[QRT]和[RTP-over-QUIC]都定义了在QUIC DATAGRAM帧上传输RTP和RTCP消息的方法。因为SIP和SDP已经与RTP媒体会话紧密耦合,调整SIP-over-QUIC,使其与RTP/RTC共存于同一QUIC传输连接中,这样可以节省一次网络往返(RTT)次数。

  • QRT只定义了一种在QUIC DATAGRAM帧中携带RTP和RTCP的方式。
  • RTP-over-QUIC定义了一种在单向QUIC STREAM帧以及QUIC DATAGRAM帧中携带RTP和RTCP的方式。

此外,QRT尝试定义SDP属性以允许在SIP中协商QRT会话。[SDP-QUIC]也描述了一组不同的SDP属性,以执行类似的任务。本文档或上述文档的后续版本可能会对信令指定一种机制,作为一个SIP-over-QUIC会话,用于在同样的SIP-over-QUIC连接中传输某个媒体会话。

10.2. 在QUIC传输会话中传输非RTP媒体流协议

[RUSH]没有规定一个手段来发现RUSH流媒体会话存在,也没有一种协商机制支持正在交换的媒体的编码参数。RUSH有两种操作模式:正常模式和多流模式。在正常模式环境中,如[RUSH]第4.3.1节所述,使用单一的双向QUIC流来发送和接收媒体流。在多流模式环境下,如[RUSH]第4.3.2节所述,使用一个双向QUIC流支持每个单独的媒体帧。双向流的使用其目的是为了给出错误反馈,而不是相反的处理方式,有一个单独分离的控制流来处理错误或使用QUIC传输错误机制来控制错误。如果第5.2节中描述的流类型机制被扩展后,这些控制也支持了双向流的话,那么SIP-over-QUIC就可以与RUSH一起使用。

[Warp]指定通过使用HTTP/3 WebTransport([WebTransH3])建立会话。但是,据作者所知,WebTransport还不包含任何类似于WebRTC使用SDP Offer/Answer模式的信令或媒体协商,因此,如果使用某种形式的会话创建机制的话,SIP-over-QUIC可能充当这个角色。Warp使用QUIC单向流来发送媒体。与MPEG-DASH类似,媒体是以ISO-BMFF “数据片 “的形式发送的,每个流携带一个数据片。这样可以很容易地被第5.2节中预留的媒体流类型所接受。

[QuicR-Arch]描述的SDP过于复杂,[QuicR-Proto]定义了QuicR Manifest用于声明媒体会话和终端能力,因此,SIP-over-QUIC可能不需要这样的处理方式。但是,试图从头开始设计这种机制,我们有可能需要额外的时间和精力来开发,SDP是一个完全可用的解决方案,复用SDP协商是一个比较好的办法。

11. 安全方面的考虑

SIP-over-QUIC的安全考虑应该与[SIP2.0]和[HTTP3]的考虑的是相同的。

SIP-over-QUIC依赖于QUIC,它本身依赖于TLS 1.3,因此默认支持[BCP195]中描述的针对降级攻击的保护措施。QUIC具体的问题及其防范措施在[QUIC-TRANSPORT]的第21节中描述。

以避免泄露潜在的QUIC传输连接的细节数据,在第4.1节给出了关于SIP-over-QUIC和通过其他利用branch 参数的传输协议来承载SIP之间的事务转换的具体指导。

总结

本草案是关于SIP-over-QUIC的一个初稿,在此初稿中规定了关于通过QUIC传输SIP信令的一些机制和语法,帧格式,请求响应和错误处理等核心内容。一些使用方式和SIP RFC3261 有一定的差异,用户在学习中要注意这些差异。

笔者通过首先介绍QUIC的背景,研究数据和性能,安全方面的讨论,说明了QUIC在当前互联网传输中的重要性。然后和读者分享了关于通过QUIC实现SIP传输机制的分享。在草案分享中,特别增加了部分关于SIP的一些核心概念,帮助用户容易理解SIP和其它的补充概念。

最后说明,虽然SIP-over-QUIC是一个未通过的草拟初稿,部署实时仍然需要各种终端,服务器端和一些中间代理服务器,SBC的支持和兼容。笔者的分享是为未来关于SIP网络传输的一个预研学习,希望能够抛砖引玉,帮助读者获得更多的关于行业发展的知识。

参考链接:

https://www.cisco.com

www.sip.org.cn

www.dinstar.cn

https://github.com/microsoft/msquic

www.asterisk.org.cn

https://www.ietf.org/proceedings/96

QUIC and TCP: A Performance Evaluation

https://github.com/Shenggan/quic_vs_tcp

https://www.rfc-editor.org/rfc/rfc9000.html

https://www.w3.org/

http://www.watersprings.org/pub/id/draft-gruessing-moq-requirements-02.html

https://www.ietf.org/id/draft-hurst-sip-quic-00.html

Efstratios Chatzoglou,Revisiting QUIC attacks: A comprehensive review on QUIC security and a hands-on study 

作者:james zhu | 来源:公众号——SIP实验室

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论