NetworkExtension3-Tunnel开发

环境

  • 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是连不上的,因为我们的代理服务器还没有开启~~

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335