- SimpleTunnel阅读笔记
- ClientTunnel
SimpleTunnel阅读笔记
SimpleTunnel是Apple官方出的关于NetworkExtension框架的demo。我想,阅读过后定有收获。
作者:@nixzhu
首先阅读README。
……
接下来观察Storyboard。
首先,VPN列表显示在ConfigurationListController,它通过reloadManagers()将所有的VPN配置加载为列表。通过segue,用户可以新增VPN配置、编辑已有VPN配置,最重要的是查看某个VPN的状态。
在StatusViewController中,被查看的VPN由targetManager表示(在VPN列表里准备segue时赋值)。其中,用户可以enable/disable VPN;在VPN enabled时,用户可以打开或关闭VPN。在viewWillAppear(_:)时,除了设置UI外,通过IPC给TunnelProvider(也就是PacketTunnelProvider扩展)发送了一个消息。消息内容并不重要,NETunnelProviderSession.sendProviderMessage()的文档说明,此消息意在唤醒PacketTunnelProvider扩展。两个UISwitch的target-action里处理了VPN的enable/disable,以及VPN的开关。其它还有监听VPN状态的通知等,主要是更新UI,不表。
通过搜索IPC message,我们可以到达PacketTunnelProvider的handleAppMessage()方法,它简单地应答了一个消息。值得注意的是,这个IPC通讯的过程用了simpleTunnelLog()(调用NSLog)来打印消息,应该有便于调试扩展的功效。
现在,我们来分析一下VPN建立的具体过程。
当用户按下开关,startVPNTunnel()被调用,它的文档里说:“此函数使用当前VPN configuration来开始一个VPN tunnel,VPN tunnel connection进程将马上开始,而此函数会立即返回。”
在PacketTunnelProvider中,我们可观察到startTunnel()方法,其文档说:“此方法被系统调用用于开始一个network tunnel。当Packet Tunnel Provider以nil参数执行completionHandler时,即通知系统它已经准备好处理网络数据。因此,Packet Tunnel Provider需要调用setTunnelNetworkSettings(_并等待其完成再执行completionHandler。”
)
具体到startTunnel()的代码,它新建了一个ClientTunnel,设置其delegate为self,并让其执行内部的startTunnel(),最后把completionHandler保存下来备用。
既然如此,我们把目光转向这个真正做事的ClientTunnel。
ClientTunnel
ClientTunnel位于SimpleTunnelServices target中,其startTunnel()首先根据来自PacketTunnelProvider(它已被通过参数传递)的配置,创建好endpoint,它是一个NWEndpoint,包装了hostname和port。至于缺少信息而创建NWBonjourServiceEndpoint类型的endpoint就不理会了。
接下来就是最重要的一步,让PacketTunnelProvider通过createTCPConnectionToEndpoint发起到
TLSParameters
endpoint的TCP链接,并将链接记到connection上,且观察其state。
那么我们就转到KVO的处理上。
在observeValue方法中,当链接状态为connected,我们把remoteAddress的hostname记下来为remoteHost;然后用readNextPacket()读取下一个包,并让delegate(也就是PacketTunnelProvider)知道tunnel打开了。其它状的处理暂时不理。
让我们转回到PacketTunnelProvider实现的tunnelDidOpendelegate方法,它利用tunnel建立了一个ClientTunnelConnection记为tunnelConnection并open它。
那我们就再来看看ClientTunnelConnection是个什么东西。其open方法里,利用clientTunnel(也就是初始化时传递的tunnel)的sendMessage发送了一个字典给tunnel服务器,这个字典里有identifier(随机生成)、值为open的command以及为值ip的tunnel type。sendMessage的实现是利用之前建立的TCP connection来write数据。
我们给服务器发送了消息,那怎么接收呢?就在之前我们没有细看的readNextPacket(),它一样是利用TCP connection来读取数据。我们知道,从TCP链接读取数据时会等待,因此,当我们发送了消息给服务器后,服务器若传回了数据,这个readNextPacket()才算真正开始做事。
通过阅读readNextPacket(),我们可以分析出:它先读取一个UInt32字长的数据,并生成totalLength,然后再读取totalLength-UInt32(MemoryLayout<UInt32>.size)这么多的数据,作为payloadData。在handlePacket(payloadData!)之后,最后再次调用自己readNextPacket(),接着接收新数据(没有数据时自然阻塞等待)。
那我们就再深入到handlePacket方法,看看payload到底是什么结构。不出意外,payload也是一个字典,只不过用了PropertyList编码。
基本上,我们现在就可以分析出这个自定义Tunnel的数据格式,客户端发送的是字典,并且利用serializeMessage方法变成[length, payload]发出,服务器传回的也是[length, payload, length, payload, …]这样的数据流,客户端再分析字典的内容作出不同的响应。
由此,我们就可以看看TunnelMessageKey和TunnelCommand了:
public enum TunnelMessageKey: String {case Identifier = "identifier"case Command = "command"case Data = "data"case CloseDirection = "close-type"case DNSPacket = "dns-packet"case DNSPacketSource = "dns-packet-source"case ResultCode = "result-code"case TunnelType = "tunnel-type"case Host = "host"case Port = "port"case Configuration = "configuration"case Packets = "packets"case Protocols = "protocols"case AppProxyFlowType = "app-proxy-flow-type"}public enum TunnelCommand: Int, CustomStringConvertible {case data = 1case suspend = 2case resume = 3case close = 4case dns = 5case open = 6case openResult = 7case packets = 8case fetchConfiguration = 9public var description: String {switch self {case .data: return "Data"case .suspend: return "Suspend"case .resume: return "Resume"case .close: return "Close"case .dns: return "DNS"case .open: return "Open"case .openResult: return "OpenResult"case .packets: return "Packets"case .fetchConfiguration: return "FetchConfiguration"}}}
handlePacket里根据具体的command做switch:
例如对于data命令:再取出data并用targetConnection.sendData()“发出去”(如果字典指定了新的host和port就为UDP模式)。需要说明的是,targetConnection根据字典指定的identifier来确定,也就是说Tunnel里可以有多条connection,或者说,服务器可以区别不同的客户端。
不过要注意的是,ClientTunnelConnection并没有实现sendData和sendDataWithEndPoint,也就是说,数据
