Split tunnelling with Network Extension

Hi everyone!
I have recently started exploration of Network Extension capabilities in macOS. Currently I am trying to write app using network extension which should provide kind of split tunnelling. The idea is to check destinations of outgoing ip packets and when they match with some list of predefined addresses redirect these packets to another address(which is belongs to some server). If the destination doesn't match with list' elements, packet should not be redirected and simply go to the original target.
I have looked through developers' forum and watched WWDC19 session about NE. Currently I'm thinking that the best approach is to use PacketTunnelProvider to redirect packets. But I'm not sure that I'll be able to achieve needed behaviour. Is it possible to redirect packets using NE? Is it possible at all to check destination address of the outgoing packets? Also it would be great if anybody could provide a link to example project which use split tunnelling. I don't work with networks on a daily basis and will be glad to get any help from the community
Answered by Systems Engineer in 668170022

And I don't understand what am I doing wrong. Could I use NEIPv4Settings() configurations of the machine in my local network?

First, check that setTunnelNetworkSettings is not producing an error before readPackets is called. Next, you should provide a tunnel remote address with your NEPacketTunnelNetworkSettings. This is the the address or hostname that you added to your serverAddress out in NETunnelProviderProtocol. And finally, yes, it is good to compare your NEIPv4Settings against traffic locally that you want to claim.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

Currently I'm thinking that the best approach is to use PacketTunnelProvider to redirect packets.

For a connection based Network Extension option, you could also take a look at NETransparentProxyProvider. Keep in mind that this works at the connection level and not the packet level.

Is it possible to redirect packets using NE?

Technically there is nothing stopping you from rewriting an NEPacket with an new destination address. The things you have to be mindful of is the behavior that this introduces. For example, if a host is performing header checksums, does this alter the checksum? Also, if the destination address is altered, does this introduce any oddities when the packet is written back to the interface.

Is it possible at all to check destination address of the outgoing packets?

Yes. You would need to parse the IP header of the packet and extract the destination address.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
@meaton
Thank you for quick response and detailed explanation!
Currently I am also struggled with launching extension that use Packet Tunnel Provider. I have been trying to make an app without GUI that just launch the extension. To check that tunnel started I want to log some message from startTunnel() and see it in Console:
Code Block swift
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void)
{
        os_log(.default, "Tunnel started successfuly")
        let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "some address")
        setTunnelNetworkSettings(settings) { error in
            completionHandler(error)
        }
}

Currently when I launch the app, dialog appears with message which says that application trying to install system extension. After opening "Security and Privacy" option in System Preferences and allowing installation I can found in systemextensionctl list that the extension has status "activated enabled". But there is no messages in Console about tunnel start.

To achieve 'no GUI' goal I removed ViewController from project and modified AppDelegate::applicationDidFinishLaunching():
Code Block swift
func applicationDidFinishLaunching(_ aNotification: Notification)
 {
  guard let extensionIdentifier = self.extensionBundle.bundleIdentifier else
  {
          os_log(.default, "Problems with extension bundle ID")
            return
  }
os_log(.default, "SE activation request")
let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main)
  activationRequest.delegate = self
OSSystemExtensionManager.shared.submitRequest(activationRequest)
}

To handle activation request result I have also added next thing:
Code Block swift
extension AppDelegate: OSSystemExtensionRequestDelegate
{
    func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result)
    {
        guard result == .completed else
        {
            os_log(.default, "Unexpected result for SystemExtension activation request: %@", result.rawValue)
            return
        }
  enableTunnelConfiguration()
    }
}

enableTunnelConfiguration:
Code Block swift
func enableTunnelConfiguration()
{
        NETunnelProviderManager.loadAllFromPreferences { managers, error in
            if let loadError = error
            {
                os_log("Error while loading from preferences: %@", loadError.localizedDescription)
            }
            let managersList = managers ?? []
            if managersList.count > 0
            {
                self.tunnelManager = managersList.first!
            }
            else
            {
                self.tunnelManager = self.makeManager()
            }
            self.tunnelManager.saveToPreferences { error in
               if let saveError = error
               {
                   os_log("Error while saving to prefs: %@", saveError.localizedDescription)
                   return
               }
               os_log("Successfuly saved")
           }
        }
        do
        {
            try self.tunnelManager.connection.startVPNTunnel()
        }
        catch
        {
            os_log("Failed to start tunnel")
        }
}

and makeManager():
Code Block swift
 private func makeManager() -> NETunnelProviderManager {
        let manager = NETunnelProviderManager()
        manager.localizedDescription = "CustomVPN"
        let proto = NETunnelProviderProtocol()
        proto.providerBundleIdentifier = "extension bundle id"
        proto.serverAddress = "same as address in startTunnel()"
        proto.providerConfiguration = [:]
        manager.protocolConfiguration = proto
        manager.isEnabled = true
        return manager
    }

Also I'm getting log "Failed to start tunnel" after startVPNTunnel() call. Should I call this method to start tunnel and see desired message. Or extension is always launch automatically when app starts. Could it be issue with entitlements or code signing? Is it mandatory to use NETunnelManagerProvider to successfully launch extension? I save only one configuration in app, so managers list will contain only one element always? Would be glad if you provide explanation one more time. Sorry if my questions sounds dumb


There could be many things going on here, but the first thing we need to start with is this:

Currently I am also struggled with launching extension that use Packet Tunnel Provider. I have been trying to make an app without GUI that just launch the extension.

You need a container app with a user interface to make sure the user knows which app is prompting the user to install the Network System Extension, install the network configuration, and then connect the packet tunnel.

Installing the Network System Extension and installing the network configuration are especially important because they provide prompts that the user needs to interact with to proceed. This prompts need to be interacted with.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Thank you for reply, @meaton!
I have created container app for extension and currently it provide two prompts for user: first one to allow the system extension installation and second to allow access to VPN configuration.
But let's return to my first question about creating a split tunnel using NEPacketTunnelProvider. Besides WWDC session about Network Extension I have watched also Session 717: What’s New in NetworkExtension and VPN. In documentation I found that includeRoutes method from NEIPvSettings class may help me. As I understood, when outgoing ipv4 packets' destination adress matches with list that has been used with includeRoutes then they pass to TUN interface and to NEPacketTunnelProvider which redirects them to tunnel server. So the flow of outgoing packets could be described as:

When the packet destination matches with list item -> redirect to TUN interface -> redirect to NEPacketTunnelProvider -> redirect to tunnel server. Otherwise packets simply going to original destination without passing to TUN interface.

Do I understand the packets flow in case of NEPacketTunnelProvider right?



Do I understand the packets flow in case of NEPacketTunnelProvider right?

In general, yes. However, if you start to see inconsistencies of the packets that should be routed via destination address, be mindful of the source address as well.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
One more question. Currently I have tried to log number of packets received by the tunnel provider using readPackets(). I have wrote the next code to achieve that
Code Block swift
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void)
 {
        os_log(.default, "Tunnel is starting...")
        self.pendingCompletion = completionHandler
        self.startPacketHandling()
 }   

Where startPacketHandling() is a private method which configure settings with IP and mask of my local machine:
Code Block swift
 private func startPacketHandling()
 {
        os_log(.default, "Start packet handling")
        let settings = NEPacketTunnelNetworkSettings()
        settings.ipv4Settings = NEIPv4Settings(addresses: ["ip of machine"], subnetMasks: ["255.255.255.0"] )
        settings.ipv4Settings?.includedRoutes = [defaultIPv4Route]
        setTunnelNetworkSettings(settings)
        { error in
            self.pendingCompletion?(nil)
            self.pendingCompletion = nil
            self.readPackets();
        }
    }

In readPackets() I try to perform logging:
Code Block swift
private func readPackets()
{
        os_log(.default, "Package reading started")
        self.packetFlow.readPackets
        {  packets, protocols in
            os_log("Packets received from tun interface: %@", packets.count)
            self.readPackets() // making a recursive call to be able to continue reading the packets
        }
 }

And now when I start the app with the extension I could see only logs till "Package reading started". But no messages about number of received packets. Hence self.packetFlow.readPackets() don't work. And I don't understand what am I doing wrong. Could I use NEIPv4Settings() configurations of the machine in my local network?

Accepted Answer

And I don't understand what am I doing wrong. Could I use NEIPv4Settings() configurations of the machine in my local network?

First, check that setTunnelNetworkSettings is not producing an error before readPackets is called. Next, you should provide a tunnel remote address with your NEPacketTunnelNetworkSettings. This is the the address or hostname that you added to your serverAddress out in NETunnelProviderProtocol. And finally, yes, it is good to compare your NEIPv4Settings against traffic locally that you want to claim.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Split tunnelling with Network Extension
 
 
Q