PacketTunnelProvider refactoring

- removing unnecessary dispatchQueue
- removing lazy initiation for wg and ovpn
- fix memory leaks
This commit is contained in:
Boris Verbitskii 2024-05-17 18:17:08 +07:00
parent 2254bfc128
commit 9be13ea465
4 changed files with 481 additions and 528 deletions

View file

@ -2,15 +2,15 @@ import Foundation
import os.log import os.log
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) { public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)") neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
} }
public func wg_log(_ type: OSLogType, title: String = "", message: String) { public func wg_log(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "WG: \(title)", message: message) neLog(type, title: "WG: \(title)", message: message)
} }
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "OVPN: \(title)", message: message) neLog(type, title: "OVPN: \(title)", message: message)
} }
public func neLog(_ type: OSLogType, title: String = "", message: String) { public func neLog(_ type: OSLogType, title: String = "", message: String) {

View file

@ -3,223 +3,232 @@ import NetworkExtension
import OpenVPNAdapter import OpenVPNAdapter
struct OpenVPNConfig: Decodable { struct OpenVPNConfig: Decodable {
let config: String let config: String
let splitTunnelType: Int let splitTunnelType: Int
let splitTunnelSites: [String] let splitTunnelSites: [String]
var str: String { var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)" "splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
} }
} }
extension PacketTunnelProvider { extension PacketTunnelProvider {
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration, let providerConfiguration = protocolConfiguration.providerConfiguration,
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else { let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
ovpnLog(.error, message: "Can't start") ovpnLog(.error, message: "Can't start")
return return
}
do {
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
return
}
} }
do { private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
// ovpnLog(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self))") withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData) let str = String(decoding: ovpnConfiguration, as: UTF8.self)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError { let configuration = OpenVPNConfiguration()
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)") configuration.fileContent = ovpnConfiguration
} if str.contains("cloak") {
configuration.setPTCloak()
}
return let evaluation: OpenVPNConfigurationEvaluation?
} do {
} ovpnAdapter = OpenVPNAdapter()
ovpnAdapter?.delegate = self
evaluation = try ovpnAdapter?.apply(configuration: configuration)
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, } catch {
withShadowSocks viaSS: Bool = false, completionHandler(error)
completionHandler: @escaping (Error?) -> Void) { return
ovpnLog(.info, message: "Setup and launch") }
let str = String(decoding: ovpnConfiguration, as: UTF8.self) if evaluation?.autologin == false {
ovpnLog(.info, message: "Implement login with user credentials")
}
let configuration = OpenVPNConfiguration() vpnReachability.startTracking { [weak self] status in
configuration.fileContent = ovpnConfiguration guard status == .reachableViaWiFi else { return }
if str.contains("cloak") { self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
configuration.setPTCloak() }
startHandler = completionHandler
ovpnAdapter?.connect(using: packetFlow)
} }
let evaluation: OpenVPNConfigurationEvaluation func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
do { guard let completionHandler = completionHandler else { return }
evaluation = try ovpnAdapter.apply(configuration: configuration) let bytesin = ovpnAdapter?.transportStatistics.bytesIn
let bytesout = ovpnAdapter?.transportStatistics.bytesOut
} catch { guard let bytesin, let bytesout else {
completionHandler(error) completionHandler(nil)
return return
}
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
} }
if !evaluation.autologin { func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
ovpnLog(.info, message: "Implement login with user credentials") ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter?.disconnect()
} }
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
ovpnAdapter.connect(using: packetFlow)
// let ifaces = Interface.allInterfaces()
// .filter { $0.family == .ipv4 }
// .map { iface in iface.name }
// ovpn_log(.error, message: "Available TUN Interfaces: \(ifaces)")
}
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter.transportStatistics.bytesIn
let bytesout = ovpnAdapter.transportStatistics.bytesOut
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter.disconnect()
}
} }
extension PacketTunnelProvider: OpenVPNAdapterDelegate { extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel. // OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow` // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil. // protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback. // send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter( func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter, _ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void completionHandler: @escaping (Error?) -> Void
) { ) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains // send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""] networkSettings?.dnsSettings?.matchDomains = [""]
if splitTunnelType == 1 { if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]() var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites { guard let splitTunnelSites else {
if let allowedIP = IPAddressRange(from: allowedIPString) { completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
ipv4IncludedRoutes.append(NEIPv4Route( return
destinationAddress: "\(allowedIP.address)", }
subnetMask: "\(allowedIP.subnetMask())"))
}
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes for allowedIPString in splitTunnelSites {
} else { if let allowedIP = IPAddressRange(from: allowedIPString) {
if splitTunnelType == 2 { ipv4IncludedRoutes.append(NEIPv4Route(
var ipv4ExcludedRoutes = [NEIPv4Route]() destinationAddress: "\(allowedIP.address)",
var ipv4IncludedRoutes = [NEIPv4Route]() subnetMask: "\(allowedIP.subnetMask())"))
var ipv6IncludedRoutes = [NEIPv6Route]() }
}
for excludeIPString in splitTunnelSites { networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
if let excludeIP = IPAddressRange(from: excludeIPString) { } else {
ipv4ExcludedRoutes.append(NEIPv4Route( if splitTunnelType == 2 {
destinationAddress: "\(excludeIP.address)", var ipv4ExcludedRoutes = [NEIPv4Route]()
subnetMask: "\(excludeIP.subnetMask())")) var ipv4IncludedRoutes = [NEIPv4Route]()
} var ipv6IncludedRoutes = [NEIPv6Route]()
guard let splitTunnelSites else {
completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0))
return
}
for excludeIPString in splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
}
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
} }
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") { // Set the network settings for the current tunneling session.
ipv4IncludedRoutes.append(NEIPv4Route( setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
} }
// Set the network settings for the current tunneling session. // Process events returned by the OpenVPN library
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) func openVPNAdapter(
} _ openVPNAdapter: OpenVPNAdapter,
handleEvent event: OpenVPNAdapterEvent,
message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
// Process events returned by the OpenVPN library guard let startHandler = startHandler else { return }
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter, startHandler(nil)
handleEvent event: OpenVPNAdapterEvent, self.startHandler = nil
message: String?) { case .disconnected:
switch event { guard let stopHandler = stopHandler else { return }
case .connected:
if reasserting { if vpnReachability.isTracking {
reasserting = false vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
} }
guard let startHandler = startHandler else { return } // Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
startHandler(nil) // Handle only fatal errors
self.startHandler = nil guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
case .disconnected: fatal == true else { return }
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking { if vpnReachability.isTracking {
vpnReachability.stopTracking() vpnReachability.stopTracking()
} }
stopHandler() if let startHandler {
self.stopHandler = nil startHandler(error)
case .reconnecting: self.startHandler = nil
reasserting = true } else {
default: cancelTunnelWithError(error)
break }
}
} }
// Handle errors thrown by the OpenVPN library // Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle only fatal errors // Handle log messages
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, ovpnLog(.info, message: logMessage)
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
} }
if let 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
ovpnLog(.info, message: logMessage)
}
} }
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {} extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

View file

@ -2,220 +2,186 @@ import Foundation
import NetworkExtension import NetworkExtension
extension PacketTunnelProvider { extension PacketTunnelProvider {
func startWireguard(activationAttemptId: String?, func startWireguard(activationAttemptId: String?,
errorNotifier: ErrorNotifier, errorNotifier: ErrorNotifier,
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration, let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else { let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start, config missing") wg_log(.error, message: "Can't start, config missing")
completionHandler(nil) completionHandler(nil)
return return
} }
do { do {
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData) let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
let wgConfigStr = wgConfig.str let wgConfigStr = wgConfig.str
wg_log(.info, title: "config: ", message: wgConfig.redux) wg_log(.info, title: "config: ", message: wgConfig.redux)
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr) let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
if tunnelConfiguration.peers.first!.allowedIPs if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation }) .map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" { .joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if wgConfig.splitTunnelType == 1 { if wgConfig.splitTunnelType == 1 {
for index in tunnelConfiguration.peers.indices { for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll() tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]() var allowedIPs = [IPAddressRange]()
for allowedIPString in wgConfig.splitTunnelSites { for allowedIPString in wgConfig.splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) { if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP) allowedIPs.append(allowedIP)
} }
} }
tunnelConfiguration.peers[index].allowedIPs = allowedIPs tunnelConfiguration.peers[index].allowedIPs = allowedIPs
} }
} else if wgConfig.splitTunnelType == 2 { } else if wgConfig.splitTunnelType == 2 {
for index in tunnelConfiguration.peers.indices { for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]() var excludeIPs = [IPAddressRange]()
for excludeIPString in wgConfig.splitTunnelSites { for excludeIPString in wgConfig.splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) { if let excludeIP = IPAddressRange(from: excludeIPString) {
excludeIPs.append(excludeIP) excludeIPs.append(excludeIP)
} }
} }
tunnelConfiguration.peers[index].excludeIPs = excludeIPs tunnelConfiguration.peers[index].excludeIPs = excludeIPs
} }
} }
} }
wg_log(.info, message: "Starting tunnel from the " + wg_log(.info, message: "Starting tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app")) (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel // Start the tunnel
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in wgAdapter = WireGuardAdapter(with: self) { logLevel, message in
guard let adapterError else { wg_log(logLevel.osLogLevel, message: message)
let interfaceName = self.wgAdapter.interfaceName ?? "unknown" }
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil) wgAdapter?.start(tunnelConfiguration: tunnelConfiguration) { [weak self] adapterError in
return guard let adapterError else {
} let interfaceName = self?.wgAdapter?.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
switch adapterError { completionHandler(nil)
case .cannotLocateTunnelFileDescriptor: return
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor") }
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor) switch adapterError {
case .dnsResolution(let dnsErrors): case .cannotLocateTunnelFileDescriptor:
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address } wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
.joined(separator: ", ") errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
wg_log(.error, message: completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)") case .dnsResolution(let dnsErrors):
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure) let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
completionHandler(PacketTunnelProviderError.dnsResolutionFailure) .joined(separator: ", ")
case .setNetworkSettings(let error): wg_log(.error, message:
wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)") errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings) completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings) case .setNetworkSettings(let error):
case .startWireGuardBackend(let errorCode): wg_log(.error, message:
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)") "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend) errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotStartBackend) completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .invalidState: case .startWireGuardBackend(let errorCode):
fatalError() wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
} errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
} completionHandler(PacketTunnelProviderError.couldNotStartBackend)
} catch { case .invalidState:
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)") fatalError()
completionHandler(nil) }
return }
} } catch {
} wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
let components = settings!.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
let pair = component.components(separatedBy: "=")
if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
let response: [String: Any] = [
"rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil) completionHandler(nil)
return return
}
self.wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} }
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
} }
}
// private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) { func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
// dispatchPrecondition(condition: .onQueue(dispatchQueue)) guard let completionHandler = completionHandler else { return }
// wgAdapter?.getRuntimeConfiguration { settings in
// let emptyTunnelConfiguration = TunnelConfiguration( let components = settings!.components(separatedBy: "\n")
// name: nil,
// interface: InterfaceConfiguration(privateKey: PrivateKey()),
// peers: []
// )
//
// wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
// self.dispatchQueue.async {
// if let error {
// wg_log(.error, message: "Failed to start an empty tunnel")
// completionHandler(error)
// } else {
// wg_log(.info, message: "Started an empty tunnel")
// self.tunnelAdapterDidStart()
// }
// }
// }
//
// let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
//
// self.setTunnelNetworkSettings(settings) { error in
// completionHandler(error)
// }
// }
// private func tunnelAdapterDidStart() { var settingsDictionary: [String: String] = [:]
// dispatchPrecondition(condition: .onQueue(dispatchQueue)) for component in components {
// // ... let pair = component.components(separatedBy: "=")
// } if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { let response: [String: Any] = [
wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)") "rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
wgAdapter.stop { error in completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
ErrorNotifier.removeLastErrorFile() }
}
if let error { private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)") guard let completionHandler = completionHandler else { return }
} if messageData.count == 1 && messageData[0] == 0 {
completionHandler() wgAdapter?.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter?.update(tunnelConfiguration: tunnelConfiguration) { [weak self] error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil)
return
}
self?.wgAdapter?.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)")
wgAdapter?.stop { error in
ErrorNotifier.removeLastErrorFile()
if let error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
#if os(macOS) #if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107). // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to // Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users. // sufficient quantities of users.
exit(0) exit(0)
#endif #endif
}
} }
}
} }

View file

@ -5,7 +5,7 @@ import Darwin
import OpenVPNAdapter import OpenVPNAdapter
enum TunnelProtoType: String { enum TunnelProtoType: String {
case wireguard, openvpn, shadowsocks, none case wireguard, openvpn
} }
struct Constants { struct Constants {
@ -34,160 +34,138 @@ struct Constants {
} }
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var wgAdapter = { var wgAdapter: WireGuardAdapter?
WireGuardAdapter(with: self) { logLevel, message in var ovpnAdapter: OpenVPNAdapter?
wg_log(logLevel.osLogLevel, message: message)
}
}()
lazy var ovpnAdapter: OpenVPNAdapter = { var splitTunnelType: Int?
let adapter = OpenVPNAdapter() var splitTunnelSites: [String]?
adapter.delegate = self
return adapter
}()
/// Internal queue. let vpnReachability = OpenVPNReachability()
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
var splitTunnelType: Int! var startHandler: ((Error?) -> Void)?
var splitTunnelSites: [String]! var stopHandler: (() -> Void)?
var protoType: TunnelProtoType?
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var protoType: TunnelProtoType = .none
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let message = String(data: messageData, encoding: .utf8) else { guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler { if let completionHandler {
completionHandler(nil) completionHandler(nil)
}
return
} }
return
}
neLog(.info, title: "App said: ", message: message) neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else { guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
neLog(.error, message: "Failed to serialize message from app") neLog(.error, message: "Failed to serialize message from app")
return return
} }
guard let completionHandler else { guard let completionHandler else {
neLog(.error, message: "Missing message completion handler") neLog(.error, message: "Missing message completion handler")
return return
} }
guard let action = message[Constants.kMessageKeyAction] as? String else { guard let action = message[Constants.kMessageKeyAction] as? String else {
neLog(.error, message: "Missing action key in app message") neLog(.error, message: "Missing action key in app message")
completionHandler(nil) completionHandler(nil)
return return
} }
if action == Constants.kActionStatus { if action == Constants.kActionStatus {
handleStatusAppMessage(messageData, completionHandler: completionHandler) handleStatusAppMessage(messageData,
} completionHandler: completionHandler)
}
} }
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { override func startTunnel(options: [String : NSObject]? = nil,
dispatchQueue.async { completionHandler: @escaping ((any Error)?) -> Void) {
let activationAttemptId = options?[Constants.kActivationAttemptId] as? String let activationAttemptId = options?[Constants.kActivationAttemptId] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId) let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
neLog(.info, message: "Start tunnel") neLog(.info, message: "Start tunnel")
if let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol { if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
let providerConfiguration = protocolConfiguration.providerConfiguration let providerConfiguration = protocolConfiguration.providerConfiguration
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil { if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
self.protoType = .openvpn protoType = .openvpn
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
self.protoType = .wireguard protoType = .wireguard
}
} }
} else {
self.protoType = .none
}
switch self.protoType { guard let protoType else {
case .wireguard: let error = NSError(domain: "Protocol is not selected", code: 0)
self.startWireguard(activationAttemptId: activationAttemptId, completionHandler(error)
errorNotifier: errorNotifier, return
completionHandler: completionHandler) }
case .openvpn:
self.startOpenVPN(completionHandler: completionHandler) switch protoType {
case .shadowsocks: case .wireguard:
break startWireguard(activationAttemptId: activationAttemptId,
// startShadowSocks(completionHandler: completionHandler) errorNotifier: errorNotifier,
case .none: completionHandler: completionHandler)
break case .openvpn:
} startOpenVPN(completionHandler: completionHandler)
}
} }
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
switch self.protoType { guard let protoType else {
case .wireguard: completionHandler()
self.stopWireguard(with: reason, completionHandler: completionHandler) return
case .openvpn: }
self.stopOpenVPN(with: reason, completionHandler: completionHandler)
case .shadowsocks: switch protoType {
break case .wireguard:
// stopShadowSocks(with: reason, completionHandler: completionHandler) stopWireguard(with: reason,
case .none: completionHandler: completionHandler)
break case .openvpn:
} stopOpenVPN(with: reason,
completionHandler: completionHandler)
}
} }
}
func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
switch protoType { guard let protoType else {
case .wireguard: completionHandler?(nil)
handleWireguardStatusMessage(messageData, completionHandler: completionHandler) return
case .openvpn: }
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
case .shadowsocks: switch protoType {
break case .wireguard:
// handleShadowSocksAppMessage(messageData, completionHandler: completionHandler) handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .none: case .openvpn:
break handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
}
} }
}
// MARK: Network observing methods // MARK: Network observing methods
override func observeValue(forKeyPath keyPath: String?,
private func startListeningForNetworkChanges() { of object: Any?,
stopListeningForNetworkChanges() change: [NSKeyValueChangeKey: Any]?,
addObserver(self, forKeyPath: Constants.kDefaultPathKey, options: .old, context: nil) context: UnsafeMutableRawPointer?) {
} guard Constants.kDefaultPathKey != keyPath else { return }
// Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi,
private func stopListeningForNetworkChanges() { // even though the underlying network has not changed (i.e. `isEqualToPath` returns false),
removeObserver(self, forKeyPath: Constants.kDefaultPathKey) // leading to "wakeup crashes" due to excessive network activity. Guard against false positives by
} // comparing the paths' string description, which includes properties not exposed by the class
guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
override func observeValue(forKeyPath keyPath: String?, let defPath = defaultPath,
of object: Any?, lastPath != defPath || lastPath.description != defPath.description else {
change: [NSKeyValueChangeKey: Any]?, return
context: UnsafeMutableRawPointer?) { }
guard Constants.kDefaultPathKey != keyPath else { return } DispatchQueue.main.async { [weak self] in
// Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi, guard let self, self.defaultPath != nil else { return }
// even though the underlying network has not changed (i.e. `isEqualToPath` returns false), self.handle(networkChange: self.defaultPath!) { _ in }
// leading to "wakeup crashes" due to excessive network activity. Guard against false positives by }
// comparing the paths' string description, which includes properties not exposed by the class
guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
let defPath = defaultPath,
lastPath != defPath || lastPath.description != defPath.description else {
return
} }
DispatchQueue.main.async { [weak self] in
guard let self, self.defaultPath != nil else { return }
self.handle(networkChange: self.defaultPath!) { _ in }
}
}
private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) { private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
wg_log(.info, message: "Tunnel restarted.") wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion) startTunnel(options: nil, completionHandler: completion)
} }
} }
extension WireGuardLogLevel { extension WireGuardLogLevel {