IOS NetWorkExtension - niancan/doc GitHub Wiki

NetworkExtension

NetworkExtension是苹果提供的用于配置 VPN 和定制、扩展核心网络功能的框架。NE 框架提供了可用于定制、扩展 iOS 和 MacOS 系统的核心网络功能的 API。

Network Extension 最早出现在 iOS 8,不过那个版本不支持虚拟网卡,只能简单调用 iOS 系统自带的 IPSec 和 IKEv2 协议的 VPN。在 iOS 9 中,开发者可以用 NETunnelProvider 扩展核心网络层,从而实现非标准化的私密VPN技术。最重要的两个类是 NETunnelProviderManagerNEPacketTunnelProvider

初始化项目

  • 新建项目
  • 建立普通 Swift 项目 Dsocket.
  • Network Extension 无法在模拟器上调试。同时,你得有开发者账号,用来申请相关 Capabilities.
  • 新建 PacketTunnel
  • 新建 Target,选择 Network Extension。

然后选择 Provider Type 为 PacketTunnel。

如果 containing app 要与 extension 共享数据,则必须要开启 App Groups。

Personal VPN 和 Network Extensions(App Proxy、Content Filter、Packet Tunnel)也当然要开启。

代码

NETunnelProviderManager

它和 vpn 设置是一一对应关系。如果 App 有两个 vpn 设置,我们通过代码就能得到两个 NETunnelProviderManager 实例。我们要通过代码,对 NETunnelProviderManager 做四种操作。

  1. 创建 vpn 配置
fileprivate func createProviderManager() -> NETunnelProviderManager {
    let manager = NETunnelProviderManager()
    manager.protocolConfiguration = NETunnelProviderProtocol()
    return manager
}
  1. 保存 vpn 配置
manager.saveToPreferences {
    if let error = $0 {
        complete(nil, error)
    } else {
        manager.loadFromPreferences(completionHandler: { (error) -> Void in
            if let error = error {
                complete(nil, error)
            } else {
                complete(manager, nil)
            }
        })
    }
}

这段代码执行时会请求用户的授权,允许之后会添加一份 vpn 的配置 3. 开启和关闭 vpn

fileprivate func startVPNWithOptions(_ options: [String : NSObject]?, complete: ((NETunnelProviderManager?, Error?) -> Void)? = nil) {
    // Load provider
    loadAndCreateProviderManager { (manager, error) -> Void in
        if let error = error {
            complete?(nil, error)
        } else {
            guard let manager = manager else {
                complete?(nil, ManagerError.invalidProvider)
                return
            }
            if manager.connection.status == .disconnected || manager.connection.status == .invalid {
                do {
                    try manager.connection.startVPNTunnel(options: options)
                    self.addVPNStatusObserver()
                    complete?(manager, nil)
                }catch {
                    complete?(nil, error)
                }
            } else {
                self.addVPNStatusObserver()
                complete?(manager, nil)
            }
        }
    }
}

public func stopVPN() {
    // Stop provider
    loadProviderManager {
        guard let manager = $0 else {
            return
        }
        manager.connection.stopVPNTunnel()
    }
}
  1. 监听并更新 vpn 状态
/// 添加vpn的状态的监听
func addVPNStatusObserver() {
    guard !observerDidAdd else {
        return
    }
    loadProviderManager {
        if let manager = $0 {
            self.observerDidAdd = true
            NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { [unowned self] (notification) -> Void in
                self.updateVPNStatus(manager)
            })
        }
    }
}

/// 更新vpn的连接状态
///
/// - Parameter manager: NEVPNManager
func updateVPNStatus(_ manager: NEVPNManager) {
    
    switch manager.connection.status {
    case .connected:
        self.vpnStatus = .on
    case .connecting, .reasserting:
        self.vpnStatus = .connecting
    case .disconnecting:
        self.vpnStatus = .disconnecting
    case .disconnected, .invalid:
        self.vpnStatus = .off
    }
}
  1. 数据传递

我们需要在主程序中传递类似账号、密码、端口、加密方式等参数给我们的VPN组件。

  • 主程序写入
let conf = ["port":1000,"method":"AES-256-CFB","password":"hello"]
let providerProtocol = manager.protocolConfiguration as! NETunnelProviderProtocol
providerProtocol.providerConfiguration = conf
manager.protocolConfiguration = orignConf
  • target读取
public var protocolConfiguration: NEVPNProtocol { get }

guard let conf = (protocolConfiguration as! NETunnelProviderProtocol).providerConfiguration else{
    NSLog("[ERROR] No ProtocolConfiguration Found")
    exit(EXIT_FAILURE)
}
let address = conf["address"] as! String
let port = conf["port"] as! Int

NEPacketTunnelProvider

真正的 vpn 核心代码。项目中 PacketTunnelProvider 是其子类,并且以下两个方法必须实现

@available(iOS 9.0, *)
open func startTunnel(options: [String : NSObject]? = nil, completionHandler: @escaping (Error?) -> Swift.Void)

@available(iOS 9.0, *)
open func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Swift.Void)

当 App 里的 NETunnelProviderManager 对象调用 startVPNWithOptions 时,控制流程就跳到 Extension 里的 startTunnel 方法。

startTunnel 有两个参数:options 是个字典,从 App 里传过来,具体有啥信息完全由开发者自己定义。completionHandler 是个闭包回调,由系统提供。我们可以将其保存到某个变量,待 vpn 启动完成时主动调用。

stopTunnel 也有两个参数:reason 表示 vpn 被关闭的理由。iOS 预先定义了一组 NEProviderStopReason 常数,但实际应用时,reason 基本不用。completionHandler 的用法同 startTunnel 第二个参数一样,不再重复。

调试 Network Extension

在已经完成 PacketTunnel 的代码情况下:

  1. 构建并运行应用。
  2. 停止运行。
  3. Xcode 菜单中 Debug -> attach to process by PID or name,填入 PacketTunnel,然后 Attach

  1. 在手机上运行(不要通过 Xcode)应用,点击连接的时候,进入了断点。