如何将网络扩展 NEPacketTunnelProvider class 从 Obj-C/Swift 移植到 Xamarin C#?

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

我正在尝试弄清楚如何进行网络扩展,以便我的 iOS 应用程序可以在 C# 中以编程方式打开自定义 VPN 隧道,但查看一些类似的 Obj-C 项目我不是确定在 Xamarin 中是否可行(因为我在 Visual Studio 中没有看到网络扩展项目)以及如何移植我收集的是必需的 PacketTunnelProvider class,我认为它必须存在并列为首先是 plist.info 中的一个扩展...我在如何移植最后作为扩展出现的 class 部分和一些像这样命名的事件处理程序方面遇到了特别大的麻烦 func Adapter(适配器:适配器,configureTunnelWithNetworkSettings networkSettings:NEPacketTunnelNetworkSettings,completionHandler:@escaping(AdapterPacketFlow?)-> Void)func Adapter(适配器:Adapter,handleEvent 事件:AdapterEvent , message: String?) 因为它们都具有与 C# 中接受 sender 和 eventArgs(或派生的东西)的事件处理程序不同的签名......如果有人在 C# 中做过这件事我想至少知道是否有可能如果不能如何移植这样的 class?

我找到了这个项目 https://github.com/ss-abramchuk/OpenVPNAdapter(因为它似乎完成了我想要的大部分工作),我设法将其转换为 Xamarin 绑定库,但我不确定如何以及是否将其合并Xamarin 中的 PacketTunnelProvider class(因为这就是自述文件所说的,您应该使用它来合并类似适配器的东西)...我认为应该先添加到 plist.info 中,像这样:

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

但是您从那里去哪里使用绑定库?这是 Obj-C 代码,它说并似乎做了我想做的事情,即将那个自定义 VPN 协议隧道添加到使用库的应用程序:

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

然后我如何移植到 C# 或者我做错了(因为下面的评论 - 绑定 dll 大于 15MB - 或者是关于内存使用的限制不是与文件大小有关)?我真的应该只是引用自定义 VPN 库直接从代码打开 VPN 隧道,然后像往常一样从那里继续(因为我还发现了一个使用 TunnelKit cocoapod 的 project/app https://github.com/passepartoutvpn ,但该应用程序的库不能与 sharpie 一起使用来制作绑定库,如果是的话,这样的应用程序是否会被 AppStore 接受)?提前感谢您的帮助。


根据@SushiHangover 的建议,我尝试绑定 TunnelKit,因为该项目看起来更小,并且成功了,部分...我已经成功构建了 ~3MB dll,这似乎比 21MB 的 OpenVPNAdapter 小得多,而且我想我已经快完成 NetworkExtension 项目了...我只是想弄清楚我使用@escaping completionHandler 是否正常以及如何获得一些我想应该以某种方式在主机应用程序中设置组常量?

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

我现在已经注释掉了 groupcontants,但至少我希望这足以移植 Swift3:

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

如果其他人知道组常量以及如何获得它们,我将不胜感激(但我还应该注意 sharpie pod 没有give/expose 我应该分配的任何字段。 也许我做错了,因为 TunnelKit 是一个完全 Swift3 项目,与 OpenVPNAdapter 不同:/

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?

对于 iOS 12+,您绝对必须使用网络扩展框架才能符合商店条件。

Xamarin.iOS 构建任务 (ValidateAppBundle) 正确地将 com.apple.networkextension.packet-tunnel 识别为有效扩展 (.appex) 所以是的,您可以构建 NEPacketTunnelProvider 扩展。

你是对的,VS 没有网络提供商 .appex 的隧道、dns 代理、过滤器控制|数据、代理类型的内置模板,但这并不意味着你不能只使用另一个模板(或从头开始创建项目)并修改它(我创建了一个 Xcode iOS 项目并开始添加扩展目标并在 VS 中反映这些更改)。

(仅供参考:在您的示例中是 Swift 代码,而不是 ObjC...)

现在由于 .appex 大小的限制(以及某些情况下的性能问题),许多扩展很难通过 Xamarin.iOS 完成。大多数遇到这种情况的开发人员,至少在 appex 开发中使用 ObjC/Swift 进行本地化...