0b3109e043c4iOS - 网络处理iOS - 大杂烩

手把手NetworkExtension: 2. 分析官方 Dem

2017-04-20  本文已影响2837人  扎克Zach

在2015年, 苹果官方在 WWDC 2015 发布NetworkExtension API 和官方 DEMO 的时候非常激动人心, 当时众多开发者根据官方 DEMO 开发出了自己的 NE 相关的程序.

官方 demo 在这里

然而这么多年过去了, swift 版本也进化到了3.1版本, 对于我这种直接学 swift 3 的语法的人来说, demo能更新到3也是极好的.

更新历史

感谢苹果爸爸, 和程序猿们, 把DEMO也升级到了swift 3. 那么我们就下载来编译一下看看?

(图略)

一编译, 一百多个error, 大都是语法问题, 不确定保留的是2.x 的语法还是3.0的, 总之在目前的3.1版swift上, 疯狂的报错......

就在纠结要不要修正这块的error (估计工作量不小)时, 不幸中的万幸, 在github上发现了修正好的版本 --->github 地址
把代码下载来, 编译, 运行, 好, 本文结束了!

呵呵

好了, 不扯淡了, 还是讲讲原理, 以及我们本篇文章的目的: 了解到如何使用NEPacketTunnelProvider, 并且能够通信的原理.

首先说说 NEPacketTunnelProvider 是干什么的

1. NEPacketTunnelProvider 是干什么的?

官方是这么说的

官方说法

简而言之, 就是系统发送网络请求都要经过这货进行包装加密传输到我们连接到的通道服务器上. 也就是一个流量出关的关卡. 怎么包装, 怎么暗度陈仓, 怎么加密都在这里实现.

那也就是说, 我们可以在这里面建立一个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
}

首先将数据反序列化, 然后判断当前包的命令是什么.

/// 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 the packetFlow 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 建立到传输过程:

VPN 建立过程时序图 传输过程时序图

那么, 下一步就是根据这次分析的官方源码内容, 我们仿造一个简单的 VPN 服务器和通信过程的代码. 依然是基于上一篇文章的ProxyDump这个项目来做.

本篇文章到此结束.

转载请告诉我, 并注明来源, 谢谢

上一篇下一篇

猜你喜欢

热点阅读