1 协议介绍
Real-Time Media Flow Protocol(简称RTMFP)是Flash和Flash之间基于UDP的点对点传输协议,由Adobe公司在2008年在Flash 10.0中发布,随后在Flash10.1中加入了Groups功能。
2 常见用法
rtmfp在Flash 10中的典型使用场景如下图:
它有如下特点:
l 使用Cirrus或者开源的Cumulus来提供Rendezvous服务
l Cirrus或者Cumulus并不提供Peer ID的交换服务,需要提供其它的方式来交换客户端之间的Peer ID
l Flash客户端之间使用NetStream来做点对点传输,Publisher需要给每一个Subscriber单独传输一份数据,这也限制集群的规模。
为了解决这个问题,Adobe在Flash 10.1中提出了Groups的概念,典型的架构如下:
它有如下特点:
l Cirrus或者开源的Cumulus提供Rendezvous服务并提供所有连接client列表
l client从Cirrus或者开源的Cumulus获取邻居节点之后,就可以组成一个完整的P2P架构,所有的audio、video和data数据都在peer之间交互。
3 协议解析
3.1 基本概念
l session:session是两个UDP地址之间的双向管道。
l flow:flow是从一个实体到另一个实体之间的逻辑路径。一个session可以包括多个flow。
l packet:网络中实际传输的数据,一个packet可以包含多个message。数据传输时都经过了128 bit的AES加密
l message:audio、video和data数据。
3.2 Scrambled Session ID
rtmfp协议中每个包的格式如下:
packet := scrambled-session-id | encrypted-part
其中scrambled-session-id是4字节,其后是经过AES加密的数据体。
scramble-session-id的生成规则如下:
scrambled-session-id = a ^ b ^ c
这里^代表XOR操作,a是session-id,b和c是encrypted-part的头8个bytes。
当目标收到这个包后,unscramble的操作如下:
session-id = x ^ b ^ c
其中x是scrambled-session-id,b和c同上。
使用scramble-session-id的目的为了减少数据包流经的NAT设备和layer-4 packet inspector对数据的干扰。
session-id用于标识通信双方建立的连接,并确定通信时使用的加密和解密的key,这些key是通过DH key exchange算法获得。但在session建立之前,双方使用一个公有加密key,即128 bit的字符串”Adobe System 02”。
3.3 raw part
encrypted-part经过解密之后就得到了raw-part,它的格式如下:
raw-part := checksum | network-layer-data | padding
其中checksum有16字节,network-layer-data是变长数据,padding都是0xFF,并把network-layer-data补齐为16字节的倍数,这是因为rtmfp使用的是16字节的加解密key。
checksum基于network-layer-data和padding计算。
3.4 network layer data
network-layer-data的格式如下:
network-layer-data = flags | timestamp | timestamp-echo | chunks
其中flags为1个字节,其格式如下:
7 6 5 4 3 2 1 0
TC TCR reserved reserved TS TSE mode
l mode:11代表握手包,01代表initiator发送包,10代表responder发送包,00不是合法值
l TSE:包中是否包含timestamp-echo域
l TS:包中是否包含timestamp域
l TCR:time critical reverse notification表明发送方正在从其它地方收到timecritical包
l TC:time critical forward notification表明发送方发送的是timecritical包
timestamp域有2字节,精度是4ms,他的计算方式如下:
timestamp = int(time * 1000 / 4) & 0xFFFF
timestamp-echo域是server收到包的时间戳,当发送放收到这个值之后,发送方就可以计算RTT值了。
chunk类型的格式如下:
chunk = type | size | payload
type字段为1个字节,其中0xFF不可用,这个是用来区分chunk数据和padding数据的标记。type的定义如下:
type |
meaning |
0x30 |
initiator hello |
0x70 |
responder hello |
0x38 |
initiator initial keying |
0x78 |
responder initial keying |
0x0f |
forwarded initiator hello |
0x71 |
forwarded hello response |
0x10 |
normal user data |
0x11 |
next user data |
0x0c |
session failed on client side |
0x4c |
session died |
0x01 |
reset keepalive request |
0x41 |
reset keepalive response |
0x5e |
negative ack |
0x51 |
some ack |
size是2字节payload长度。
payload根据type的不同有不同的数据体。
3.5 message flow
session中包括3类消息:
l handshake:握手包,包括initiator hello, responder hello, initiator initial keying,responder initial keying, responder hello cookie change和responderredirect
l control:控制包,包括ping, ping reply, rekeying initiate, rekeying response, close, closeacknowledge, forwarded initiator hello.
l flow:流消息,包括user data, next user data, buffer probe, user data ack, user dataack, flow exception report.
session的建立是通过握手(handshake)来完成的,正常的messageflow如下:
如果是在NAT打洞是,cumulus server就作为一个forwarder,他会把initiatro hello包转发到其它的client:
另外,cumulus server还可以让client重定向到其它server:
这里所说的client是Flash Player,而server是cumulus server或者Flash media server。当然server也可以给client发送initiator hello请求,这个在cumulus中被称为man in the middle,不过这个特性还不稳定。
session的建立包括4次握手:
1 initiator -> target:initiator hello
2 target -> initiator: responder hello
3 initiator -> target:initiator initial keying
4 target -> initiator: responder initial keying
这个4次握手过程可以阻止Dos攻击和syn-flooding攻击。
每个session都有一个session-id来唯一标识这个session,并且session中的每个packet都会包含这个session-id,但是在session建立的4个握手包中,initiator-hello, responder hello和initiator initialkeying的session-id字段都是0,在发送最后一个包responder initial keying时,session建立成功并且session-id确定,所以responderinitial keying包含合法的session-id。
我们接下来详细介绍一下这4个握手包
3.5.1 initiator hello
initiator hello包的格式如上所述,这里只说明payload部分的格式:
initiator-hello payload = first | epd type | epd value| tag
其中:
l first:1 byte magic number
l epd type:1 byte,只有两个合法值:
n 0x0a:client-server模式,epd value是想要连接的server的rtmfp url
n 0x0f:peer-to-peer模式,epd value是想要连接的client的peer id,一般是固定的32字节
l epd value:varlen + body
l tag:16 bytes随机数
3.5.2 responder hello
responder hello包的payload格式如下:
responder hello payload = tag-echo | cookie | responder-certificate
其中:
l tag-echo:和initiator hello中的tag一致,但和initiator hello中不同的是,这里在前面有一个varlen来表明tag的长度
l cookie:responder产出的64 bytes随机数,用来防止syn-flooding攻击
l responder certificate:diffie-hellman key exchange算法交换的信息,它的格式如下:
certificate= \0x01\0x0A\0x41\0x0E | dh-public-num | \0x02\0x15\0x02\0x02\0x15\0x05\0x02\0x15\0x0E
dh-public-num是一个64 byte(128 byte)随机数。
dh-public-num的生成规则为
y2 = g ^ x2 % p
其中g和p是公开的两个数,其中g等于2,p是一个1024 bits的数,x2是responder随机生成的数,y2就是在网络中传输的dh-public-num。
3.5.3 initiator initial keying
initiator initial keying包的payload格式如下:
payload = initiator-session-id | cookie-echo | initiator-certificate| initiator-component | ‘X’
其中:
l initiator-session-id:initiator选择的session-id,responder用它来发送数据给initiator(生成scrambled session id)
l cookie-echo:和上一个包中的cookie一致
l initiator-certificate:格式和上面的responder certificate一致
和上述的一样,这里的dh-public-num的生成规则如下:
y1 = g ^ x1 % p
其中g和p的定义和上述一致,x1是initiator随机生成的数,y1就是传输的dh-public-num。这时initiator知道了y2和x1,就可以生成sharedsecret:
shared secret = y2 ^ x1 % p
这时就可以生成这个session对应的加解密key了:
decode key = HMAC-SHA256(shared-secret, HMAC-SHA256(responder nonce,initiator nonce))
encode key = HMAC-SHA256(shared-secret, HMAC-SHA256(initiator nonce,responder nonce))
这些加解密key都只使用低位的128bit
l initiator-component:在DH算法中使用的initiator nonce。
3.5.4 responder initial keying
responder initial keying的payload的格式如下:
payload = responder session id | responder’s nonce | ‘X’
其中:
l responder session id:responder生成的session id,initiator用它来生成scrambled session id,这个值和initiator session id不一样。
l responder’s nonce:
这时responder知道了y1和x2,就可以生成sharedsecret:
shared secret = y2 ^ x1 % p
DH算法保证这个responder的sharedsecret和initiator的shared secret是一样的。
这时就可以生成这个session对应的加解密key了:
encode key = HMAC-SHA256(shared-secret, HMAC-SHA256(responder nonce,initiator nonce))
decode key = HMAC-SHA256(shared-secret, HMAC-SHA256(initiator nonce,responder nonce))
这些加解密key都只使用低位的128bit。
可以看到responder的encode key和initiator的decode key是一样的,同样,responder的decode key和initiator的encode key是一样的。
注意responder initial keying依然使用”Adobe System 02”作为对称key来加解密,而不是使用新生成的非对称的key来加解密,非对称的key仅在session建立之后使用。
3.5.5 user data
至此session就建立好了,后续传输的就是数据消息,主要包括两类:
l normal user data:正常的flow中数据消息
l next user data:和normal user data在一个packet中传输,不能单独使用。
normal user data包的payload格式如下:
payload = flags | flow-id | seq | forward-seq-offset | options |data
其中:
l flags:1 byte,各bit的意义如下:
bit |
meaning |
0x80 |
options域是否存在 |
0x40 |
|
0x20 |
这个包前面还有包 |
0x10 |
这个包后面还有包 |
0x08 |
|
0x04 |
|
0x02 |
丢弃包 |
0x01 |
结束包 |
l flow-id:flow标识,varlen类型
l forward-seq-offset:用于滑窗的标识,varlen类型
l options:一些选项
l data:audio、video和data数据
next user data包的payload格式如下:
payload = flags | data
字段定义同上