RTMP握手协议及lal RTMP握手实现解析

目录

在这里插入图片描述

1. RTMP握手解析

  1. 为了在保证握手的身份验证功能的基础上尽量减少通信的次数,RTMP握手发送顺序一般是:
|client|Server |
|---C0+C1---->|<--S0+S1+S2-- |
|---C2---->
  1. RTMP握手分为简单握手和复杂握手。

1. 简单握手

1. c0和s0

  1. version:版本号,固定为0x03
  2. 结构如下:

image.png

2. c1和s1

  1. times:4字节,包含一个timestamp,用于本终端发送的所有后续块的时间起点。
    1. 4字节时间戳一般以毫秒为单位,这个值也可以是0,也可以是任意值。
  2. zero:4字节,字段内容全是0。
    1. 通过4字节二进制串全0,服务端可以判断出是客户端使用的是否是简单模式。
  3. random-bytes:1528字节,可以是任意内容。
    1. 终端需要区分出响应来自发起的握手还是对端发起的握手。
    2. 内容随机,不需要加密。
  4. nginx-rtmp-module作为客户端时,c1中time使用的是当前unix时间戳的毫秒部分。 nginx-rtmp-module作为服务端时,如果判断客户端为简单模式,解析完c1中的时间戳后并没有使用这个时间戳。发送s1时,是将c1的1536字节原样返回的。
  5. 结构如下:

image.png

3. c2和s2

  1. time:4个字节,表示对端发送的时间戳。
    1. c2的time应该设置为s1中的time字段。
    2. s2的time应该设置为c1中的time字段。
  2. time2:4个字节,表示接收对端发送过来的时间戳。
    1. c2的time2应该设置为收到s1的时间点。
    2. s2的time2应该设置为收到c1的时间点。
  3. random-bytes: 1528字节,表示对端发送过来的随机数据。
    1. c2的random-bytes应该设置为收到s1的random-bytes。
    2. s2的random-bytes应该设置为收到c1的random-bytes。
  4. obs客户端(obs使用简单握手模式)和nginx-rtmp-module服务端握手,c1、c2、s1、s2的整个1536字节是完全相同的。
    1. 说明time和time2这些字段,nginx-rtmp-module并没有完全按照文档说的来做。
  5. 结构如下:

image.png

2. 复杂握手

1. hmac-sha256

  1. hmac-sha256算法,复杂模式会使用它做一些签名运算和验证。
  2. 这个算法的输入为一个key(长度可以为任意)和一个input字符串(长度可以为任意),经过hmac-sha256运算后得到一个32字节的签名串。
  3. key和input固定时,hmac-sha256运算结果也是固定的。

2. c0和s0

  1. version:版本号,固定为0x03

3. c1和s1

  1. 复杂握手将c1和s1划分成四个部分,根据key和digest的位置,分为两种方式。
  2. 第一种方式:
#schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes
  1. 第二种方式:
#schema1
time: 4bytes
version: 4bytes
digest: 764bytes
key: 764bytes
  1. 两种schema中,使用哪一种由客户端决定,服务端首先按照schema0解析,失败则再按照schema1解析。
  2. 结构解析如下:
  3. time:4字节,同简单模式,ffmpeg使用的是[0, 0, 0, 0]
    1. 4字节时间戳一般以毫秒为单位,这个值也可以是0,也可以是任意值。
  4. version:4字节,版本号,nginx-rtmp-module使用的是[0x0C, 0x00, 0x0D, 0x0E]。ffmpeg使用的是[9, 0, 124, 2]
  5. key:764字节,结构如下:
random-data: (offset) bytes
key-data: 128 bytes
random-data: (764 - offset - 128 - 4) bytes
offset: 4 bytes
  1. digest:764字节,结构如下:
offset: 4 bytes
random-data: (offset) bytes
digest-data: 32 bytes
random-data: (764 - 4 - offset - 32) bytes

4. c2和s2

  1. c2和s2主要用来对S1和C1的验证,长度同样为1536字节。
  2. 结构如下:
random-data: 1504bytes
digest-data: 32bytes
  1. random-data和digest-data都应来自对应的数据。

5. digest相关

1. digest位置
  1. c1和s1结构分为两种格式。
  2. 第一种方式:
#schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes
  1. 第二种方式:
#schema1
time: 4bytes
version: 4bytes
digest: 764bytes
key: 764bytes
  1. digest的位置可以在前半部分,也可以在后半部分。
  2. 当digest位置在前半部分时,digest的位置信息(offset)保存在前半部分起始位置。
  3. c1格式展开如下:
| 4字节time | 4字节版本号 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
// 取余728是因为前半部分的764字节要减去offset字段的4字节,再减去digest的32字节
// 12是因为要跳过4字节time + 4字节版本号 + 4字节offset
offset = (c1[8] + c1[9] + c1[10] + c1[11]) % 728 + 12
  1. offset的取值范围为[12,740)。
  2. 当digest在后半部分时,offset保存在后半部分的起始位置。
  3. c1格式展开如下:
| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
// 取余728是因为后半部分的764字节要减去offset字段的4字节,再减去digest的32字节
// +8+764+4是因为要跳过4字节time + 4字节版本号 + 前半部分764字节 + 4字节offset
offset = (c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3]) % 728 + 8 + 764 + 4
  1. offset的取值范围为[776,1504)
2. digest生成
  1. c1和c2中1528字节复杂二进制串生成规则:
    1. 将1528字节复杂二进制串进行随机化处理。
    2. 在1528字节随机二进制串中写入32字节的digest签名。
  2. 具体过程见代码解析。

2. lal中RTMP握手实现

  1. lalserver是纯Golang开发的流媒体(直播音视频网络传输)服务器。目前已支持RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]/WebSocket[S]-FLV/TS, GB28181协议。并支持通过插件形式进行二次开发扩展。
    在这里插入图片描述

  2. lal github 地址: https://github.com/q191201771/lal

  3. lal 官方文档地址:lal官方文档

  4. lal中每来一个rtmp连接,就会开启一个协程进行接收,(s *ServerSession) handshake() 函数负责rtmp握手。

  5. 相关原理参考上述RTMP握手解析介绍。

func (s *ServerSession) handshake() error {
    
    
	if err := s.hs.ReadC0C1(s.conn); err != nil {
    
    
		return err
	}
	Log.Infof("[%s] < R Handshake C0+C1.", s.UniqueKey())

	Log.Infof("[%s] > W Handshake S0+S1+S2.", s.UniqueKey())
	if err := s.hs.WriteS0S1S2(s.conn); err != nil {
    
    
		return err
	}

	if err := s.hs.ReadC2(s.conn); err != nil {
    
    
		return err
	}
	Log.Infof("[%s] < R Handshake C2.", s.UniqueKey())
	return nil
}

1. 服务端接收客户端发送的c0,c1 chunk

1. ReadC0C1(reader io.Reader) 函数解析

  1. ReadC0C1(reader io.Reader) 函数会解析c0c1 chunk,生成新的digest-data,如果digest-data长度不为0,则说明是复杂握手,否则是简单握手,同时会构造s0,s1,s2 chunk用于后续发送。
  2. 判断好简单握手还是复杂握手后,开始构造s0,s1,s2。
  3. s0占1字节,表示版本号,值为3。
  4. s1占1536字节,4字节time,另外:
    1. 对于简单握手:
      1. 接下来4字节为zero,都为0
      2. 最后1528字节为random-bytes,添加随机字符
    2. 对于复杂握手:
      1. 接下来4字节version,内容为:0x0D, 0x0E, 0x0A, 0x0D
      2. 最后1528字节为random-bytes,先往里边填充随机字符,然后按照digest key结构,获取digest的offset,根据36字节固定的key生成新的digest-data替换原先digest-data。
  5. s2占1536字节,分为简单模式和复杂模式:
    1. 对于简单模式:
      1. s2复制了c0c1 chunk中c1的内容
    2. 对于复杂模式:
      1. s2先填充1528字节的random-bytes,然后再根据parseChallenge()函数生成的新digest-data作为key,生成新的digest-data填充到s2的末尾32字节。
  6. 代码如下:
func (s *HandshakeServer) ReadC0C1(reader io.Reader) (err error) {
    
    
	c0c1 := make([]byte, c0c1Len)
	if _, err = io.ReadAtLeast(reader, c0c1, c0c1Len); err != nil {
    
     // 读取c0c1Len(1+1536)字节
		return err
	}

	s.s0s1s2 = make([]byte, s0s1s2Len) // 用于存储s0s1s2

	s2key := parseChallenge(c0c1, clientKey[:clientPartKeyLen], serverKey[:serverFullKeyLen]) //解析c0c1或者s0s1
	s.isSampleMode = len(s2key) == 0                                                          // s2key长度不为0,说明是复杂握手

	s.s0s1s2[0] = version

	s1 := s.s0s1s2[1:]
	s2 := s.s0s1s2[s0s1Len:]

	bele.BePutUint32(s1, uint32(time.Now().UnixNano())) // s1添加时间戳
	random1528(s1[8:])                                  // 填充1528字节随机字符

	if s.isSampleMode {
    
    
		//s1
		bele.BePutUint32(s1[4:], 0) // 简单握手zero4字节内容都为0

		copy(s2, c0c1[1:]) // s2复制c1内容
	} else {
    
    
		//s1
		copy(s1[4:], serverVersion) // s1添加version,服务端为:0x0D, 0x0E, 0x0A, 0x0D

		offs := int(s1[8]) + int(s1[9]) + int(s1[10]) + int(s1[11]) // s1使用 digest key结构,获取offset
		offs = (offs % 728) + 12                                    // 12 = 4字节time+4字节模式串+4字节offset
		// 填充s1内容,key为36字节固定key,生成新的32字节的digest-data填入到s1
		makeDigestWithoutCenterPart(s.s0s1s2[1:s0s1Len], offs, serverKey[:serverPartKeyLen], s.s0s1s2[1+offs:])

		// s2
		// make digest to s2 suffix position
		random1528(s2)

		replyOffs := s2Len - keyLen
		makeDigestWithoutCenterPart(s2, replyOffs, s2key, s2[replyOffs:]) // 将digest-data填充到s2的后32字节
	}
	return nil
}

2. parseChallenge(b []byte, peerKey []byte, key []byte) 函数解析

  1. parseChallenge()函数用于解析c0c1(也可解析s0s1)
  2. 首先会大端模式获取c0c1 chunk的第5~9字节作为version,如果version为0,说明是简单模式,否则则是复杂模式。
  3. 对于复杂模式,使用findDigest()函数先按照schema0格式(即key digest结构)进行查找digest-data的起始下标offs,如果没有找到,则按照schema1格式(即digest key结构)进行查找digest-data的起始下标offs。
  4. 当找到c1的digest-data的起始下标offset后,使用serverKey前36字节作为key,digest-data作为input,生成新的digest-data返回。
  5. serverKey内容为:
// 36+32
var serverKey = []byte{
    
    
	'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
	'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ',
	'S', 'e', 'r', 'v', 'e', 'r', ' ',
	'0', '0', '1',

	0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
	0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
	0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE,
}
  1. 代码如下:
// c0c1 clientPartKey serverFullKey
// s0s1 serverPartKey clientFullKey
func parseChallenge(b []byte, peerKey []byte, key []byte) []byte {
    
    
	//if b[0] != version {
    
    
	//	return nil, ErrRtmp
	//}

	ver := bele.BeUint32(b[5:]) // 大端,从下标5(c0 1字节 + c1 time4字节)开始读取4个字节,获取ver
	if ver == 0 {
    
                   // 如果ver = 0,说明是简单模式,复杂模式的ver不等于0
		Log.Debug("handshake simple mode.")
		return nil
	}

	offs := findDigest(b[1:], 764+8, peerKey) // 按照key digest结构进行查找digest-data的下标,time + version + key = 4 + 4 + 764
	if offs == -1 {
    
    
		offs = findDigest(b[1:], 8, peerKey) // 按照digest key结构进行查找
	}
	if offs == -1 {
    
    
		Log.Warn("get digest offs failed. roll back to try simple handshake.")
		return nil
	}
	Log.Debug("handshake complex mode.")

	// use c0c1 digest to make a new digest
	digest := makeDigest(b[1+offs:1+offs+keyLen], key) // 使用本端key和c0c1的digest-data生成新的digest-data并返回

	return digest
}

3. findDigest(b []byte, base int, key []byte) 函数解析

  1. findDigest()函数用于查找c1或s1的digest-data的起始位置offs。
  2. 参数base为按照key digest结构或者digest key结构中digest的offset的起始位置,见schema0和schema1结构。
    1. key digest结构中,base为764+8(4字节time + 4字节version + 764字节key)
      1. digest offset = c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3] + base + 4
    2. digest key结构中,base为8(4字节time + 4字节version)
      1. digest offset = (c1[8] + c1[9] + c1[10] + c1[11]) + base + 4
  3. 获取digest offs后,根据clientKey前30字节作为key,offs左边部分拼接上offs+len(digest[32字节])右边部分作为hmac-sha256的input,生成32字节的digest-data。
  4. clientKey内容为:
// 30+32
var clientKey = []byte{
    
    
	'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
	'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ',
	'0', '0', '1',

	0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
	0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
	0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE,
}
  1. 将生成的digest-data与原digest-data比较,相同则说明是复杂模式。
  2. 代码如下:
func findDigest(b []byte, base int, key []byte) int {
    
    
	// calc offs 
	offs := int(b[base]) + int(b[base+1]) + int(b[base+2]) + int(b[base+3]) // digest offset
	offs = (offs % 728) + base + 4                                          // offset + base + len(offset),移动offs到digest-data
	// calc digest 
	digest := make([]byte, keyLen)                    // digest-data为keyLen(32)字节
	makeDigestWithoutCenterPart(b, offs, key, digest) // digest-data生成
	// compare origin digest in buffer with calced digest
	if bytes.Compare(digest, b[offs:offs+keyLen]) == 0 {
    
     // 比较原digest和计算得到的digest是否相同,相同则为复杂模式
		return offs
	}
	return -1
}

4. makeDigestWithoutCenterPart(b []byte, offs int, key []byte, out []byte) 函数解析

  1. makeDigestWithoutCenterPart()函数用于拼接digest-data的左边部分和右边部分作为为hmac-sha256的input,根据给定的key生成新的digest。
func makeDigestWithoutCenterPart(b []byte, offs int, key []byte, out []byte) {
    
    
	mac := hmac.New(sha256.New, key) // 30字节固定key作为hmac-sha256的key
	//c1 digest左边部分拼接上c1 digest右边部分(如果右边部分存在的话)作为hmac-sha256的input(整个大小是1536-32)
	// left
	if offs != 0 {
    
    
		mac.Write(b[:offs])
	}
	// right
	if len(b)-offs-keyLen > 0 {
    
     // digest的random data部分
		mac.Write(b[offs+keyLen:])
	}
	// calc
	copy(out, mac.Sum(nil)) // hmac-sha256计算得出32字节的digest填入c1中digest字段中
}

2. 服务端发送s0,s1,s2 chunk给客户端

1. WriteS0S1S2(write io.Writer) 函数解析

  1. WriteS0S1S2()函数将s0,s1,s2 chunk发送给客户端。
func (s *HandshakeServer) WriteS0S1S2(write io.Writer) error {
    
    
	_, err := write.Write(s.s0s1s2)
	return err
}

3. 服务端接收客户端发送的c2 chunk

1. ReadC2(conn io.Reader) 函数解析

  1. ReadC2()函数会读取c2Len字节,读取不报错即算完成。
func (s *HandshakeServer) ReadC2(conn io.Reader) error {
    
    
	c2 := make([]byte, c2Len)
	if _, err := io.ReadAtLeast(conn, c2, c2Len); err != nil {
    
    
		return err
	}
	return nil
}

参考:
lal官方文档-rtmp handshake握手之简单模式和复杂模式

猜你喜欢

转载自blog.csdn.net/weixin_41910694/article/details/127406703