swiftvpnopenvpntunnelnetworkextension

Allow only certain apps to use my VPN - Swift


I am making an app that activates a VPN connection based on OpenVPN, retrieves a certificate from the database, and opens a tunnel using NEPacketTunnelProvider and NetworkExtension.

I used the following repository, and now my VPN is working fine.

But the problem is that I want to allow only one app to use this VPN when enabled (WhatsApp precisely), and I want to restrict all other apps of using it.

On Android it's possible by giving the bundle identifier of the allowed apps to the PackageManager.

Can you please help me?

This is my PacketTunnelProvider class:

import NetworkExtension
import OpenVPNAdapter

extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

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.
    print("started!")
    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 = [
//            // Additional parameters as key:value pairs may be provided here
//        ]

    // Uncomment this line if you want to keep TUN interface active during pauses or reconnections
    // configuration.tunPersist = true

    // Apply OpenVPN configuration
    let evaluation: OpenVPNConfigurationEvaluation
    do {
        evaluation = try vpnAdapter.apply(configuration: configuration)
    } catch {
        completionHandler(error)
        return
    }

    // Provide credentials if needed
    if !evaluation.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 == .reachableViaWiFi else { return }
        self?.vpnAdapter.reconnect(afterTimeInterval: 5)
    }

    // Establish connection and wait for .connected event
    startHandler = completionHandler
    vpnAdapter.connect(using: packetFlow)
}

override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    stopHandler = completionHandler

    if vpnReachability.isTracking {
        vpnReachability.stopTracking()
    }

    vpnAdapter.disconnect()
}

}

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 (Error?) -> Void) {
    // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
    // send empty string to NEDNSSettings.matchDomains
    networkSettings?.dnsSettings?.matchDomains = [""]

    // Set the network settings for the current tunneling session.
    setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}

// 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(logMessage)
}

}

This is the function used in my VPN View Model to start a tunnel:

    func configureVPN(serverAddress: String, username: String, password: String) {
    var configData:Data = Data.init()
    self.getCertificate{certificate in
        configData = certificate!
        
        guard
            //If we want to read from a file
 //                let configData = self.readFile(name: "vtest2"),
            let providerManager = self.providerManager
        else {
            return
        }
        
        self.providerManager?.loadFromPreferences { error in
            if error == nil {
                let tunnelProtocol = NETunnelProviderProtocol()
                tunnelProtocol.username = username
                tunnelProtocol.serverAddress = serverAddress
                tunnelProtocol.providerBundleIdentifier = self.providerId // bundle id of the network extension target
                tunnelProtocol.providerConfiguration = ["ovpn": configData]
                tunnelProtocol.disconnectOnSleep = false
                providerManager.protocolConfiguration = tunnelProtocol
                providerManager.localizedDescription = "Slyfone Guard" // the title of the VPN profile which will appear on Settings
                providerManager.isEnabled = true
                providerManager.saveToPreferences(completionHandler: { (error) in
                    if error == nil  {
                        providerManager.loadFromPreferences(completionHandler: { (error) in
                            do {
                                try providerManager.connection.startVPNTunnel(options: nil) // starts the VPN tunnel.
                            } catch let error {
                                print(error.localizedDescription)
                            }
                        })
                    }
                })
            }
        }
    }
}

Solution

  • As an engineer from Apple said:

    The way to do it is to use Per-App VPN. See the Per-App VPN On Demand section in the NETunnelProviderManager documentation.

    With NEPacketTunnelProvider on macOS (as of 10.15.4) you can set this up yourself with NEAppRule. A very generic example of setting up Safari to trigger the VPN would be:

    var perAppManager = NETunnelProviderManager.forPerAppVPN()
    /* ... */
    NETunnelProviderManager.forPerAppVPN().loadFromPreferences(completionHandler: { error in
      precondition(Thread.isMainThread)
      /* ... */
      let proto = (perAppManager.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
      proto.serverAddress = "server.vpn.com"
      proto.providerBundleIdentifier = "com.perapp-vpn.macOSPacketTunnel.PacketTunnelTest"
      var appRules = [NEAppRule]()
      let appRule = NEAppRule(signingIdentifier: "com.apple.Safari", designatedRequirement: "identifier \"com.apple.Safari\" and anchor apple")
      appRule.matchDomains = ["example.com"]
      appRules.append(appRule)
      perAppManager.appRules = appRules
      perAppManager.isOnDemandEnabled = true
        
      perAppManager.protocolConfiguration = proto
      perAppManager.isEnabled = true
      perAppManager.localizedDescription = "Testing Per-App VPN"
      self.perAppManager.saveToPreferences { saveError in
        /* Proceed to connect */
    }
    })
    

    That was a very generic case and forPerAppVPN() is only available on macOS. A more real-world case world case for iOS would be to create this process through MDM. That entire flow is explained in the documentation I mentioned previously. I would start by just creating a configuration profile in Configurator 2 and testing it out.

    No idea if it works on OpenVPN