Reputation: 1913
I have an app which is setting up a VPN connection via Wireguard framework. App is using AppDelegate Lifecycle even if in SwiftUI. I'm using a real device.
I need to close my VPN connection started via Wireguard when app is manually closed by the user (not when in background, in that case I need connection alive), if user swipes the apps in order to close it, I'd expect the app to call applicationWillTerminate and call turnOffTunnel() before the app terminates. How to achieve this?
I do not want the vpn connection to be alive if app is closed. I can see that the VPN is still running if I check the VPN setting of iOS. On andoird a colleague of mine can do it, is it possible in iOS?
I tried:
with this one, applicationWillTerminate when user closes the app via swipe, it is called, but not always. Still, my method is never called.
my app currently is NOT supporting background activity, but maybe in the future it will, I read this could somehow interfere, but info I found are a bit old
import SwiftUI
import UIKit
import NetworkExtension
@main
struct testWireGuardInstructions01SwiftUIApp: App {
//used to replicate old UIKit appDelegate 1/2
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyManager.shared)
}
}
}
//MARK: - section used to replicate UIKit app delegate behaviour
//used to replicate old UIKit appdelegate 2/2
class AppDelegate: UIResponder, UIApplicationDelegate {
let myManager = MyManager.shared
var bgTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0);
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func applicationWillTerminate(_ application: UIApplication) {
Logger.info("applicationWillTerminate")
myManager.turnOffTunnel()
}
}
//MARK: - ContentView
import SwiftUI
import SafariServices
struct ContentView: View {
@EnvironmentObject var myManager: MyManager
@State private var isShowingWebView = false
var body: some View {
VStack(spacing: 20) {
Text(myManager.isConnected ? "Connected" : "Disconnected")
.font(.title)
.foregroundColor(myManager.isConnected ? .green : .red)
//Simulate obtaining result of a QR code
Button("Obtain Data from QR") {
myManager.getConfigurationInfoFromQR()
}
Button("Start") {
print("Start")
myManager.turnOnTunnel { isSuccess in
if isSuccess {
NSLog("Starting tunnel succeeded")
}
}
}
Button("Stop") {
print("Stop")
myManager.turnOffTunnel()
}
Button("Verify") {
print("Verify")
myManager.logConnecitonStatusEX()
}
Button("Logout") {
myManager.removeTunnel { isSuccess in
NSLog("removeTunnel exitus: \(isSuccess)")
}
}
}
}
}
struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {}
}
//MARK: - MY MANAGER Class
import Foundation
import NetworkExtension
import Security
import LocalAuthentication
class MyManager: ObservableObject {
///Remeber to set providerBundleIdentifier properly in this class
static let shared = MyManager() //here since I am using it in AppDelegate UIKit code
var tunnelManager: NETunnelProviderManager?
var wgQuickConfig: String?
@Published var isConnected: Bool = false
private init() {}
// Assumptions:
// - We just need one tunnel in iOS Settings / macOS Preferences for the app.
// We can reuse that with different configs if required.
// - The tunnel shall not be started from iOS Settings / macOS Preferences.
// It has to be started using the container app only.
// - The tunnel need not be configured to start automatically under
// certain conditions using on-demand
func turnOnTunnel(completionHandler: @escaping (Bool) -> Void) {
// Check if the tunnel is already connected, avoid multiple connections
if isConnected {
NSLog("Tunnel is already connected")
completionHandler(true)
return
}
guard let data = readFromKeychain(service: "com.example.myapp", account: "wgQuickConfig"),
let wgQuickConfig = String(data: data, encoding: .utf8) else {
fatalError("No wgQuickConfig found in Keychain")
}
self.wgQuickConfig = wgQuickConfig
// We use loadAllFromPreferences to see if this app has already added a tunnel
// to iOS Settings or (macOS Preferences)
NETunnelProviderManager.loadAllFromPreferences { tunnelManagersInSettings, error in
if let error = error {
NSLog("Error (loadAllFromPreferences): \(error)")
completionHandler(false)
return
}
// If the app has already added a tunnel to Settings, we are going to modify that.
// If not, we create a new instance and save that to Settings.
// We will always have either 0 or 1 tunnel only in Settings for this app.
let preExistingTunnelManager = tunnelManagersInSettings?.first
self.tunnelManager = preExistingTunnelManager ?? NETunnelProviderManager()
// Configure the custom VPN protocol
let protocolConfiguration = NETunnelProviderProtocol()
// Set the tunnel extension's bundle id
protocolConfiguration.providerBundleIdentifier = "yourtunnel.tunnelsxtension"
// Set the server address as a non-nil string.
// It would be good to provide the server's domain name or IP address.
protocolConfiguration.serverAddress = "yourIP"
//added by me
//---------------------------------------------------------------------------
guard let wgQuickConfig = self.wgQuickConfig else {
fatalError("no wgQuickConfig")
}
protocolConfiguration.providerConfiguration = [
"wgQuickConfig": wgQuickConfig
]
guard let tunnelManager = self.tunnelManager else {
fatalError("no tunnel manager")
}
//---------------------------------------------------------------------------
tunnelManager.protocolConfiguration = protocolConfiguration
tunnelManager.isEnabled = true
// Save the tunnel to preferences.
// This would modify the existing tunnel, or create a new one.
tunnelManager.saveToPreferences { error in
if let error = error {
NSLog("Error (saveToPreferences): \(error)")
completionHandler(false)
return
}
// Load it back so we have a valid usable instance.
tunnelManager.loadFromPreferences { error in
if let error = error {
NSLog("Error (loadFromPreferences): \(error)")
completionHandler(false)
return
}
// At this point, the tunnel is configured.
// Attempt to start the tunnel
do {
NSLog("Starting the tunnel")
guard let session = tunnelManager.connection as? NETunnelProviderSession else {
fatalError("tunnelManager.connection is invalid")
}
try session.startTunnel()
DispatchQueue.main.async {
self.isConnected = true
}
} catch {
NSLog("Error (startTunnel): \(error)")
completionHandler(false)
}
completionHandler(true)
}
}
}
}
///Simply cuts off the vpn connection
func turnOffTunnel() {
NETunnelProviderManager.loadAllFromPreferences { tunnelManagersInSettings, error in
if let error = error {
NSLog("Error (loadAllFromPreferences): \(error)")
return
}
if let tunnelManager = tunnelManagersInSettings?.first {
guard let session = tunnelManager.connection as? NETunnelProviderSession else {
fatalError("tunnelManager.connection is invalid")
}
switch session.status {
case .connected, .connecting, .reasserting:
NSLog("Stopping the tunnel")
session.stopTunnel()
DispatchQueue.main.async {
self.isConnected = false
}
self.logConnecitonStatusEX()
default:
break
}
}
}
}
//remove from device preferences settigns
//remove from keychain
func removeTunnel(completionHandler: @escaping (Bool) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { tunnelManagersInSettings, error in
if let error = error {
NSLog("Error (loadAllFromPreferences): \(error)")
completionHandler(false)
return
}
guard let tunnelManager = tunnelManagersInSettings?.first else {
NSLog("No tunnelManager found to remove")
completionHandler(false)
return
}
tunnelManager.removeFromPreferences { error in
if let error = error {
NSLog("Error (removeFromPreferences): \(error)")
completionHandler(false)
return
}
_ = self.deleteFromKeychain(service: "com.example.myapp", account: "wgQuickConfig")
NSLog("Tunnel configuration removed successfully")
DispatchQueue.main.async {
self.isConnected = false
}
self.logConnecitonStatusEX()
completionHandler(true)
}
}
}
}
Upvotes: 0
Views: 204