美文网首页OC-开发案例收集
NetworkExtension3-Tunnel开发

NetworkExtension3-Tunnel开发

作者: 梦即是幻 | 来源:发表于2020-08-28 11:36 被阅读0次

环境

  • Xcode 11.6
  • iOS 13
  • MacOS 10.15

导航

1-总览

2-Client开发

3-Tunnel开发

4-Server开发

5-App和Extension通信

完整代码在此,熟悉的小伙伴可以直接试试。

主App已经OK,下面就来看看Network Extension中的流程。

默认模板代码如下:

class PacketTunnelProvider: NEPacketTunnelProvider {

    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        // Add code here to start the process of connecting the tunnel.
    }
    
    override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        // Add code here to start the process of stopping the tunnel.
        completionHandler()
    }
    
    override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
        // Add code here to handle the message.
        if let handler = completionHandler {
            handler(messageData)
        }
    }
    
    override func sleep(completionHandler: @escaping () -> Void) {
        // Add code here to get ready to sleep.
        completionHandler()
    }
    
    override func wake() {
        // Add code here to wake up.
    }
}

简单说明:

  • startTunnel(options:,completionHandler:)方法:启动网络隧道,当主App调用startVPNTunnel()后执行;最后通过调用completionHandler(nil or error),完成建立隧道或由于错误而无法启动隧道。
  • stopTunnel(with reason:, completionHandler:)方法:停止网络隧道,当主App调用stopVPNTunnel()或其他原因停止网络隧道时候执行;如果想在PacketTunnelProvider内部停止,不能调用这个方法,应该调用cancelTunnelWithError()。
  • handleAppMessage(_ messageData: , completionHandler:)方法:处理主App发送过来的消息,主App可以通过let session = manager.connection as? NETunnelProviderSession,再调用session.sendProviderMessage(_ messageData: Data, responseHandler:)向tunnel发送数据,tunnel回调completionHandler返回数据。
  • sleep(completionHandler:)方法:当设备即将进入睡眠状态时,系统会调用此方法。
  • wake()方法:当设备从睡眠模式唤醒时,系统会调用此方法。

我们这里只需关心startTunnel方法,其他默认即可,第一步是拿到配置信息:

class PacketTunnelProvider: NEPacketTunnelProvider {

    private var pendingCompletion: ((Error?) -> Void)?
    private var config: YYVPNManager.Config!
    private var udpSession: NWUDPSession!
    private var observer: AnyObject?
    
    override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        os_log(.default, log: .default, "Starting tunnel, options: %{public}@", "\(String(describing: options))")
        do {
            guard let proto = protocolConfiguration as? NETunnelProviderProtocol else {
                throw NEVPNError(.configurationInvalid)
            }
            config = try YYVPNManager.Config(proto: proto)
        } catch {
            os_log(.default, log: .default, "Get configuration failed: %{public}@", error.localizedDescription)
            completionHandler(error)
        }
        
        os_log(.default, log: .default, "Get configuration: %{public}@", "\(String(describing: config))")
        
        pendingCompletion = completionHandler
        setupUDPSession()
        }
}

比较简单,这里要说明一下,扩展在另外一个进程,使用print是无法看到打印信息的,可以使用os_log,然后通过mac上的控制台app可以实时查看日志,如图:

image

连接UDP Server

继续之前最后2行代码:

pendingCompletion = completionHandler
setupUDPSession()

先用pendingCompletion持有completionHandler,因为必须要在连上Server后才能配置tunnel,拦截流量。

然后是setupUDPSession方法:

private extension PacketTunnelProvider {
    func setupUDPSession() {
        let endPoint = NWHostEndpoint(hostname: config.hostname, port: config.port)
        udpSession = createUDPSession(to: endPoint, from: nil)
        observer = udpSession.observe(\.state, options: [.new]) { [weak self] session, _ in
            self?.udpSession(session, didUpdateState: session.state)
        }
    }
    
    func udpSession(_ session: NWUDPSession, didUpdateState state: NWUDPSessionState) {
        switch state {
        case .ready:
            setupTunnelNetworkSettings()
            localPacketsToServer()
        case .failed:
                        os_log(.default, log: .default, "Connet UDP Server failed")
            pendingCompletion?(NEVPNError(.connectionFailed))
            pendingCompletion = nil
        default:
            break
        }
    }
}
  1. 创建createUDPSession,并监听状态
  2. OK后调用setupTunnelNetworkSettings()和localPacketsToServer(),配置tunnel,拦截本地流量

拦截本地IP包

要能拦截流量,需要2步:

首先,通过setTunnelNetworkSettings指定隧道的网络设置:开启一个虚拟网卡Tun接口,需要配置虚拟IP,DNS,代理设置,MTU和IP路由等信息。

然后使用packetFlow.readPackets即可从虚拟网卡文件中读取IP数据包再发送给代理Server。

tun虚拟网卡通过映射一个文件的方式从协议栈获取IP数据,或将数据写入协议栈,具体工作原理可以参考这个博客

image

最简单的配置如下:

private extension PacketTunnelProvider {
    func setupTunnelNetworkSettings() {
        let ip = "192.168.0.2"
        let subnet = "255.255.255.0"
        
        let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: config.hostname)
        /// 分配给TUN接口的IPv4地址和网络掩码
        let ipv4Settings = NEIPv4Settings(addresses: [ip], subnetMasks: [subnet])
        /// 指定哪些IPv4网络流量的路由将被路由到TUN接口
        ipv4Settings.includedRoutes = [NEIPv4Route.default()]
        settings.ipv4Settings = ipv4Settings
        
        setTunnelNetworkSettings(settings) { [weak self] error in
            self?.pendingCompletion?(error)
            if let error = error {
                os_log(.default, log: .default, "setTunnelNetworkSettings error: %{public}@", error.localizedDescription)
            } else {
                self?.remotePacketsToLocal()
            }
        }
    }
    
    func localPacketsToServer() {
        os_log(.default, log: .default, "LocalPacketsToServer")
        packetFlow.readPackets { packets, _ in
            os_log(.default, log: .default, "readPackets")
            packets.forEach {
                self.udpSession.writeDatagram($0) { error in
                    if let error = error {
                        os_log(.default, log: .default, "udpSession.writeDatagram error: %{public}@", "\(error)")
                    }
                }
            }
            
            self.localPacketsToServer()
        }
    }
}

从代理Server获取Response

  1. 监听udpSession数据回调。

  2. 使用packetFlow.writePackets将数据写入虚拟网卡,再通过协议栈通知应用层获取数据。

func remotePacketsToLocal() {
    udpSession.setReadHandler({ [weak self] packets, _ in
        if let packets = packets {
            packets.forEach {
                self?.packetFlow.writePackets([$0], withProtocols: [AF_INET as NSNumber])
            }
        }
    }, maxDatagrams: .max)
}

Network Extension中的代码也搞定了,不过这个时候点击Start是连不上的,因为我们的代理服务器还没有开启~~

参考链接

相关文章

网友评论

    本文标题:NetworkExtension3-Tunnel开发

    本文链接:https://www.haomeiwen.com/subject/gqflsktx.html