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

2017-03-07 10:53:02来源:http://nixwang.com/2017/03/05/airplay-without-bonjour/作者:Nix的自我修养人点击

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


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


Bonjour 简介

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


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


分配地址

设备会随机选择一个 IP,并且测试这个 IP 有没有占用,如果被占用就再随机选择一个 IP。


命名

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._tcpPTR 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 类型的,是一些服务参数。


具体的参数含义可以参考这个 非官方的 AirPlay 文档 。


使用 Wireshark 抓包可以看到:


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



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


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


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



最后,魔盒发出响应



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


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


因此整个过程是:


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

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


让 Bonjour 跨子网工作

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


配置 Unicast DNS 服务器 。
配置 Avahi 服务器在子网间转发 mDNS 请求, 思科也提供了这种解决方案 。
使用 DNS-SD Proxy。

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


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


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


DNS-SD Proxy 主要的思路是: 在 iPhone 上注册一个与魔盒提供的同样的服务,但服务的 Host 指向魔盒的 IP,达到欺骗 iPhone 的目的。 这样就解决了服务发现的问题。


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


注册完成后,手机上的 Control Center 上查看,并没有找到注册的 AirPlay 服务。在同子网的 Mac 上再次使用 dns-sd -Z _airplay._tcp 命令可以看到下面的结果:



_airplay._tcp PTR AirProxy._airplay._tcp
AirProxy._airplay._tcpSRV 0 0 7200 MagicBox_M16S-8f144d951aed0c2f.local. ; Replace with unicast FQDN of target host
AirProxy._airplay._tcpTXT ""


这里的 TXT 记录是空的,这里我们需要将前面魔盒服务的 TXT 记录复制出来。这个时候就要用到同样位于 DNSServiceRegister 所在头文件 dns_sd.h 中的 DNSServiceUpdateRecord 方法,签名是下面这样的:



DNSServiceErrorType DNSSD_API DNSServiceUpdateRecord
(
DNSServiceRef sdRef,
DNSRecordRef RecordRef,/* may be NULL */
DNSServiceFlags flags,
uint16_t rdlen,
const void*rdata,
uint32_t ttl
);


它可以用来修改 TXT 记录,调用方法如下:



NSDictionary *txtRecord = obj.txtRecord;
char rdata[1024];
int index = 0;
for (NSString *key in txtRecord.allKeys) {
NSString *keyValuePair = [NSString stringWithFormat:@"%@=%@", key, txtRecord[key]];
index += sprintf(rdata + index, "%c%s", (int)[keyValuePair length], [keyValuePair UTF8String]);
}
DNSServiceErrorType updateErrorCode = DNSServiceUpdateRecord(sdRef, NULL, flags, index, rdata, 0);


注意其中 rdata 参数的格式是 <键值字符串长度><键=值>。其中 txtRecord 是一个包含 TXT 记录内容的字典,内容跟前面抓取的魔盒的 TXT 记录相同,结构如下:



NSDictionary *airPlayTXT = @{
@"deviceid": <mac_address>,
@"features": @"0x4A7FFFF7,0xE",
@"flags": @"0x4",
@"model": @"HappyCast3,1",
@"srcvers": @"220.68",
@"vv": @"2",
@"pk": @"d02e124a749eef1ffbd11db9a6b84fd7e1f5ee7acdf1420e607e50e08e8ea950",
@"pi": @"2e388006-13ba-4041-9a67-25dd4a43d536",
@"pw": @"0",
@"rhd": @"3.0.0.0",
};


在 DNSServiceRegister 方法的回调中调用上面的方法就可以完成 TXT 记录的更新。这样就可以在 iPhone 上的控制中心里找到我们注册的 AirProxy 服务。


这个时候选择 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 记录:



这个时候,可以看到 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 的目的。


参考
Wan Bonjour
AirPlay without Bonjour on Enterprise Wireless Networks
What, exactly, is required to make Airplay work across VLANs?

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台