环境
- Xcode 11.6
- iOS 13
- MacOS 10.15
导航
完整代码在此,熟悉的小伙伴可以直接试试。
主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可以实时查看日志,如图:
连接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
}
}
}
- 创建createUDPSession,并监听状态
- OK后调用setupTunnelNetworkSettings()和localPacketsToServer(),配置tunnel,拦截本地流量
拦截本地IP包
要能拦截流量,需要2步:
首先,通过setTunnelNetworkSettings指定隧道的网络设置:开启一个虚拟网卡Tun接口,需要配置虚拟IP,DNS,代理设置,MTU和IP路由等信息。
然后使用packetFlow.readPackets即可从虚拟网卡文件中读取IP数据包再发送给代理Server。
tun虚拟网卡通过映射一个文件的方式从协议栈获取IP数据,或将数据写入协议栈,具体工作原理可以参考这个博客
最简单的配置如下:
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
监听udpSession数据回调。
使用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是连不上的,因为我们的代理服务器还没有开启~~