手把手NetworkExtension: 2. 分析官方 Dem
在2015年, 苹果官方在 WWDC 2015 发布NetworkExtension API 和官方 DEMO 的时候非常激动人心, 当时众多开发者根据官方 DEMO 开发出了自己的 NE 相关的程序.
官方 demo 在这里
然而这么多年过去了, swift 版本也进化到了3.1版本, 对于我这种直接学 swift 3 的语法的人来说, demo能更新到3也是极好的.
![](https://img.haomeiwen.com/i549820/23116d788614103e.png)
感谢苹果爸爸, 和程序猿们, 把DEMO也升级到了swift 3. 那么我们就下载来编译一下看看?
(图略)
一编译, 一百多个error, 大都是语法问题, 不确定保留的是2.x 的语法还是3.0的, 总之在目前的3.1版swift上, 疯狂的报错......
就在纠结要不要修正这块的error (估计工作量不小)时, 不幸中的万幸, 在github上发现了修正好的版本 --->github 地址
把代码下载来, 编译, 运行, 好, 本文结束了!
![](http://upload-images.jianshu.io/upload_images/549820-06fe4b07c6e69240.png)
好了, 不扯淡了, 还是讲讲原理, 以及我们本篇文章的目的: 了解到如何使用NEPacketTunnelProvider, 并且能够通信的原理.
首先说说 NEPacketTunnelProvider 是干什么的
1. NEPacketTunnelProvider 是干什么的?
官方是这么说的
![](http://upload-images.jianshu.io/upload_images/549820-b8922c052bb45651.png)
简而言之, 就是系统发送网络请求都要经过这货进行包装加密传输到我们连接到的通道服务器上. 也就是一个流量出关的关卡. 怎么包装, 怎么暗度陈仓, 怎么加密都在这里实现.
那也就是说, 我们可以在这里面建立一个TCP请求连接到我们的接受服务器做成一个通道咯.
2. 我们分析一下 DEMO SimpleTunnel的相关源码.
2.1 建立 TCP 连接
上一篇文章说过, 建立请求是发生在startTunnel
方法当中的. Demo 的源码如下
/// Begin the process of establishing the tunnel.
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
let newTunnel = ClientTunnel()
newTunnel.delegate = self
if let error = newTunnel.startTunnel(self) {
completionHandler(error as NSError)
}
else {
// Save the completion handler for when the tunnel is fully established.
pendingStartCompletion = completionHandler
tunnel = newTunnel
}
}
这里的ClientTunnel其实是在SimpleTunnelServices当中定义的一个客户端通道类.
首先新建了个通道, 然后发起通道请求.核心内容应该就是newTunnel.startTunnel(self)
了.
那么我们进去看看里面发生了什么.
/// Start the TCP connection to the tunnel server.
open func startTunnel(_ provider: NETunnelProvider) -> SimpleTunnelError? {
guard let serverAddress = provider.protocolConfiguration.serverAddress else {
return .badConfiguration
}
let endpoint: NWEndpoint
if let colonRange = serverAddress.rangeOfCharacter(from: CharacterSet(charactersIn: ":"), options: [], range: nil) {
// The server is specified in the configuration as <host>:<port>.
let hostname = serverAddress.substring(with: serverAddress.startIndex..<colonRange.lowerBound)
let portString = serverAddress.substring(with: serverAddress.index(after: colonRange.lowerBound)..<serverAddress.endIndex)
guard !hostname.isEmpty && !portString.isEmpty else {
return .badConfiguration
}
endpoint = NWHostEndpoint(hostname:hostname, port:portString)
}
else {
// The server is specified in the configuration as a Bonjour service name.
endpoint = NWBonjourServiceEndpoint(name: serverAddress, type:Tunnel.serviceType, domain:Tunnel.serviceDomain)
}
// Kick off the connection to the server.
connection = provider.createTCPConnection(to: endpoint, enableTLS:false, tlsParameters:nil, delegate:nil)
// Register for notificationes when the connection status changes.
connection!.addObserver(self, forKeyPath: "state", options: .initial, context: &connection)
return nil
}
看内容应该能看出个大概, 先判断一下外部传进来的地址是不是正常的 ip:port格式, 如果不是则用Bonjour服务来生成默认的地址. 没什么卵用.
如果是正常的, 那么我们用 IP 和端口新建一个Endpoint, 就是目标端点服务器. 之后新建一个连接到目标服务器的 TCP 连接. provider.createTCPConnection(to: endpoint, enableTLS:false, tlsParameters:nil, delegate:nil)
之后添加观察者观察这个连接的状态变化
2.2 我们看一下连接建立后都发生了什么
switch connection!.state {
case .connected:
if let remoteAddress = self.connection!.remoteAddress as? NWHostEndpoint {
remoteHost = remoteAddress.hostname
}
// Start reading messages from the tunnel connection.
readNextPacket()
// Let the delegate know that the tunnel is open
delegate?.tunnelDidOpen(self)
case .disconnected:
closeTunnelWithError(connection!.error as NSError?)
case .cancelled:
connection!.removeObserver(self, forKeyPath:"state", context:&connection)
connection = nil
delegate?.tunnelDidClose(self)
default:
break
}
连接建立之后, 读取服务器回传的数据. 目测是为了得到服务器分配的 IP 地址之类的信息. 其他两项很简单.
那么我们看一下这一段最关键的readNextPacket()
都做了些什么
/// Read a SimpleTunnel packet from the tunnel connection.
func readNextPacket() {
guard let targetConnection = connection else {
closeTunnelWithError(SimpleTunnelError.badConnection as NSError)
return
}
// First, read the total length of the packet.
targetConnection.readMinimumLength(MemoryLayout<UInt32>.size, maximumLength: MemoryLayout<UInt32>.size) { data, error in
if let readError = error {
simpleTunnelLog("Got an error on the tunnel connection: \(readError)")
self.closeTunnelWithError(readError as NSError?)
return
}
let lengthData = data
guard lengthData!.count == MemoryLayout<UInt32>.size else {
simpleTunnelLog("Length data length (\(lengthData!.count)) != sizeof(UInt32) (\(MemoryLayout<UInt32>.size)")
self.closeTunnelWithError(SimpleTunnelError.internalError as NSError)
return
}
var totalLength: UInt32 = 0
(lengthData as! NSData).getBytes(&totalLength, length: MemoryLayout<UInt32>.size)
if totalLength > UInt32(Tunnel.maximumMessageSize) {
simpleTunnelLog("Got a length that is too big: \(totalLength)")
self.closeTunnelWithError(SimpleTunnelError.internalError as NSError)
return
}
totalLength -= UInt32(MemoryLayout<UInt32>.size)
// Second, read the packet payload.
targetConnection.readMinimumLength(Int(totalLength), maximumLength: Int(totalLength)) { data, error in
if let payloadReadError = error {
simpleTunnelLog("Got an error on the tunnel connection: \(payloadReadError)")
self.closeTunnelWithError(payloadReadError as NSError?)
return
}
let payloadData = data
guard payloadData!.count == Int(totalLength) else {
simpleTunnelLog("Payload data length (\(payloadData!.count)) != payload length (\(totalLength)")
self.closeTunnelWithError(SimpleTunnelError.internalError as NSError)
return
}
_ = self.handlePacket(payloadData!)
self.readNextPacket()
}
}
}
调用targetConnection.readMinimumLength
做一个TCP连接的监听, 监听服务器的数据. 调用这个方法可以设置一次性读取多少数据, 这里我们设置成MemoryLayout<UInt32>.size, 也就是4个字节. 之所以这么操作, 是因为这个项目自己写了个协议.
2.3 自定义传输协议
通过分析Tunnel
类以及Tunnel.serializeMessage()
和 Tunnel.sendMessage()
等方法, 可以发现, 每一个数据包Packet当中, 前4个字节, 也就是 UInt32 长度的位置存放的是整个数据包的长度, 后边的内容就是数据包的真实内容Content, 而 Content 内容当中又用Dictionary来存放了不同的命令和内容. 一个Pakcet当中结构如下:
4 byte totalLength | n byte content data
而这部分data 解析出来是个Dictionary, 其中的 Key 是 enum TunnelMessageKey
. 其中有两条在发送请求数据时是必然存在的.
key | value |
---|---|
identifier | 新建tcp连接是生成. |
command | 当前包的命令. 参见enum TunnelCommand . |
2.4 监听并解析数据包
当收到数据时进入targetConnection.readMinimumLength
的代码块内.
先判断当前数据是否够4个字节. 如果不是这么长说明连接出问题了, 直接报错断开连接.
将包总长度赋予totalLength变量中. 并减去已经读完的标记位4字节大小.
剩下的这部分totalLength长度就是我们整个数据包后半部分存放真实数据的地方, 接着调用targetConnection.readMinimumLength
来读取真实数据内容
在第二层的读取数据的代码块当中可以看到, 跟第一层一样先判断长度, 然后调用self.handlePacket(payloadData!)
来处理数据包, 然后再调用readNextPacket()
递归监听读取下一个完整数据包.
2.5 处理数据包payload内容
在2.2的代码当中, 最关键的一步解析就在self.handlePacket(payloadData!)
里, 那么我们看一下这里都发生了什么. 我们稍微精简一下
/// Process a message payload.
func handlePacket(_ packetData: Data) -> Bool {
let properties: [String: AnyObject]
do {
properties = try PropertyListSerialization.propertyList(from: packetData, options: PropertyListSerialization.MutabilityOptions(), format: nil) as! [String: AnyObject]
}
catch {
simpleTunnelLog("Failed to create the message properties from the packet")
return false
}
...
switch commandType {
case .data:
guard let data = properties[TunnelMessageKey.Data.rawValue] as? Data else { break }
/* check if the message has properties for host and port */
if let host = properties[TunnelMessageKey.Host.rawValue] as? String,
let port = properties[TunnelMessageKey.Port.rawValue] as? Int
{
simpleTunnelLog("Received data for connection \(connection?.identifier) from \(host):\(port)")
/* UDP case : send peer's address along with data */
targetConnection.sendDataWithEndPoint(data, host: host, port: port)
}
else {
targetConnection.sendData(data)
}
break
case .suspend:
...
case .resume:
...
case .close:
...
case .packets:
if let packets = properties[TunnelMessageKey.Packets.rawValue] as? [Data],
let protocols = properties[TunnelMessageKey.Protocols.rawValue] as? [NSNumber], packets.count == protocols.count
{
targetConnection.sendPackets(packets, protocols: protocols)
}
break
default:
return handleMessage(commandType, properties: properties, connection: connection)
}
return true
}
首先将数据反序列化, 然后判断当前包的命令是什么.
- data: 这里是指定走哪个endpoint服务器的数据. 在AppProxy里面用到. 我们不做讨论
- packets: 是系统发送网络请求. 比如要访问某个网站, 将请求封装好放入data中, 发送给endpoint服务器进行解析转发
- default: 这里是处理其他请求用. 在客户端当中需要处理
.openResult
和.fetchConfiguration
的. 它的请求详情在ClientTunnel
当中
/// Handle a message received from the tunnel server.
override func handleMessage(_ commandType: TunnelCommand, properties: [String: AnyObject], connection: Connection?) -> Bool {
var success = true
switch commandType {
case .openResult:
// A logical connection was opened successfully.
guard let targetConnection = connection,
let resultCodeNumber = properties[TunnelMessageKey.ResultCode.rawValue] as? Int,
let resultCode = TunnelConnectionOpenResult(rawValue: resultCodeNumber)
else
{
success = false
break
}
targetConnection.handleOpenCompleted(resultCode, properties:properties as [NSObject : AnyObject])
case .fetchConfiguration:
guard let configuration = properties[TunnelMessageKey.Configuration.rawValue] as? [String: AnyObject]
else { break }
delegate?.tunnelDidSendConfiguration(self, configuration: configuration)
default:
simpleTunnelLog("Tunnel received an invalid command")
success = false
}
return success
}
对于客户端来说, 一个是处理服务器返回的连接已建立的数据包, 也就是鉴权完成. 另一个就是在服务器返回网络配置内容时配置好当前通道的网络情况, 比如dns, ip等. 这个在建vpn时是最关键的一步.
在本 DEMO 中, 新建 VPN 连接通道时, 先鉴权(demo 当中省略了), 然后服务器返回配置信息. 正确设置好配置信息后, VPN 算是真正建立起来了. demo 中配置这一步放在了 PacketTunnelProvider.tunnelConnectionDidOpen
中.
/// Handle the event of the logical flow of packets being established through the tunnel.
func tunnelConnectionDidOpen(_ connection: ClientTunnelConnection, configuration: [NSObject: AnyObject]) {
// Create the virtual interface settings.
guard let settings = createTunnelSettingsFromConfiguration(configuration) else {
pendingStartCompletion?(SimpleTunnelError.internalError as NSError)
pendingStartCompletion = nil
return
}
// Set the virtual interface settings.
setTunnelNetworkSettings(settings) { error in
var startError: NSError?
if let error = error {
simpleTunnelLog("Failed to set the tunnel network settings: \(error)")
startError = SimpleTunnelError.badConfiguration as NSError
}
else {
// Now we can start reading and writing packets to/from the virtual interface.
self.tunnelConnection?.startHandlingPackets()
}
// Now the tunnel is fully established, call the start completion handler.
self.pendingStartCompletion?(startError)
self.pendingStartCompletion = nil
}
}
这一步就只做了一件事情: 将配置信息传给系统, 也就是调用setTunnelNetworkSettings
方法, 完成建立 VPN 的过程.
NEPacketTunnelNetworkSettings
的配制方法都在createTunnelSettingsFromConfiguration
方法里, 这里就不贴出来了, 里面写的很直观.
3. 与服务器进行正常通信
上面我们分析的基本上是建立 TCP 连接前到配置网络信息的过程. 建立好 TCP 后, 服务器自动回传配置信息. 那么在用户产生请求时, 是怎么进行收发数据的呢?
IP packets with matching destination addresses will then be diverted to Packet Tunnel Provider and can be read using the
packetFlow
property. The Packet Tunnel Provider can then encapsulate the IP packets per a custom tunneling protocol and send them to a tunnel server. When the Packet Tunnel Provider decapsulates IP packets received from the tunnel server, it can use thepacketFlow
property to inject the packets into the networking stack.
官方已经说了, 通过packetFlow来收发系统/app 与服务器进行的通讯数据包.
理论上, 只要 VPN 一建立就需要监听往返的数据包了. 仔细观察一下2.5节中的最后一块代码区域, self.tunnelConnection?.startHandlingPackets()
, 从上下文和它的名字上来看, 应该是开始监听了吧? 我们看一下这个方法:
/// Make the initial readPacketsWithCompletionHandler call.
func startHandlingPackets() {
packetFlow.readPackets { inPackets, inProtocols in
self.handlePackets(inPackets, protocols: inProtocols)
}
}
果然不出所料, 在这里开始监听数据包了. NEPacketTunnelFlow.readPackets
根据官方说法, 是监听 虚拟网卡 TUN 发给PacketTunnelProvider的数据的. 而writePackets
就是反向操作. 我们看一下handlePackets
都做了什么
/// Handle packets coming from the packet flow.
func handlePackets(_ packets: [Data], protocols: [NSNumber]) {
guard let clientTunnel = tunnel as? ClientTunnel else { return }
let properties = createMessagePropertiesForConnection(identifier, commandType: .packets, extraProperties:[
TunnelMessageKey.Packets.rawValue: packets as AnyObject,
TunnelMessageKey.Protocols.rawValue: protocols as AnyObject
])
clientTunnel.sendMessage(properties) { error in
if let sendError = error {
self.delegate.tunnelConnectionDidClose(self, error: sendError as NSError?)
return
}
// Read more packets.
self.packetFlow.readPackets { inPackets, inProtocols in
self.handlePackets(inPackets, protocols: inProtocols)
}
}
}
通过前面几节的分析, 现在应该很容易就能理解这段内容. 将 TUN 发过来的数据打包封装成协议内容, 通过建立的TCP Tunnel发给服务器, 然后再次进入到监听状态, 这样循环的监听.
既然数据发给服务器了, 那么在哪里能收到服务器返回的数据呢?
回看2.2节的内容, readNextPacket()
里面的逻辑到最后一步也是递归自身循环监听服务器发过来的内容. 虽然2.2节当中和本节当中都调用了一个叫handlePackets
的方法, 但是两个地方调用的不是同一个方法. 本节中调用的是从 TUN 中读取到的内容进行处理, 而2.2节中是处理从服务器传回来的packet. 回看一下2.5节处理payload的内容, 其中.packets就是处理处理数据包的内容. targetConnection.sendPackets(packets, protocols: protocols)
这句是关键.
对于客户端来说, 这个connection应该是ClientTunnelConnection
这个类了. 进去找找:
/// Send packets to the virtual interface to be injected into the IP stack.
override func sendPackets(_ packets: [Data], protocols: [NSNumber]) {
packetFlow.writePackets(packets, withProtocols: protocols)
}
很简单的一步. 用packetFlow.writePackets(packets, withProtocols: protocols)
方法把数据传回 TUN, 让对应的 APP 收到数据.
4. 总结
到此, 整个 VPN 或者说是 TCP 连接建立到传递数据的逻辑源码讲完了. 下面用个时序图来简单归纳一下整个 VPN 建立到传输过程:
![](http://upload-images.jianshu.io/upload_images/549820-5fed18e536fd98d1.png)
![](http://upload-images.jianshu.io/upload_images/549820-bc31de782d94650d.png)
那么, 下一步就是根据这次分析的官方源码内容, 我们仿造一个简单的 VPN 服务器和通信过程的代码. 依然是基于上一篇文章的ProxyDump这个项目来做.
本篇文章到此结束.
转载请告诉我, 并注明来源, 谢谢