Very ignorant VPN network extension question

The documentation for, eg NEAppProxyProvider, has a lot of redirections and implicit configurations that I simply don't know and sometimes can't follow. I wanted to try making a VPN network extension that (for the moment) just got loaded and did nothing. I can't even get that far, it seems. I'm clearly missing the setup I need to do.

Unfortunately, the old SimpleTunnelCustomizedNetworkingUsingtheNetworkExtensionFramework sample doesn't build anymore, what with it being Swift 3 based. Is there a newer, made-for-idiots sample program somewhere?

Is there a newer, made-for-idiots sample program somewhere?

There is not a newer sample for NEAppProxyProvider out there. I would open an enhancement request for this.

Having said that, I am assuming that since you mentioned Simple Tunnel that this is for iOS? If so, then for iOS NEAppProxyProvider requires a supervised device for deployment.

If this is for macOS, can you tell me where you are running into issues? Also, you can open a TSI if you want me to look at your project in greater detail.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

(Apparently only answers get formatting, so this isn't an answer but here it goes anyway.)

My code is shamefully bad, of course. I can hopefully be forgiven due to the lack of sample code...

I have "class AppProxyProvider: NEAppProxyProvider" in the extension; however, it does not seem to load. Interestingly, I can see it in Network preferences -- but it says "Please use 'AppProxyTest' to control this content filter configuration." But then I'm back to not sure what I'm supposed to do...

This is the code I'm using to try to load it; there is a lot of extraneous code in there, I'm aware, as well as a lot of prints, because those continue to be my favourite method for tracing progress. I have os_log() calls in the extension, but as I said, it doesn't seem to get loaded.

    private func loadExtension() {
        guard let extensionIdentifier = extensionBundle.bundleIdentifier else {
            print("Can't get the extension identifier")
            return
        }

        NEAppProxyProviderManager.loadAllFromPreferences() { providers, error in
            if let err = error {
                print("Got provider manager error \(err.localizedDescription)")
            } else {
                print("providers = \(providers)")
            }
        }

        let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main)
        activationRequest.delegate = self
        OSSystemExtensionManager.shared.submitRequest(activationRequest)
        os_log("Bundle ID = %{public}@", activationRequest.identifier)

        let filterManager = NEFilterManager.shared()
        filterManager.loadFromPreferences { error in
            if error != nil {
                print("The first loadFromPreferences got error \(error)")
            } else {
                print("Was able to load filterManager preferences the first time")
            }
        }

        if filterManager.providerConfiguration == nil {
            let providerConfiguration = NEFilterProviderConfiguration()
            providerConfiguration.filterSockets = false
            providerConfiguration.filterPackets = true
            filterManager.providerConfiguration = providerConfiguration
            if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
                filterManager.localizedDescription = appName
            }
            print("providerConfiguration = \(providerConfiguration)")
        } else {
            print("filterManager.providerconfiguration = \(filterManager.providerConfiguration)")
        }

        filterManager.isEnabled = true
        filterManager.localizedDescription = "Test App Proxy Provider"
        print("filterManager = \(filterManager)")

        filterManager.saveToPreferences { saveError in
            if saveError == nil {
                return
            }
            if let error = saveError as NSError? {
                print("Failed to save the filter configuration: \(error)")
            }
        }

        filterManager.loadFromPreferences() { error in
            if let err = error {
                print("The second loadFromPreferences got error \(err.localizedDescription)")
            }
        }
    }

Since you are on macOS I am going to frame my response in the form of NETransparentProxyManager that uses NEAppProxyProvider.

Install the system extension:

// Install the system extension:
let request = OSSystemExtensionRequest.activationRequest(
    forExtensionWithIdentifier: "com.example.apple-samplecode.TransparentProxyTestBed.TransparentProxy",
    queue: .main
)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)

// Use the OSSystemExtensionRequestDelegate to realize the status
extension ViewController: OSSystemExtensionRequestDelegate {
    
    func request(_ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension replacement: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
        // If a System Extension is already running, replace it.
        return .replace
    }

    func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
        // Process OSSystemExtensionRequest
    }
    
    func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
        // Log the error
    }
}

Create the network configuration:

NETransparentProxyManager.loadAllFromPreferences { managers, error in
    precondition(Thread.isMainThread)
    if let nsError = error as NSError? {
        // Handle error
        return
    }
    let NEManagers = managers ?? []
    
    let managerPT = NEManagers.first(where: Self.isOurManager(_:))
    let manager: NETransparentProxyManager
    if let m = managerPT {
        manager = m
    } else {
        manager = NETransparentProxyManager()
    }
    self.storeConfiguration(in: manager)
    manager.saveToPreferences { errorQ in
        if let nsError = errorQ as NSError? {
            // Handle error
            return
        }
        // Notify that the configuration was saved.
    }
}

private static func isOurManager(_ manager: NETransparentProxyManager) -> Bool {
    guard
        let proto = manager.protocolConfiguration,
        let tunnelProto = proto as? NETunnelProviderProtocol
    else {
        return false
    }
    return tunnelProto.providerBundleIdentifier == "com.example.apple-samplecode.TransparentProxyTestBed.TransparentProxy"
}

private func storeConfiguration(in manager: NETransparentProxyManager) {
    let proto = (manager.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
    // This is only for sample purposes to get you off the ground.
    proto.serverAddress = "localhost"
    proto.providerBundleIdentifier = "com.example.apple-samplecode.TransparentProxyTestBed.TransparentProxy"
    manager.protocolConfiguration = proto
    manager.isEnabled = true
    manager.localizedDescription = "Testing Transparent Proxy"
}

Call startVPNTunnel to kick off the proxy:


NETransparentProxyManager.loadAllFromPreferences { managers, error in
    precondition(Thread.isMainThread)
    if let nsError = error as NSError? {
        // Handle error
        return
    }
    let NEManagers = managers ?? []
    
    guard let manager = NEManagers.first(where: Self.isOurManager(_:)) else {
        return
    }
    do {
        let options: [String : NSObject]? = ["Optional": "Value" as NSObject]
        try manager.connection.startVPNTunnel(options: options)
        // Handle Success
    } catch {
        // Handle error
    }
}

Now, over in your provider, make sure you are using os_log statements to make sure you can debug what is happening in the provider.

final class TransparentProxyProvider: NEAppProxyProvider {
    
    static let log = OSLog(subsystem: "com.example.apple-samplecode.TransparentProxyTestBed.TransparentProxy", category: "provider")
    private let queue = DispatchQueue(label: "TransparentProxyProvider", autoreleaseFrequency: .workItem)
    private let log: OSLog

    override init() {
        self.log = Self.log
        os_log(.debug, log: self.log, "init")
        super.init()
    }

    override func startProxy(options: [String: Any]? = nil, completionHandler: @escaping (Error?) -> Void) {
        os_log(.debug, log: self.log, "provider will start")
        self.queue.async {
            // Again, for testing only.
            let settings = NETransparentProxyNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
            settings.includedNetworkRules = [
            
	            NENetworkRule(remoteNetwork: NWHostEndpoint(hostname: "example.com", port: "443"),
	                          remotePrefix: 0,
	                          localNetwork: nil,
	                          localPrefix: 0,
	                          protocol:.TCP,
	                          direction: .outbound)
	        ]
            
            // If it succeeds, start the tunnel.
            self.setTunnelNetworkSettings(settings) { error in
                if let err = error as NSError? {
                    completionHandler(err)
                    // Handle error
                    return
                }
                // All is good.
                completionHandler(nil)
 
            }
        }
    }

    override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
        
        os_log(.debug, log: self.log, "provider will handle new flow, flow: %{public}@", flow.description)
        return true
    }

}

To use these os_log statements log out the subsystem here:

$ log stream --level debug --predicate 'subsystem == "com.example.apple-samplecode.TransparentProxyTestBed.TransparentProxy"'
Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Very ignorant VPN network extension question
 
 
Q