xamarinopenvpnnetworkextension

How to port a network extension NEPacketTunnelProvider class from Obj-C/Swift to Xamarin C#?


I'm trying to figure out how to make a network extension so that my iOS app can programmatically open an custom VPN tunnel in C#, but looking at some similar Obj-C projects I'm not sure if it's possible in Xamarin (as I don't see a network extension project in Visual Studio) and how to port a what I gather is a required PacketTunnelProvider class which I think must be present and listed as an extension in the plist.info first...I'm in particular having most trouble in how to port the parts of that class which appear at the end as an extension and some event handlers named like this func Adapter(adapter: Adapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings, completionHandler: @escaping (AdapterPacketFlow?) -> Void) and func Adapter(adapter: Adapter, handleEvent event: AdapterEvent, message: String?) as they both have a different signature than an event handler in C# which accepts sender and eventArgs (or something derived)…if anyone did this in C# I'd like to know at least if it's possible if not how to port such a class?

I've found this one project https://github.com/ss-abramchuk/OpenVPNAdapter (since it seems to do most of what I want) that I managed to translate into a Xamarin binding library but I'm unsure how and if to incorporate its PacketTunnelProvider class in Xamarin (as that is what the readme says you should use to incorporate something like that adapter)...I gather one should add to plist.info something like this first:

<key>NSExtension</key>
<dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.networkextension.packet-tunnel</string>
    <key>NSExtensionPrincipalClass</key>
    <string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>

but where do you go from there to use the binding library? This is the Obj-C code that says and seemingly does what I want to do to after i.e. add that custom VPN protocol tunnel to an app using the library:

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
        ]

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

}

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(settings) {
        (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
}

}

// Extend NEPacketTunnelFlow to adopt OpenVPNAdapterPacketFlow protocol so that
// `self.packetFlow` could be sent to `completionHandler` callback of OpenVPNAdapterDelegate
// method openVPNAdapter(openVPNAdapter:configureTunnelWithNetworkSettings:completionHandler).
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

How do I port to C# then or maybe I'm doing it all wrong (because of the comment bellow - the binding dll is bigger than 15MB - or is that limit in regard to use of memory which isn't related to file size)? Should I actually be just referencing the custom VPN library to open up a VPN tunnel from code directly and go on from there like it's business as usual (as I also found a project/app https://github.com/passepartoutvpn which uses a TunnelKit cocoapod, but that app's lib won't work with sharpie to make the binding library, and if so would the app like that even be admissible to the AppStore)? Thank you for any help in advance.


Per @SushiHangover advice, I've tried binding TunnelKit, as that project seemed smaller, and succeeded, partially... I've managed to build ~3MB dll, which seems much smaller than 21MB OpenVPNAdapter, and I think I'm almost there with the NetworkExtension project...I've got just to figure out the did I do ok with @escaping completionHandler and how to get some group constants which I guess should be set within the Host app somehow?

public override void StartTunnel(NSDictionary<NSString, NSObject> options, Action<NSError> completionHandler)
    {
        //appVersion = "\(GroupConstants.App.name) \(GroupConstants.App.versionString)";
        //dnsTimeout = GroupConstants.VPN.dnsTimeout;
        //logSeparator = GroupConstants.VPN.sessionMarker;
        base.StartTunnel(options, completionHandler);
    }

I've commented out the groupcontants for now but at least I'm hoping that's good enough porting of Swift3's:

override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
    appVersion = "\(GroupConstants.App.name) \(GroupConstants.App.versionString)"
    dnsTimeout = GroupConstants.VPN.dnsTimeout
    logSeparator = GroupConstants.VPN.sessionMarker
    super.startTunnel(options: options, completionHandler: completionHandler)
}

If anyone else knows about the group constants and how to get them I'd be grateful (but I should also note that sharpie pod didn't give/expose any of those fields I should be assigning. Maybe I did it wrong as that's TunnelKit is a completely Swift3 project unlike the OpenVPNAdapter :/


Solution

  • Should I actually be just using the a custom VPN library to open up a VPN tunnel and go from there, but would the app then be admissible to the AppStore?

    For iOS 12+, you absolutely have to use the Network Extension framework to be Store eligible.

    The Xamarin.iOS build task (ValidateAppBundle) correctly identifies com.apple.networkextension.packet-tunnel as a valid extension (.appex) so yes you can build an NEPacketTunnelProvider extension.

    You are correct the VS does not have build-in templating for Network Provider .appex's for tunnels, dns proxy, filter control|data, proxy types, but that does not mean you can not just use another one of the templates (or create the project from scratch) and modify it (I create an Xcode iOS project and start adding extension targets and just mirror those changes in VS).

    (FYI: That is Swift code in your example, not ObjC...)

    Now due to limitations in .appex size (and performance issues in some cases), a lot of extensions are very difficult to do via Xamarin.iOS. Most devs that encounter this, go native using ObjC/Swift for at least the appex development...