在今年的WWDC演讲中,苹果宣布FaceTime在web浏览器中可用,同时支持Android和Windows用户端。我们上次研究FaceTime还是六年前(详见此篇文章),现在是时候更新了。FaceTime肯定使用了WebRTC,这篇文章中我会给大家解释为什么很大程度上能确定它使用了WebRTC。
摘要
FaceTime Web确实在媒体服务中使用了WebRTC,并使用Insertable Streams API进行端对端加密。它还使用了一种有趣的方法来避免simulcast。
感谢Dag-Inge Aas抽出时间组织了一次会议,帮助我利用必要数据进行分析。那之后我的工具更加实用了。所以除了WebRTC内部转储之外,我还从Chrome日志中获取了RTP转储和SCTP转储,以及一些进行端对端加密(E2EE)的JavaScript。
加入会议
当我把邀请链接粘贴到浏览器上时,它要求我输入名字,然后加入会话。但接入需要由发起会话的人接受才行。另外到目前为止,好像还不能直接从web上发起会话。
设备测试急需能显示音频状态的仪器,我们实在要花太多时间才能弄清楚,是谁的麦克风没有正常工作。
在由WebSocket发送的二进制信号流量中,我可以找到Dag-Inge的电话号码。毕竟他的号码对我来说并不陌生。但你一定要谨慎决定给谁分享会话邀请。
URL
首先,让我们从Dag-Inge发送的URL开始。
https://facetime.apple.com/join#v=1&p=<a base64 encoded string>&k=<another base64 encoded string>
URL中hash部分的使用是意料之中的,毕竟去年Jitsi Meet中也使用了同样的技术进行端到端加密。hash的重要特性是它不会被发送到服务器上,所以成为了放置敏感数据(如加密密钥)的好地方。
p参数(“p” argument)会通过安全的WebSocket发送到服务器上,所以这很可能是一个公钥。接下来我们将进一步介绍端对端加密。
webrtc-internals分析
如果你想自己分析得出结论,可以在这里找到转储,然后使用导入工具导入。
getUserMedia约束
对于webrtc-internals转储,getUserMedia部分比较容易解释。getUserMedia用于获取麦克风和摄像头允许。视频约束要求具备面向用户的摄像头,和720像素的理想清晰度,没有指定宽度,但对于大多数相机来说,该要求最终捕获到的都是1280×720像素的视频流。之后通过使用track.applyConstraints方法,再将其裁剪为720×720的正方形图像。
RTCPeerConnection参数
RTCPeerConnection部分比较复杂。构造函数的参数实际表明,端到端加密中确实使用了Insertable Streams(现名为Encoded Transform)。
Connection:21-1 URL: https://facetime.apple.com/applications/facetime/current/en-us/index.html?rootDomain=facetime#join/
Configuration: "{
iceServers: [],
iceTransportPolicy: all,
bundlePolicy: max-bundle,
rtcpMuxPolicy: require,
iceCandidatePoolSize: 0,
sdpSemantics: \"unified-plan\",
encodedInsertableStreams: true,
extmapAllowMixed: true
}
"Legacy (chrome) constraints: ""
Signaling state: => SignalingStateHaveLocalOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable
ICE connection state: => checking => connected
Connection state: => connecting => connected
这里的统一计划sdpSemantics是默认的(如果你采用的仍然是其他方法,建议你尽快停止这样做)。rtcpMuxPolicy被设置为require,max-bundle的bundlePolicy也很标准。encodedInsertableStreams设置为true,是为了启用Insertable Streams API。它只在Chrome92或更高版本的chrome://webrtc-internals中显示。
该应用没有使用ICE服务器。这意味着如果UDP被屏蔽,一切都无法继续了。鉴于这种连接只与服务器对话,很多非限制性环境(比如很多企业)通常不使用ICE服务器。这样有限的传输选项,使得FaceTime不太可能成为The Verge所说“Zoom真正的竞争对手”。至少商业用例是如此。
Offer及answer交流
高级别的WebRTC流程如下所示。
一开始,客户端向服务器提供一个数据通道,然后服务器发送一个新的offer来添加音频和视频。在每个步骤中,添加到SDP中的媒体部分的数量(2、7、12…)是相当重要的,稍后会做解释。
SDP分析
第一个纯数据通道offer的answer是非常标准的(点击此处,查看之前在SDP研究文章中所述关于行的含义)。
v=0
o=- 6972089255627587584 6972089255627587584 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=ice-lite
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 127.0.0.1
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:1 1 udp 1 17.252.108.36 3483 typ host
a=ice-ufrag:iiTRM3DatXbD
a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM
a=ice-options:trickle
a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F
a=setup:passive
a=mid:0
a=sctp-port:5000
a=max-message-size:262144
a=rtcp-mux
a=rtcp-rsize
然而,它确实在没有任何意义的部分指定了rtcp、rtcp-mux和rtcp-rsize属性。此举并不明智。
ice-options是一个会话级别的属性,不属于媒体。这是由WebRTC发展产生的一个bug。
服务器是一个ice-lite服务器,所以没有端对端的连接。但Dag-Inge和我会以1-1的方式开始会话,之后才由他那边的第二个设备加入。这种使用直接端对端连接的会话模式通常被称为P2P4121,相当常见,但本篇文章不会涉及。
特别需要注意的一点是:控制DTLS 握手方向的设置属性为被动。这一点在我们观察统计数字时很重要。
视频参数
重点在于来自服务器的第一个offer。它在数据通道部分没有变化,而且服务器为每位参与者增加了三个视频部分和两个音频部分。
m=video 9 UDP/TLS/RTP/SAVPF 123
c=IN IP4 127.0.0.1
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:1 1 udp 1 17.252.108.36 3483 typ host
a=ice-ufrag:iiTRM3DatXbD
a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM
a=ice-options:trickle
a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F
a=setup:passive
a=mid:1
a=extmap:1 urn:3gpp:video-orientation
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:123 H264/90000
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 goog-remb
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42c01f
不出所料,视频部分使用了H.264基线配置文件42c01f作为唯一可用的编解码器。它们差别不大,且显示为recvonly,用于表明收发器是单向使用的。
RTX不用于重传,且我们能看到urn:3gpp:video-orientation扩展(它允许指定视频方向,这一点对于移动设备相当重要。因为这些设备在通话过程中的位置往往会发生变化)以及abs-send-time扩展。后者与我2013年所述,至今仍然流行的REMB带宽估计算法一起使用。较新的(2015年)transport-cc算法和扩展还没有被启用。感谢Harald促成了苹果在SDP中加“goog-remb”这一举措。
我们从视频部分的offer中领会的最后一点是:它缺少一个nack反馈机制(只有“nack pli”。对于WebRTC库来说可能没什么区别)。
音频参数
另一方面,两个音频m-line也很值得探讨。
m=audio 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 127.0.0.1
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:1 1 udp 1 17.252.108.36 3483 typ host
a=ice-ufrag:iiTRM3DatXbD
a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM
a=ice-options:trickle
a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F
a=setup:passive
a=mid:4
a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level vad=off
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 opus/48000/2
a=rtcp-fb:96 goog-remb
a=fmtp:96 ptime=60
m=audio 9 UDP/TLS/RTP/SAVPF 97
c=IN IP4 127.0.0.1
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:1 1 udp 1 17.252.108.36 3483 typ host
a=ice-ufrag:iiTRM3DatXbD
a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM
a=ice-options:trickle
a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F
a=setup:passive
a=mid:5
a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level vad=off
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=recvonly
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:97 opus/48000/2
a=rtcp-fb:97 goog-remb
a=fmtp:97 ptime=40;useinbandfec=1
Opus编解码器用于音频。它一定是人为添加到本地FaceTime客户端的(以前没有)。Natalie Silvanovich可能会像以前一样,研究一下此处潜在的漏洞,供我们探讨。
安装了iOS 15(但不支持E2EE方案)的设备才能加入通话,也说明了上述情况。
但我不得不说,我不理解把指纹、ice-ufrag和ice-pwd安排到每个媒体部分,而不是会话里的操作。这使相同的信息重复了许多次。
另外,这个offer的设置属性是被动的(来自之前的answer)。这直接违反了RFC 5763。奇怪的是,Chrome浏览器接受了该操作。这时就需要IETF协议来解决问题了。
说到header的扩展,我们观察到ssrc-audio-level有一个额外的指定符vad=off和abs-send-time扩展。ssrc-audio-level通常用于SFU中的主动式扬声器检测,FaceTime就在使用这种功能。
这意味着FaceTime会发送未加密的音频级数据。这是种效果不太理想但却很常见的做法。在Lennart Grahls对WebRTC的开发贡献中,我们可能会找到修复加密header扩展的方法。
使用vad=off在技术上是允许的。但这个功能很鸡肋,WebRTC库甚至没有实现它。如果你认为大有可为,那可以星标这个bug,但不要期望这个问题有什么进展。在文章的JavaScript部分我们会再次看到这个问题。
这里两部分音频的使用很有趣。他们有不同的编解码器规格。也都有Opus,但有效载荷类型(96和97)不同,打包时间一个是40毫秒,一个是60毫秒,比WebRTC通常使用的20毫秒大得多。此外,Opus inband FEC只对其中一部分音频启用。这可能是因为使用了两种不同有效载荷类型,并没有使用Opus–DTX–不连续传输。
如果通话是端对端加密的(这点并未说明,但使用 insertable streams 来提供另一加密层已经表明了这一点。希望FaceTime未来能发布一份像Google Duo那样详尽的白皮书。)
服务器的下一个offer是再增加五个媒体部分。此处有两点值得注意:
首先,新的媒体部分是sendonly,表明有新的参与者加入了通话。其次,FaceTime相当微妙地在每个部分增加了带有cname的a=ssrc行(不清楚他们为什么这样做。但这意味着远程端现在可以使用这些SSRCs发送数据。具体位置如下:
a=ssrc:3633224106 cname:3243262565732983040
a=ssrc:2993385163 cname:3243262565732983040
a=ssrc:4150121997 cname:3243262565732983040
a=ssrc:1095248263 cname:3243262565732983040
a=ssrc:2285848791 cname:3243262565732983040
很明显,这几行代码的cname相同。表明它们来自同一个参与者(解码RFC 3550语言即可得出该结论)。这有点像一个不太理想的simulcast,但实际上是一个相当有趣的黑客操作。这一点会在揭秘JavaScript时详述。
这些cnames似乎都是数字,其中一个甚至是负数,这样的选择很奇怪。
其余的WebRTC API调用就是常规操作了——在第三个设备加入时又增加了五个媒体部分。
下面我们来看转储的数据部分。
WebRTC数据
chrome://webrtc-internals收集的数据信息向我们展示了很多应用程序相关的行为,比如比特率。
该数据也能告诉我们一些加密相关的信息。我们能看到DTLS交换中使用的证书种类(即现行标准ECDSA证书)和SRTP密码,即AEAD_AES_256_GCM,这是一个去年在Chrome中启用的密码套件。因为它不是默认的密码套件,所以只在某些情况下启用。这也解释了SDP中使用被动设置的原因,详情请见chrome bug。看来FaceTime的开发者也读过bug这篇文章。或者说他们也具备文章中的这些知识。
在传入的音频数据中,我们可以看到适量的前向纠错(FEC),没有丢包。
虽然Opus FEC在冗余方面的表现不如RED好,但它确实提升了低丢包情况下的弹性。数据包率低于通常的每秒50个。但这可以理解。因为帧比较大(40和60ms。并不是20ms;且较大的帧增加了延迟,降低了成本)。传入的比特率约为50 kbps,比传出的30 kbps带宽略高。
下图表明,每次只能发送一个音频SSRCs,说明有一些类似于音频通话的行为出现。
稍后我们会在RTP转储中说明这一点。
在下图中,我们可以看到类似视频通话的行为。
通常此处是simulcast,但图中传递给我们的信息并不是这样。比如分辨率方面,我们看到了三种不同的分辨率——192×192、320×320和720×720。带宽估算缓慢上升的同时,流从低分辨率流(中间位置)升至中等分辨率(顶部位置),最后到达720×720层这一顶端,连接稳定,带宽大约750 kbps。
通常情况下,上图操作是使用simulcast完成的。FaceTime的开发者似乎有意避开这一做法,转而使用JavaScript级别的黑客完成这一操作。
我们可以看到,用于出站流的分辨率与入站流相同。转储中的比特率表现不太正常,表明在通话期间有相当长的时间没有发送视频。当然,与任何新的设置一样,我们找到了可以解释这种行为的操作。
高分辨率流的目标比特率差不多是800 kbps左右,320×320流是200 kbps,192×192缩略图是60 kbps。这样的流程只会在通话的特定阶段(比如当第三个设备加入时?)被发送,之后就变成就会同其他流氓一起发送了。
webrtc-internals没有跟踪RTCRtpSender.setParameters调用的功能,该功能可以让大家更深入了解获得这种行为所需的高级操作。希望能有人无偿提供一个补丁。
看过统计数据后,我们接着讨论JavaScript。