实现方式
NetWorkExtension + OpenVPNAdapter.
NetWorkExtension主要帮助我们完成VPN的配置与获取配置信息。OpenVPNAdapter帮我们建立连接。
NetWorkExtension
NetWorkExtension App拓展。
创建NetWorkExtension
- 创建NetWorkExtension target。
- 开启相关权限。
- 配置VPN到手机。
NetWorkExtension Target创建
61C716C6-2116-405C-843D-C4ADA12F1D52.png B9597FB0-4976-4704-95C8-88D631EAB7E7.png 11B9489C-91F6-4339-A8DC-74106AA03026.png权限配置
权限配置需要在宿主app与app extension都配置完成。
D4159E5A-B09C-48AA-AA56-949C43338CD0.png权限配置完毕后会在鉴权文件中显示。
8E5AA5B6-187E-4CE2-9540-177E333D2B83.png
如若涉及到宿主app与app extension之间的通讯,利用app groups。同样宿主app、app extension都需要打开并配置相同的app group
9D017D88-7D2F-475B-90F3-6B13A4E3B45D.png
配置VPN到手机
宿主app引入NetWorkExtension框架,通过NetWorkExtension框架下提供的API 将openVPN的配置信息配置到手机。
func confingVPN() {
//获取VPNManager
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
guard error == nil else {
// Handle an occured error
print("loadAllFromPreferences error \(String(describing: error))")
return
}
self.providerManager = managers?.first ?? NETunnelProviderManager()
//配置VPN
self.providerManager?.loadFromPreferences(completionHandler: { (error) in
guard error == nil else {
// Handle an occured error
print("loadFromPreferences error")
return
}
// Assuming the app bundle contains a configuration file named 'client.ovpn' lets get its
// Data representation
guard
let configurationFileURL = Bundle.main.url(forResource: "client2", withExtension: "ovpn"),
let configurationFileContent = try? Data(contentsOf: configurationFileURL)
else {
fatalError()
}
let tunnelProtocol = NETunnelProviderProtocol()
// If the ovpn file doesn't contain server address you can use this property
// to provide it. Or just set an empty string value because `serverAddress`
// property must be set to a non-nil string in either case.
tunnelProtocol.serverAddress = "223.100.8.226 11194"
// The most important field which MUST be the bundle ID of our custom network
// extension target.
tunnelProtocol.providerBundleIdentifier = "app extension的bundle identifier "
// Use `providerConfiguration` to save content of the ovpn file.
tunnelProtocol.providerConfiguration = ["ovpn": configurationFileContent]
// Provide user credentials if needed. It is highly recommended to use
// keychain to store a password.
// tunnelProtocol.username = "username"
// tunnelProtocol.passwordReference = Data() // A persistent keychain reference to an item containing the password
// Finish configuration by assigning tunnel protocol to `protocolConfiguration`
// property of `providerManager` and by setting description.
self.providerManager?.protocolConfiguration = tunnelProtocol
self.providerManager?.localizedDescription = "Fch OpenVPN Client"
self.providerManager?.isEnabled = true
// Save configuration in the Network Extension preferences
self.providerManager?.saveToPreferences(completionHandler: { (error) in
if let error = error {
// Handle an occured error
print("saveToPreferences error \(String(describing: error)) ")
}
})
// self.providerManager?.removeFromPreferences(completionHandler: { (error) in
//
// })
})
}
}
有几点很重要
- tunnelProtocol.providerBundleIdentifier 为你创建的appextension的bundle Identifier. 宿主app会通过这个将我们的配置信息传递到appextension。
- tunnelProtocol.providerConfiguration字典文件中key值会在app extension中作为获取配置文件的key使用,需要前后保持一致。
OpenVPNAdapter配置文件config.ovpn
配置过程会打开手机设置来完成,需要进行指纹验证。成功后会可手机设配置查看相关配置信息。
IMG_7901.PNG
IMG_7902.PNG IMG_7903.PNG
完成配置后使用NetWorkExtension下的API控制VPN的开启。
开启后建立连接的过程会在app extension之中利用OpenVPNAdapter完成。
@objc func startVPN() {
self.providerManager?.loadFromPreferences(completionHandler: { (error) in
guard error == nil else {
// Handle an occured error
print("loadFromPreferences error \(String(describing: error))")
return
}
do {
try self.providerManager?.connection.startVPNTunnel()
self .addVPNStatusObserver();
print("startVPNTunnel state \(String(describing: self.providerManager?.connection.status))")
} catch {
// Handle an occured error
print("startVPNTunnel error \(String(describing: error))")
}
})
}
OpenVPNAdapter
集成
使用carthage将OpenVPNAdapter集成到自己项目当中。
建立连接
按照OpenVPNAdapter提供的代码即可。
enum PacketTunnelProviderError: Error {
case fatalError(message: String)
}
@available(iOSApplicationExtension 9.0, *)
class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
// There are many ways to provide OpenVPN settings to the tunnel provider. For instance,
// you can use `options` argument of `startTunnel(options:completionHandler:)` method or get
// settings from `protocolConfiguration.providerConfiguration` property of `NEPacketTunnelProvider`
// class. Also you may provide just content of a ovpn file or use key:value pairs
// that may be provided exclusively or in addition to file content.
// In our case we need providerConfiguration dictionary to retrieve content
// of the OpenVPN configuration file. Other options related to the tunnel
// provider also can be stored there.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else {
fatalError()
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
// configuration.settings = [
// ]
//
configuration.keyDirection = 1;
// Apply OpenVPN configuration
let properties: OpenVPNProperties
do {
properties = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
// Provide credentials if needed
if !properties.autologin {
// If your VPN configuration requires user credentials you can provide them by
// `protocolConfiguration.username` and `protocolConfiguration.passwordReference`
// properties. It is recommended to use persistent keychain reference to a keychain
// item containing the password.
guard let username: String = protocolConfiguration.username else {
fatalError()
}
// Retrieve a password from the keychain
// guard let password: String = ... {
// fatalError()
// }
let credentials = OpenVPNCredentials()
credentials.username = username
// credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
// Checking reachability. In some cases after switching from cellular to
// WiFi the adapter still uses cellular data. Changing reachability forces
// reconnection so the adapter will use actual connection.
vpnReachability.startTracking { [weak self] status in
guard status != .notReachable else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
// Establish connection and wait for .connected event
startHandler = completionHandler
vpnAdapter.connect()
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
}
@available(iOSApplicationExtension 9.0, *)
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings, completionHandler: @escaping (OpenVPNAdapterPacketFlow?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow : nil)
}
}
// Process events returned by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
return
}
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
print("handleLogMessage \(logMessage)")
NSLog("handleLogMessage \(logMessage)")
}
// Printing description of logMessage:
// "Transport Error: Transport error on \'223.100.8.226: NETWORK_EOF_ERROR\n"
// Printing description of error:
// Error Domain=me.ss-abramchuk.openvpn-adapter.error-domain Code=26 "OpenVPN error occured" UserInfo={NSLocalizedFailureReason=General transport error, me.ss-abramchuk.openvpn-adapter.error-key.message=Transport error on '223.100.8.226: NETWORK_EOF_ERROR, me.ss-abramchuk.openvpn-adapter.error-key.fatal=false, NSLocalizedDescription=OpenVPN error occured}
}
整个过程大致为:通过我们在宿主app中设置的key获取到配置信息,利用OpenVPNAdapter读取配置信息,并建立连接。
建立成功后手机状态栏会显示出VPN的标志。
56B86A1C-870C-4E78-93CF-27E72152D277.png
配置文件
配置文件在OpenVPNAdapter中支持两种方式
健值对方式
remote 223.100.8.226 11194
标签方式
<ca>
</ca>
config文件配置基本两种方式
- 用户名密码验证方式。
- 证书验证方式。
根据实际情况大致配置如下
client
#路由模式
dev tun
#改为tcp
proto tcp
#OpenVPN服务器的外网IP和端口
remote xxx.xxx.x.xxx xxxxx
resolv-retry infinite
nobind
persist-key
persist-tun
#ca ca.crt
#cert test1.crt
#key test1.key
ns-cert-type server
#tls-auth ta.key 1
comp-lzo
verb 3
#密码认证相关
#auth-user-pass
通常情况下这是一种比较标准常见的配置文件。但是在OpenVPNAdapter可能会存在问题。OpenVPNAdapter并不能完善的支持所有标签,导致我们在建立连接过程中出现很多问题。详细参考 常见错误。
参考资料
openVPN的客户端的client.ovpn配置.
常见错误
(Error) error = <variable not available>变量不支持。
.ovpn中tls-auth变量导致的OpenVPNAdapter并不支持健值对这种方法
tls-auth ta.key 1
改为
<tls-auth>
-----BEGIN OpenVPN Static key V1-----
···从你的ta.key中复制过来
-----END OpenVPN Static key V1-----
</tls-auth>
Error Domain=me.ss-abramchuk.openvpn-adapter.error-domain Code=67 "Failed to establish connection with OpenVPN server"建立连接失败
原因很多种,例如:
ca 证书文件格式不正确。因为OpenVPNAdapter认为我们我们配置的ca ca.crt中ca.crt为我们的证书文件内容。但实际上它是一个证书文件的路径。所以我们也适用标签方式配置
<ca>
....
</ca>
同理
#ca ca.crt
#cert test1.crt
#key test1.key
#tls-auth ta.key 1
都使用标签方式进行配置。
"UNUSED OPTIONS\n4 [resolv-retry] [infinite] \n5 [nobind] \n6 [persist-key] \n7 [persist-tun] \n10 [verb] [3] \n11 [key-deriction] [1] \n\n"
317545E1-8DE2-4298-8FA1-867AC78544B4.png有几个标签在没有用,应该是能够识别这些标签但是无法使用。
其中key-deriction比较重要,所以我们在代码中配置
configuration.keyDirection = 1;
"TCP recv EOF\n"TCP EOF错误
Transport error on '223.100.8.226: NETWORK_EOF_ERROR
由于key-deriction无法使用导致。在代码中配置后解决。
网友评论
如果要实现过滤请求的功能倒是可以在服务端来做(具体工作量,难易程度不清楚)。