2

I'm new to iOS development, and am writing a VPN app, using the OpenVPN framework. I'm making use of the Network Extension target to tunnel network traffic to our OpenVPN server. However, when trying to debug, I can't attach the debugger to the Network Extension - it's stuck at "Waiting to Attach" in the Debug Navigator view.

In this blog post by Alex Grebenyuk he gives some recommendations for troubleshooting when you fail to attach the debugger to the extension. I've meticulously checked all of them, and my app correctly uses everything there: Both the app and the network extension have the Packet Tunnel Network Extension capability and bundle names all work out.

Some more info on the bundle names in case it might be relevant:

  • Bundle name for the app: com.example.Example-App
  • Bundle name for the Network Extension: com.example.Example-App.Example-VPN
  • Group name: group.com.example.Example-App

I'm out of ideas here. Could the problem be in the Network Extension never being started? It doesn't seem to be the case, because the startTunnel() function is called. The code for starting the VPN through the extension:

func configureVPN(serverAddress: String) {
    guard let configData = readTestFile() else { return } 
    // Test file with ovpn configuration seems is read in correctly - contents are shown if printed to console.
    self.providerManager?.loadFromPreferences { error in
        if error == nil {
            let tunnelProtocol = NETunnelProviderProtocol()
            tunnelProtocol.serverAddress = serverAddress
            tunnelProtocol.providerBundleIdentifier = "com.example.Example-App.Example-VPN" // bundle id of the network extension target
            tunnelProtocol.providerConfiguration = ["ovpn": configData]
            self.providerManager.protocolConfiguration = tunnelProtocol
            self.providerManager.localizedDescription = "Example VPN"
            self.providerManager.isEnabled = true
            self.providerManager.saveToPreferences(completionHandler: { (error) in
                if error == nil  {
                    self.providerManager.loadFromPreferences(completionHandler: { (error) in
                        do {
                            print("Trying to start connection now") // This line is printed to the Console
                            try self.providerManager.connection.startVPNTunnel() // start the VPN tunnel.
                        } catch let error {
                            print(error.localizedDescription)
                        }
                    })
                }
            })
        }
    }
}

Not sure if this is relevant information, but I am using the OpenVPNAdapter in the PacketTunnelProvider, following this tutorial:

import NetworkExtension
import OpenVPNAdapter

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 = [ :
            // 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
            let password: String = "Test"

            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 as! OpenVPNAdapterPacketFlow)
    }

    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)
        }
    }
}

Any help with this would be hugely appreciated! Thanks.

Jorn Rigter
  • 745
  • 1
  • 6
  • 25

1 Answers1

0

You need to attache the bundle name and not the bundle identifier and the bundle name should not be an identifier. The PRODUCT_BUNDLE_IDENTIFIER of your network extension would be something like com.example.my-network-extension-id (it cannot be com.example.Example-App.Example-VPN, as a third period is not allowed for network extension IDs) and the PRODUCT_NAME would be something like My Network Extension, and you would then attach to My Network Extension (yes, with spaces and no extension).

Mecki
  • 125,244
  • 33
  • 244
  • 253