跨子网、不依赖多播的 AirPlay 镜像

2018-02-07  本文已影响0人  nix1024

AirPlay 是苹果的一个私有标准,可以用来将 iDevice(iPhone、iPad、iPod) 上的音视频流或者镜像投射到 Apple TV 上。尽管 AirPlay 协议是私有的,但国内主流的机顶盒,如天猫魔盒、小米盒子等都对其提供了支持。

AirPlay 有一个很大的局限性:只能在 Apple TV(或者支持 AirPlay 的机顶盒)与 iDevice 处在同一子网内才能工作。之所以有这个限制,是因为 AirPlay 的服务发现部分基于 Bonjour

Bonjour 简介

Bonjour 是苹果开发的一种「零配置网络架构」,使得同一局域网内的主机能够相互发现彼此提供的服务,而不需要用户配置 IP 等信息。想象一下,将一台打印机接到局域网内,然后在电脑上就可以直接选择这台打印机。

Bonjour 为了实现「零配置」,做了三件微小的工作:

虽然是零配置网络,但实际主机之间的通信还是基于 TCP/IP,于是我们需要分配 IP 地址。传统的 IP 分配方式有两种:静态分配和 DHCP。

苹果增加了另外一种方式:在没有 DHCP 服务器时(如 Ad Hoc 网络),Bonjour 会为主机自动指定随机的一个 IP 地址,然后检测是否有冲突,如果有冲突就再随机指定一个。这样的好处是不依赖路由器,在去中心化的 Ad-hoc 网络中也能正常工作。

Bonjour 为服务(DNS service)指定一个唯一的类似 foo._airplay._tcp.local. 的名字,名字中间的字段表示服务的类型(_airplay)和传输协议(_tcp),后面的服务发现都基于这个名字。类似的,主机(也叫 host,跟西部世界没关系 :])也有唯一的名字,如 magicbox.local.,与服务不同的是,主机的名字没有类型和传输协议。具体命名规则见这里

当一台主机(host)接入局域网时,Bonjour 会自动生成一个局域网内唯一的名字。虽然 host 有了唯一的名字,但实际通信的时候还是需要 IP 地址,名字与 IP 地址的映射依靠 mDNS 来完成。

mDNS(multicast DNS) 不同于常见的 DNS,它是局域网内依赖多播(multicast)工作的 DNS 协议。他不需要独立的 DNS 服务器,当需要解析一个名字时,会向多播地址(224.0.0.251)的 5353 端口来发送查询请求,收到该请求的主机如果发现自己是要找的对象,就会发送回复。macOS、iOS 以及安装了 Bonjour 服务的 Windows 都会有一个 mDNSResponder 进程专门处理这些请求。开发者只需要将自己的服务注册到系统中,mDNSResponder 会自动完成服务发现工作,不需要处理具体的协议细节。

当用户需要某项服务时,Bonjour 会根据所需类型(如 AirPlay 的类型是 _airplay._tcp)来查找局域网内所有该类型的服务。每个服务对应一个 host,再通过 host 解析为实际的 IP 地址。

通过 DNS 发现服务的过程称为 DNS-SD(DNS Service Discovery)。

AirPlay 的服务发现过程

现在有一台 Mac 和一台天猫魔盒(支持 AirPlay)处在同一子网内,在 Mac 上点击右上角的 AirPlay 按钮,就会自动发现魔盒。

在 Mac 上可以用

dns-sd -Z _airplay._tcp
dns-sd -Z _raop._tcp

来查看两种类型的服务。

_airplay._tcp                                   PTR     zzzzzzz._airplay._tcp
zzzzzzz._airplay._tcp                           SRV     0 0 7200 MagicBox_M16S-8f144d951aed0c2f.local. ; Replace with unicast FQDN of target host
zzzzzzz._airplay._tcp                           TXT     "deviceid=b5:b7:1b:56:da:b9" "features=0x4A7FFFF7,0xE" "srcvers=220.68" "flags=0x4" "vv=2" "model=HappyCast3,1" "pw=0" "rhd=3.0.0.0" "pk=eb41e959a9ceea6a5d942c032492fdd60b14f6da148bcab48277fdbbfff18816" "pi=2e388006-13ba-4041-9a67-25dd4a43d536"

_raop._tcp                                      PTR     b5b71b56dab9@zzzzzzz._raop._tcp
b5b71b56dab9@zzzzzzz._raop._tcp                 SRV     0 0 6200 MagicBox_M16S-8f144d951aed0c2f.local. ; Replace with unicast FQDN of target host
b5b71b56dab9@zzzzzzz._raop._tcp                 TXT     "ch=2" "cn=0,1,2,3" "da=true" "et=0,3,5" "vv=2" "ft=0x4A7FFFF7,0xE" "am=HappyCast2,1" "md=0,1,2" "rhd=3.0.0.0" "pw=false" "sr=44100" "ss=16" "sv=false" "tp=UDP" "txtvers=1" "sf=0x4" "vs=220.68" "vn=65537" "pk=eb41e959a9ceea6a5d942c032492fdd60b14f6da148bcab48277fdbbfff18816"

可以看到,两种服务分别有三条 DNS 记录:第一条是 PTR 类型的,由服务类型指向服务名;第二条是 SRV 类型的,由服务名指向主机名;第三条是 TXT 类型的,是一些服务参数。

TXT 记录中具体的参数含义可以参考这个非官方的 AirPlay 文档

使用 Wireshark 抓包可以看到:

当点击右上角 AirPlay Icon 时,Mac 首先发出 query,查找 _airplay._tcp. 和 _raop._tcp. 类型的服务。

mDNS query

应该是由于缓存,query 中包含了两个 Answer:分别指向两个服务:b5b71b56dab9@zzzzzzz._raop._tcp.local 和 zzzzzzz._airplay._tcp.local。

其中,zzzzzzz 和 b5b71b56dab9@zzzzzzz 分别是是魔盒注册的两个服务名(默认使用天猫魔盒的名字),b5b71b56dab9 是魔盒的 mac 地址。

接下来,Mac 开始解析 b5b71b56dab9@zzzzzzz._raop._tcp.local 服务

SRV query

最后,魔盒发出响应

mDNS response

可以看到 answer 中的记录指明了 b5b71b56dab9@zzzzzzz._raop._tcp.local 服务指向的 host 是 MagicBox_M16S-8f144d951aed0c2f.local。

在 Additional records 里面,包含了一条 A 记录,其中 MagicBox_M16S-8f144d951aed0c2f.local 指向的 IP 地址是 192.168.43.1。

因此整个过程是:

  1. 通过服务类型搜索服务,得到服务名。
  2. 通过服务名找到 host 名称。
  3. 通过 host 名称得到 IP 地址。

Bonjour 依赖局域网上的多播,因此有两个缺点:Bonjour 无法跨越子网发现服务;企业或者学校等大型网络中,多播通常是被禁用的。

让 Bonjour 跨子网工作

让 Bonjour 跨子网工作有下面几种方法:

这里只介绍最后一种方法。

假设有一台 iPhone 和一台天猫魔盒处于不同的子网,或者在同一个子网但多播被禁用了,现在要将 iPhone 的屏幕投射到魔盒上。

AirPlay 的整个过程可以看做两个部分,服务发现(依赖 Bonjour)和实际连接、传输音视频流。因为实际连接的过程并不要求 iPhone 和魔盒在同一子网内,只要 IP 可达即可,所以现在的问题是服务无法被发现。

DNS-SD Proxy 主要的思路是:在 iPhone 上注册一个与魔盒提供的同样的服务,但服务的 Host 指向魔盒的 IP,达到欺骗 iPhone 的目的。iPhone 在搜索服务时会自问自答,因此不依赖局域网的多播,这样就解决了服务发现的问题。

苹果提供了 NSNetServiceDNSService 来注册服务,但是这两个库有一个缺点:无法指定 host(认为本机是提供服务的 host)。这里就需要使用更底层的 DNSServiceRegister 来注册服务,苹果提供了示例代码。这个方法可以传入 host 名称,这里我们填入 MagicBox_M16S-8f144d951aed0c2f.local.,端口跟前面魔盒的数据一致,名字分别叫 AirProxy 和 6c5ab5637001@AirProxy。

调用 DNSServiceRegister 时,第三个参数应该传入 kDNSServiceInterfaceIndexLocalOnly,否则无法正常发现和连接;此外还需要传入 TXT 记录。videoTXTDict 是一个包含 TXT 记录内容的字典,内容跟前面抓取的魔盒的 TXT 记录相同。

    NSDictionary *videoTXTDict = @{
                                   @"deviceid": deviceID,
                                   @"features": @"0x5A7FFFF7,0x1E",
                                   @"flags": @"0x4",
                                   @"model": @"AppleTV2,1",
                                   @"srcvers": @"220.68",
                                   @"vv": @"2",
                                   @"pk": @"8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
                                   @"pi": @"2e388006-13ba-4041-9a67-25dd4a43d536",
                                   @"pw": @"0",
                                   @"rhd": @"5.0.0.5",
                                   };

    TXTRecordRef videoTXTRecord;
    TXTRecordCreate(& videoTXTRecord, 0, NULL);
    for (id key in videoTXTDict.allKeys) {
        TXTRecordSetValue(& videoTXTRecord, [key UTF8String], strlen([videoTXTDict[key] UTF8String]), [videoTXTDict[key] UTF8String]);
    }
    
    DNSServiceRegister(&_sdRef, kNilOptions, kDNSServiceInterfaceIndexLocalOnly, [name UTF8String], "_airplay._tcp.", NULL, [host UTF8String], htons(self.port), TXTRecordGetLength(&videoTXTRecord), TXTRecordGetBytesPtr(& videoTXTRecord), RegisterReplyCallback, (__bridge void *)self);

然后用同样的办法注册 _raop._tcp. 类型的服务,这个时候选择 AirProxy 服务就会发现系统提示「无法连接到 AirProxy」。

把手机连上 Mac,输入

rvictl -s <iPhone 的 UUID>

再用 Wireshark 抓 rvi0 接口上的包,就会发现所有的 MDNS 包中都没有 A 记录,也就是说 Bonjour 搜索到服务,找到 host 之后,无法解析 host 的 IP。

这个时候再回到 dns_sd.h 中,就可以发现有一个用来在服务中增加记录的方法 DNSServiceAddRecord,调用方法如下:

NSArray *IPComponents = [[obj host] componentsSeparatedByString:@"."];
char rawData[5];
sprintf(rawData, "%c%c%c%c", (char)[IPComponents[0] integerValue], (char)[IPComponents[1] integerValue], (char)[IPComponents[2] integerValue], (char)[IPComponents[3] integerValue]);
        
DNSRecordRef recordRef = NULL;
DNSServiceErrorType errorCode = DNSServiceAddRecord(sdRef, &recordRef, flags, kDNSServiceType_A, strlen(rawData), rawData, 0);

这个时候再抓包,可以看到 MDNS 包中多了两条 A 记录:

A 记录

这个时候,可以看到 AirProxy._airplay._tcp.local 服务指向的 host 是 MagicBox_M16S-8f144d951aed0c2f.local.,但我们添加的 A 记录名称是 AirProxy._airplay._tcp.local 和 6c5ab5637001@AirProxy._raop._tcp.local。

这样在注册的时候把 host 名称改成 AirProxy._airplay._tcp.local 或者 6c5ab5637001@AirProxy._raop._tcp.local Bonjour 就可以正确解析了。

至此,就达到了欺骗 Bonjour 连接 AirPlay 的目的。

参考

上一篇 下一篇

猜你喜欢

热点阅读