How to activate NEPacketTunnelProvider?

Looking over the SimpleTunnel code example, how is the subclassed NEPacketTunnelProvider being used and the startTunnel() function being called? I've looked over the documentation and watched the "What's New in Network Extension and VPN" WWDC15 video and I'm not sure how it's actually started.

When I do a search for "PacketTunnelProvider" I don't see any references outside the file itself aside from the NSExtensionPrincipalClass entry in the associated Info.plist.

Is creating this file and having it present in a system extension enough to "activate" the PacketTunnelProvider class and call startTunnel()? What else must be done?

The SimpleTunnel example uses an App Extension since it's targeting iOS. Am I correct in thinking that for the macOS it should be a System Extension?

Replies

Is creating this file and having it present in a system extension enough to "activate" the PacketTunnelProvider class and call startTunnel()? What else must be done?

If I am understanding you correctly, no, at some point after the System Extension is installed, startVPNTunnel must be called to call over into the NEPacketTunnelProvider.

So the steps here for macOS would be:

1) Create a OSSystemExtensionRequest.activationRequest for your NEPacketTunnelProvider.

2) Use NETunnelProviderManager.loadAllFromPreferences to load your preferences for tunnel configuration. This is where you setup specifics from the container app about your tunnel. Also, where you configure the NETunnelProviderProtocol and any onDemandRules.

3) Call saveToPreferences on your NETunnelProviderManager. This should prompt the Network Configuration prompt to allow.

4) Call connection.startVPNTunnel(options: options) on NETunnelProviderManager to try and start the tunnel from the container app side.

5) Number (4) will trigger the call into the NEPacketTunnelProvider for startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void)

6) In startTunnel(options...) on the provider side you then configure NEPacketTunnelNetworkSettings with the specific IPv4 / IPv6 settings and routes you want to claim for your tunnel. Also, you can setup any specific DNS domains you wish to custom serve.

7) After NEPacketTunnelNetworkSettings are created these settings are passed into setTunnelNetworkSettings and then any error that is triggered here should be passed back to the container app via the completionHandler.

8) You should be off and running at this point.


The SimpleTunnel example uses an App Extension since it's targeting iOS. Am I correct in thinking that for the macOS it should be a System Extension?

Yes, for Catalina and above a Network System Extension is used.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

Hi Matt @meaton , can you clarify on step 4 what the connection.startVPNTunnel(.....) object is? You kind of just pulled it out of nowhere in your explanation.

Also, can you point me to a good code example of setting up an app with a network extension for packet tunneling? I'm new to MacOS development and haven't created a VPN before, so any help you can point me to would be greatly appreciated!

Add a Comment

can you clarify on step 4 what the connection.startVPNTunnel(.....) object is?

If your app has an embedded packet tunnel provider, it might have one or more associated VPN configurations. It can access these by calling +[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:]. Each of those is represented by an NETunnelProviderManager object. NETunnelProviderManager is a subclass of NEVPNManager. NEVPNManager has connection property that you use to manage the state of the VPN tunnel associated with that configuration. That’s of type NEVPNConnection, but in the case of a packet tunnel provider the object will be of type NETunnelProviderSession, a subclass. Both of those types have methods to start the tunnel:

  • -startVPNTunnelAndReturnError:

  • -startTunnelWithOptions:andReturnError:

Either works, but the latter gives you more… hey hey… options.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I've added the connection.startVPNTunnel method to my container app code after lines to create, load and set the tunnel protocol, and save preferences. However, I still cannot activate the NEPacketTunnelProvider. The error message I get when running the app is Error starting the VPN tunnel: Error Domain=NEVPNErrorDomain Code=1 "(null)" and Unable to save tunnel manager preferences. How can I get the packet tunnel provider to start running and showing me packets coming and going through the network? (I don't want to use the NEFilterData/PacketProvider because I want to use the Per-App VPN NEPacketTunnelProvider.) My code is below:

    NETunnelProviderManager *tunnelProviderManager = [NETunnelProviderManager forPerAppVPN];
    tunnelProviderManager.localizedDescription = @"TunnelProviderManager";
    
    [tunnelProviderManager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Unable to load tunnel manager: %s", error.description.UTF8String);
            
        } else {
            NETunnelProviderProtocol *tunnelProtocol = [NETunnelProviderProtocol new];
            tunnelProtocol.providerBundleIdentifier = @".........";
            tunnelProtocol.serverAddress = @"127.0.0.1";
            tunnelProtocol.includeAllNetworks = true;
            
            tunnelProviderManager.protocolConfiguration = tunnelProtocol;
            tunnelProviderManager.enabled = true;
        }
    }];
    
    [tunnelProviderManager saveToPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Unable to save tunnel manager preferences");
        } else {
            NSLog(@"saved tunnel manager preferences");
        }
    }];
    
    NSError *error = nil;
    [tunnelProviderManager.connection startVPNTunnelAndReturnError:&error];
    if (error) {
        NSLog(@"Error starting the VPN tunnel: %@", error);
    } else {
        NSLog(@"Started the VPN tunnel");
    }

Please give me guidance!

Is this one code snippet? Or three separate code snippets? Because if it’s just one snippet then it’s not surprising that the save fails. The load method is asynchronous, so you can’t save immediately after loading because the load hasn’t completed yet. You have to save in the load’s completion handler.

I want to use the Per-App VPN NEPacketTunnelProvider.

Keep in mind that per-app VPN can only be deployed in managed environments. See TN3134 Network Extension provider deployment.

You can use NETestAppMapping for local testing. See here for the details.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Right, as Quinn mentioned, you only want to call the following items after the previous had completed successfully because this behavior is async. That list of events would be:

  • Call loadFromPreferencesWithCompletionHandler.
  • Do your NETunnelProviderManager configuration once the above function has completed.
  • Once the above two bullet points are done, call saveToPreferencesWithCompletionHandler on your manager.
  • Once all of the above are done and have not returned any error call loadFromPreferencesWithCompletionHandler and then startVPNTunnel.

Thank you @eskimo and @meaton!! It seems like my tunnel is started and I can see the IPv4Settings in System Settings > Network > VPN & Filters.

I am curious as to where I can see the log that gets output from the network extension. I have tried using os_log.Logger, NSLog, and print statements, but when I checked the Console app on my MacOS, I don't see the entries. Right now the entry is just to tell me the startTunnel method has been entered, but eventually I want to use it to see that I can capture packets and read the packets before they go on their merry way. I've also tried writing out to a temp file instead of the log, but I also couldn't find the file after running the app. Please see my code snippet of the startTunnel method below:

        let logger = Logger(subsystem: "com.yourcompany.yourapp", category: "NetworkExtension")
        logger.info("Inside startTunnel method")
        NSLog("Inside startTunnel method")
        print("Inside startTunnel method")
        // Add code here to start the process of connecting the tunnel.
        
        let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") // the dest endpoint for tunnel traffic
        
        let ipv4Settings = NEIPv4Settings(addresses: ["1.2.3.4"], subnetMasks: ["255.255.255.255"])
        let includedRoute = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "0.0.0.0") // all routes should go through the tunnel
        ipv4Settings.includedRoutes = [includedRoute]
        networkSettings.ipv4Settings = ipv4Settings
        
        setTunnelNetworkSettings(networkSettings) { error in
            if let error = error {
                NSLog("Error occurred in setTunnelNetworkSettings: \(error.localizedDescription)")
            } else {
                NSLog("setTunnelNetworkSettings complete")
                completionHandler(nil)
            }
        }
        
        completionHandler(nil) // notify the system that the tunnel is ready

Please advise.

  • Glad to hear that your tunnel is up and running!

Add a Comment

If you’re logging at the .info level, make sure you enable the Action > Include Info Messages menu.

You can find this and other tips in Your Friend the System Log.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I am still not able to see log entries from the network extension in the Console app. I've enabled the Info messages under the Action menu, and I've read through the post "Your Friend the System Log". I also read the linked post, "Recording Private Data", but it doesn't look like I've tagged anything as private in my log entries. When I toggle click between Include Info Messages and Include Debug Messages in the Action menu of the Console app, I don't see anything changing even the slightest. Is that expected or is there something wrong with my Console app?

I also tried having the extension write out to file at the start of the startTunnel method with the function below (which works in my test project), but I don't see the file getting created when I run my app. Is the network extension unable to write out to file?

    func writeToFile() {
        guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            // Handle error
            fatalError("Documents directory not found.")
        }

        let fileURL = documentsDirectory.appendingPathComponent("example.txt")

        let text = "Hello, world!"
        if let data = text.data(using: .utf8) {
            do {
                try data.write(to: fileURL)
                print("Data written to file successfully.")
            } catch {
                print("Error writing data to file: \(error)")
            }
        }
    }

I am still not able to see log entries from the network extension in the Console app.

I recommend that you put some log testing code — just a button that logs something using the some logging code as your sysex — into a simple Mac test app. Run that app from Xcode and confirm that it works as expected there. Then run the app from the Finder and look for the logging in Console. If you can see that then you will be able to see the equivalent logging from your sysex. And if you can see the logging from your test app but can’t see your logging from your sysex, it’s likely that the code doing the logging isn’t actually running.

Is the network extension unable to write out to file?

No, but it’s sandboxed and run as root, so the file is probably not showing up where you expect it to show up.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Oh my goodness, I can see the log entries in the Console app now! Hallelujah and thank goodness!

For other newbies to macOS dev like me, here's a few tips I hope would help in following @eskimo's recommendation:

  1. To run your test app from Finder after running it from Xcode, select Xcode "Product" menu and then select "Show Build Folder in Finder". You can double-click on your built target in the Finder popup to run it.
  2. To see the log entries in the Console app, make sure that you click on "Start streaming" when you open up the Console app. Then you can use the search bar to filter for your logs. I kept looking for the system extension logs under Reports > system.log to no avail because it's under Devices > device_name once you start streaming log messages.

Still working on write to file....

While still working on write to file, I'm running into this issue now. Error Domain=NSCocoaErrorDomain Code=513 UserInfo={NSFilePath=<private>, NSUnderlyingError=0x600000c782a0 {Error Domain=NSPOSIXErrorDomain Code=1}}.

I read the thread https://developer.apple.com/forums/thread/63075 and added .noFileProtection to my write method call, but that didn't fix the issue. Since the network extension works as root to write to file and I want to write to /private/tmp/example.txt, why am I seeing this error?

The underlying error there is 1, or EPERM, which means you’re hitting some sort of sandbox or MAC restriction. See On File System Permissions.

I want to write to /private/tmp/example.txt

That’d do it. Your NE sysex is sandboxed and thus can’t write to arbitrary file system paths like this. For debugging it’d be reasonable to add a temporary exception entitlement but, if this is for your real product, you’ll need to rethink how you use the file system.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"