关于erlang:排查rtmp协议推流时握手bug

6次阅读

共计 5618 个字符,预计需要花费 15 分钟才能阅读完成。

转推流程序的过程:从一个观看地址拉流,而后推流到另一个推流地址。次要用于 cdn 之间转推,目前市面上大多数 cdn 厂商都违心不反对动静转推,因而只能通过转推流程序进行转推。

bug 景象:应用 obs studio 推流到微赞能够胜利,然而应用 Erlang 版本的转推流程序推流到微赞却失败。

日志如下:

14:12:35.926 [debug] payload [{amf,["onBWDone",0,null]}], msgtype[command_msg_4_amf0] 
14:12:35.949 [debug] play succ ======> url ["/live-sz/w1520993434573948"] 
14:12:35.949 [debug] {rtmp_msg,4,0,data_msg_4_amf0,1,{amf,["|RtmpSampleAccess",true,true]}}
14:12:35.949 [debug] {rtmp_msg,4,0,data_msg_4_amf0,1,{amf,["onStatus",{object,[{"code","NetStream.Data.Start"}]}]}}
14:12:36.038 [error] gen_server <0.122.0> terminated with reason: no match of right hand value <<0,0,0,0,0,0,0,0,113,142,194,240,185,25,41,180,242,33,5,112,128,97,178,8,79,179,28,53,152,242,82,43,234,104,113,246,170,189,182,146,122,36,155,3,152,180,226,122,36,97,52,67,53,158,107,170,178,119,209,132,40,233,102,182,142,233,218,71,55,8,121,67,117,58,130,91,107,224,202,5,1,132,37,245,143,231,20,198,121,204,57,80,102,165,104,245,79,71,254,169,15,3,166,12,148,45,24,62,253,66,93,139,84,139,54,236,47,5,98,95,51,231,222,144,8,153,232,166,227,151,57,98,214,63,238,167,212,49,51,160,83,248,246,199,...>> in rtmp_handshake:create_c2/2 line 61

<!–more–>

很显然是 rtmp_handshake:create_c2/2 函数呈现匹配谬误,对应代码如下:

-spec create_c2(C0C1, S0S1S2) -> Result when
          C0C1 :: iodata(),
          S0S1S2 :: binary(),
          Result :: {ok, C2},
          C2 :: iolist().
create_c2(C0C1, S0S1S2) when is_list(C0C1) ->
    create_c2(iolist_to_binary(C0C1), S0S1S2);
create_c2(<<_C0:1/binary, C1:16#600/binary>>, <<S0:1/binary, S1:16#600/binary, S2:16#600/binary>>) ->
    <<3>> = S0,
    case C1 of
        <<_:32, 0:32, _/binary>> ->
            S2 = C1,
            {ok, S1};
        _ ->
            {ok, S1DigestData} = verify_s1(S1),    
            DigestKey = crypto:hmac(sha256, ?C2_PUBLIC_KEY, S1DigestData),
            C2Len = 16#600,
            C2DigestDataLen = 32,
            RandomBin = random_binary(C2Len - 8 - C2DigestDataLen),
            {T1, T2, _} = now(),
            Epoch = <<(1000000 * T1 + T2):32/little>>,
            Data = [Epoch, binary:part(S1, 0, 4), RandomBin],
            S2DigestData = crypto:hmac(sha256, DigestKey, Data),
            {ok, [Data, S2DigestData]}
    end.

rtmp 握手过程中 C1 数据包匹配 <<_:32, 0:32, _/binary>> 格局后和 S2 数据包匹配不胜利,程序间接 crash dump。因而须要弄清楚 rtmp 握手过程中是否有对 S2 和 C1 进行匹配验证。

Rtmp 握手过程

此处重点关注 rtmp 握手过程中的 C1 和 S2 数据包。

先看官网文档中的握手过程,中文翻译版本能够参见:rtmp 标准 1.0。官网文档中对于是否要保障 C1 和 S2 完全一致,并没有明确说法。因而能够先参见 obs studio 依赖的 librtmp 库,看握手过程是如何解决的。obs studio 依赖的 librtmp 的代码如下连贯:

  1. https://github.com/obsproject…
  2. https://github.com/obsproject…

第一个链接 rtmp.c 中的代码是推流地址中没有加密串的状况下的握手过程代码,第二个链接 handshake.h 中的代码是推流地址中有加密串的状况下的握手过程代码。代码中应用条件编译 CRYPTO 宏来抉择编译不同的代码。其中 HandShake 函数属于客户端的握手函数,SHandShake属于服务端的握手函数。非加密版本具体 C 语言代码如下(已增加对应的中文正文进行阐明):

#ifndef CRYPTO
static int
HandShake(RTMP *r, int FP9HandShake)
{
    //C0,C1 -- S0, S1, S2 -- C2 音讯握手协定
    int i;
    uint32_t uptime, suptime;
    int bMatch;
    char type;
    char clientbuf[RTMP_SIG_SIZE + 1], *clientsig = clientbuf + 1;
    char serversig[RTMP_SIG_SIZE];

    clientbuf[0] = 0x03;  //C0, 一个字节。03 代表协定版本号为 3         /* not encrypted */

    uptime = htonl(RTMP_GetTime());  // 这是一个工夫戳,放在 C1 音讯头部  
    memcpy(clientsig, &uptime, 4);

    memset(&clientsig[4], 0, 4);  // 前面放 4 个字节的空数据而后就是随机数据  

    // 前面是随机数据,总共 1536 字节的 C1 音讯 
#ifdef _DEBUG
    for (i = 8; i < RTMP_SIG_SIZE; i++)
        clientsig[i] = 0xff;
#else
    for (i = 8; i < RTMP_SIG_SIZE; i++)
        clientsig[i] = (char)(rand() % 256);
#endif

    // 发送 C0,C1 音讯  
    if (!WriteN(r, clientbuf, RTMP_SIG_SIZE + 1))
        return FALSE;

    // 上面读一个字节也就是 S0 音讯,看协定是否一样  
    if (ReadN(r, &type, 1) != 1)    /* 0x03 or 0x06 */
        return FALSE;

    RTMP_Log(RTMP_LOGDEBUG, "%s: Type Answer   : %02X", __FUNCTION__, type);

    if (type != clientbuf[0])  //C/ S 版本不统一 
        RTMP_Log(RTMP_LOGWARNING, "%s: Type mismatch: client sent %d, server answered %d",
                 __FUNCTION__, clientbuf[0], type);

    // 读取 S1 音讯,外面有服务器运行工夫  
    if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
        return FALSE;

    /* decode server response */

    memcpy(&suptime, serversig, 4);
    suptime = ntohl(suptime);

    RTMP_Log(RTMP_LOGDEBUG, "%s: Server Uptime : %d", __FUNCTION__, suptime);
    RTMP_Log(RTMP_LOGDEBUG, "%s: FMS Version   : %d.%d.%d.%d", __FUNCTION__,
             serversig[4], serversig[5], serversig[6], serversig[7]);

    /* 2nd part of handshake */
    if (!WriteN(r, serversig, RTMP_SIG_SIZE))  // 发送 C2 音讯,内容就等于 S1 音讯的内容。return FALSE;

    // 读取 S2 音讯  
    if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
        return FALSE;

    // 服务端返回的 S2 音讯和 C1 音讯进行了比对,然而即便没有 match,也是返回 TRUE,只是打印了 log 
    bMatch = (memcmp(serversig, clientsig, RTMP_SIG_SIZE) == 0);
    if (!bMatch)
    {RTMP_Log(RTMP_LOGWARNING, "%s, client signature does not match!", __FUNCTION__);
    }

    /* er, totally unused? */
    (void)FP9HandShake;
    return TRUE;
}

static int
SHandShake(RTMP *r)
{
    ...
    return TRUE;
}
#endif

rtmp 握手过程中的确存在对 S2 和 C1 进行匹配验证的操作,然而这个操作并不影响握手是否是胜利的,只是增加了一条 warnning 日志而已。因而 obs studio 还是能推流胜利。绝对应的在咱们的转推流程序中,须要针对这个状况不进行强认证,删除掉匹配的操作即可。

抓包剖析

以微赞和网宿为例

  • obs 推流网宿握手胜利的包点此下载
  • obs 推流微赞握手胜利的包点此下载

网宿推流没有走加密流程,S2 和 C1 匹配,具体数据包截图如下:

微赞推流走加密流程,S2 和 C1 不匹配,具体数据包截图如下:

到此,整个 rtmp 推流握手过程就比较清楚了。

因而只须要将 Erlang 代码的流程更改下即可(删除 S2 和 C1 的匹配过程),见上面的 Erlang 代码:

-spec create_c2(C0C1, S0S1S2) -> Result when
          C0C1 :: iodata(),
          S0S1S2 :: binary(),
          Result :: {ok, C2},
          C2 :: iolist().
create_c2(C0C1, S0S1S2) when is_list(C0C1) ->
    create_c2(iolist_to_binary(C0C1), S0S1S2);
create_c2(<<_C0:1/binary, C1:16#600/binary>>, <<S0:1/binary, S1:16#600/binary, _S2:16#600/binary>>) ->
    <<3>> = S0,
    case C1 of
        <<_:32, 0:32, _/binary>> ->
          {ok, S1};
        _ ->
            {ok, S1DigestData} = verify_s1(S1),    
            DigestKey = crypto:hmac(sha256, ?C2_PUBLIC_KEY, S1DigestData),
            C2Len = 16#600,
            C2DigestDataLen = 32,
            RandomBin = random_binary(C2Len - 8 - C2DigestDataLen),
            {T1, T2, _} = now(),
            Epoch = <<(1000000 * T1 + T2):32/little>>,
            Data = [Epoch, binary:part(S1, 0, 4), RandomBin],
            S2DigestData = crypto:hmac(sha256, DigestKey, Data),
            {ok, [Data, S2DigestData]}
    end.

至此,转推流胜利,示例图如下:

论断

尽管 Adobe 公司本人出的 rtmp 协定不是 iso 规范的,然而你们这些公司好歹也尽量依照规定来啊,贼鸡儿坑。


参考:

  • obs-studio: https://github.com/obsproject…
  • obs studio 握手:https://github.com/obsproject…
  • RTMPdump(libRTMP)握手源代码:https://blog.csdn.net/leixiao…
  • rtmp 协定过程剖析:https://www.cnblogs.com/lidab…
  • librtmp 应用示例:https://github.com/leixiaohua…
  • rtmplib rtmp 协定过程剖析:https://www.cnblogs.com/lidab…

记得帮我点赞哦!

精心整顿了计算机各个方向的从入门、进阶、实战的视频课程和电子书,依照目录正当分类,总能找到你须要的学习材料,还在等什么?快去关注下载吧!!!

朝思暮想,必有回响,小伙伴们帮我点个赞吧,非常感谢。

我是职场亮哥,YY 高级软件工程师、四年工作教训,回绝咸鱼争当龙头的斜杠程序员。

听我说,提高多,程序人生一把梭

如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个激励,将不胜感激。

职场亮哥文章列表:更多文章

自己所有文章、答复都与版权保护平台有单干,著作权归职场亮哥所有,未经受权,转载必究!

正文完
 0