环境
- Xcode 11.6
- iOS 13
- MacOS 10.15
导航
完整代码在此,熟悉的小伙伴可以直接试试。
第一步,先来搞定主App相关界面和功能。
首先,创建工程,大概是这样,一共3个target:
YYVPN
├── YYVPNLib Client和Tunnel共享的代码
├── Client 主App,界面相关
├── Tunnel Network Extension,获取流量的地方
Client和Tunnel需要开启App Groups和Network Extension的Packt Tunnel,如下图:
需要付费开发者账号
因为最后要连上自己的Server,所以得配置IP和端口,加上开启/关闭流量拦截等功能,界面如下:
比较简单,用Swift UI写着也很方便,代码如下:
ConfigView:
import SwiftUI
struct ConfigView: View {
@ObservedObject var viewModel =
ConfigViewModel(config: .init(hostname: "172.20.49.36", port: "8899"))
var body: some View {
NavigationView {
Form {
Section(header: Text("Settings")) {
HStack(alignment: .center) {
Text("IP").font(.callout)
TextField("IP", text: $viewModel.config.hostname)
.multilineTextAlignment(.trailing)
.foregroundColor(.gray)
}
HStack(alignment: .center) {
Text("Port").font(.callout)
TextField("Port", text: $viewModel.config.port)
.multilineTextAlignment(.trailing)
.foregroundColor(.gray)
}
}
Section(header: Text("Status")) {
Text("Status: ") + Text(viewModel.status.rawValue)
if viewModel.status == .off || viewModel.status == .invalid {
Button(action: {
self.hideKeyboard()
self.viewModel.didTapStart()
}) {
Text("Start")
}
} else {
Button(action: {
self.hideKeyboard()
self.viewModel.didTapStop()
}) {
Text("Stop")
}
}
}
Section {
YYAlertButton(text: "Remove",
title: "确定删除?",
message: nil,
confirm: {
self.viewModel.didTapRemove()
},
cancel: nil)
}
}
.navigationBarTitle("VPN Status")
}
}
}
ConfigViewModel:
使用Swift UI离不开Combine,之前写过一篇Combine最简流程源码解析,可以加深对Combine的理解。
import Foundation
import Combine
import YYVPNLib
final class ConfigViewModel: ObservableObject {
@Published var config: YYVPNManager.Config
@Published var status = YYVPNManager.Status.off
init(config: YYVPNManager.Config) {
self.config = config
YYVPNManager.shared.statusDidChangeHandler = { [weak self] status in
self?.status = status
}
}
func didTapStart() {
YYVPNManager.shared.start(with: config) { error in
}
}
func didTapStop() {
YYVPNManager.shared.stop()
}
func didTapRemove() {
YYVPNManager.shared.removeFromPreferences { error in
}
}
}
YYVPNLib
一个单独的静态库,封装了管理VPN相关的逻辑,方便多个Target复用,核心方法都封装在YYVPNManager里。
详细的大家可以下载完整源码来看,这里简单说下流程:
要拦截流量,需要主App启动Network Extension进程,这通过调用NETunnelProviderManager对象tunnel的tunnel.connection.startVPNTunnel()方法。
-
而NETunnelProviderManager对象通过调用NETunnelProviderManager.loadAllFromPreferences获取,第一次肯定是nil,需要自己手动创建并保存,大概如下,有2个地方需要注意:
let manager = NETunnelProviderManager() let proto = NETunnelProviderProtocol() /// providerBundleIdentifier必须是Network Extension的Target的Bundle ID proto.providerBundleIdentifier = config.bundleIDTunnel proto.serverAddress = "YYVPN" /// 如果要设置用户名和密码,passwordReference必须取keychain里面的值 //proto.passwordReference = Data() manager.protocolConfiguration = proto manager.localizedDescription = "YYVPN" manager.isEnabled = true manager.saveToPreferences{ error in }
NETunnelProviderManager.loadAllFromPreferences会读取本应用创建的所有VPN配置,保存在设置中,注意不要重复添加了。