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,我们可以到达PacketTunnelProviderhandleAppMessage()方法,它简单地应答了一个消息。值得注意的是,这个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:)并等待其完成再执行completionHandler。”

具体到startTunnel()的代码,它新建了一个ClientTunnel,设置其delegateself,并让其执行内部的startTunnel(),最后把completionHandler保存下来备用。

既然如此,我们把目光转向这个真正做事的ClientTunnel

ClientTunnel

ClientTunnel位于SimpleTunnelServices target中,其startTunnel()首先根据来自PacketTunnelProvider(它已被通过参数传递)的配置,创建好endpoint,它是一个NWEndpoint,包装了hostnameport。至于缺少信息而创建NWBonjourServiceEndpoint类型的endpoint就不理会了。

接下来就是最重要的一步,让PacketTunnelProvider通过createTCPConnectionToEndpoint:enableTLS:TLSParameters:delegate:发起到endpoint的TCP链接,并将链接记到connection上,且观察其state

那么我们就转到KVO的处理上。

observeValue方法中,当链接状态为connected,我们把remoteAddresshostname记下来为remoteHost;然后用readNextPacket()读取下一个包,并让delegate(也就是PacketTunnelProvider)知道tunnel打开了。其它状的处理暂时不理。

让我们转回到PacketTunnelProvider实现的tunnelDidOpendelegate方法,它利用tunnel建立了一个ClientTunnelConnection记为tunnelConnectionopen它。

那我们就再来看看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, …]这样的数据流,客户端再分析字典的内容作出不同的响应。

由此,我们就可以看看TunnelMessageKeyTunnelCommand了:

  1. public enum TunnelMessageKey: String {
  2. case Identifier = "identifier"
  3. case Command = "command"
  4. case Data = "data"
  5. case CloseDirection = "close-type"
  6. case DNSPacket = "dns-packet"
  7. case DNSPacketSource = "dns-packet-source"
  8. case ResultCode = "result-code"
  9. case TunnelType = "tunnel-type"
  10. case Host = "host"
  11. case Port = "port"
  12. case Configuration = "configuration"
  13. case Packets = "packets"
  14. case Protocols = "protocols"
  15. case AppProxyFlowType = "app-proxy-flow-type"
  16. }
  17. public enum TunnelCommand: Int, CustomStringConvertible {
  18. case data = 1
  19. case suspend = 2
  20. case resume = 3
  21. case close = 4
  22. case dns = 5
  23. case open = 6
  24. case openResult = 7
  25. case packets = 8
  26. case fetchConfiguration = 9
  27. public var description: String {
  28. switch self {
  29. case .data: return "Data"
  30. case .suspend: return "Suspend"
  31. case .resume: return "Resume"
  32. case .close: return "Close"
  33. case .dns: return "DNS"
  34. case .open: return "Open"
  35. case .openResult: return "OpenResult"
  36. case .packets: return "Packets"
  37. case .fetchConfiguration: return "FetchConfiguration"
  38. }
  39. }
  40. }

handlePacket里根据具体的command做switch:

例如对于data命令:再取出data并用targetConnection.sendData()“发出去”(如果字典指定了新的host和port就为UDP模式)。需要说明的是,targetConnection根据字典指定的identifier来确定,也就是说Tunnel里可以有多条connection,或者说,服务器可以区别不同的客户端。

不过要注意的是,ClientTunnelConnection并没有实现sendDatasendDataWithEndPoint,也就是说,数据