How to start NEAppProxyProvider?

Hey everyone, I try to figure out how it is possible to say the OS to call my overridden function "startProxy(options...)" from my extension. I have all the entitlements and profiles to build and run the app, but the function is not called.

Do I have to configure something ( maybe the NEAppProxyProviderManager.shared() ? ) in the containing main app that the operating systems knows that there is a proxy to use?


I had a look at the SimpleTunnel implementation from Apple, but I don't get it where in the code the system it told that it has to start using the proxy extension.


PS: Is it possible to test the AppProxyProvider without having a running proxy server anywhere? For example to say the serveraddress is the local gateway ,pass the data to it and it handles them as a normal packets from the device?


Thanks and greets,

effe2402

Replies

Is it possible to test the AppProxyProvider without having a running proxy server anywhere? For example to say the serveraddress is the local gateway, pass the data to it and it handles them as a normal packets from the device?

That might be possible in the case of an app proxy provider. The thing you have to watch out for is routing. In a packet tunnel provider you want to become the default route, so you see all the traffic, but then you can’t make arbitrary outgoing connections because those will come back through your provider. An app proxy provider doesn’t become the default route — it gets traffic from specific source applications — so it may be able to make arbitrary outgoing connections.

I’ve not tried it myself though.

Do I have to configure something … in the containing main app that the operating systems knows that there is a proxy to use?

You need to start the provider. In a production setup you’d usually start on demand but for testing I’ve found it’s best to start it manually, using a control in the containing app’s UI. Doing this is quite straightforward:

  1. Get a reference to your NEAppProxyProviderManager

  2. Read the

    connection
    property
  3. Cast it to NETunnelProviderSession

  4. Call

    startTunnel(options:)
    on that

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for your answer, I tried out your advice, but I always get "nilError", because the ProviderManager seems to be uninitialized. I do the following :

  
    var proxyManager = NEAppProxyProviderManager()
    @IBOutlet weak var wv: UIWebView!
  
    override func viewDidLoad() {
        super.viewDidLoad()
      
      
        initProviderManager()
        /
        wv.loadRequest(URLRequest(url: URL(string: "https://www.google.com")!))
    }
   
    private func initProviderManager() {
        let session = self.proxyManager.connection as! NETunnelProviderSession
        do {
          try session.startTunnel(options: nil)
        }
        catch {
            print(error)
        }
      
     


At line 43, the proxyManager is uninitialized. If I use NEAppProxyProviderManager.shared() instead, there is an error that I can't cast connection to NETunnelProviderSession. Do I have to call the "loadAllFromPreferences" Function before?

At line 43 …

Just FYI, your line numbers don’t match up with what you posted. It doesn’t matter in this case but it’s something to watch out for in the future.

Do I have to call the "loadAllFromPreferences" Function before?

Yes. Here’s what I do.

private func startLoading() {
    guard case .loading = self.state else { fatalError() }
    NEAppProxyProviderManager.loadAllFromPreferences { (managers, error) in
        assert(Thread.isMainThread)
        if let error = error {
            self.state = .failed(error: error)
        } else {
            let manager = managers?.first ?? NEAppProxyProviderManager()
            self.state = .loaded(snapshot: Snapshot(from: manager), manager: manager)
        }
    }
}

IMPORTANT Line 8 is wrong. It should be something like this:

guard let manager = managers?.first else {
    self.state = .failed(error: ***)
    return
}

I carried that line over from my packet tunnel provider, where this design makes sense. In an app proxy provider you can’t construct a manager from scratch because an app proxy provider can only be configured via a configuration profile. I’ll get around to fixing that the next time I crack open my project, but I wanted to post the code as is rather than making untested edits in a text editor )-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi,


In my case, managers and error are both nil


- (void)viewDidLoad {
    [super viewDidLoad];
    /
    /
    [NEAppProxyProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray * managers,NSError * error){
        NSLog(@"loadall %@, %@",managers, error);
    }];
}


This is in my container app which contains my NEAppProxyProvider extension.

I am not able to get startProxyWithOptions called by the system...

I am running from xcode using the Extension target and selecting the container app.

There’s two things I’d check here:

  • Entitlements — There’s instructions for this in my Debugging Entitlement Issues post.

    IMORTANT Make sure you start by dumping the entitlements of your built binary (that is, your

    .app
    and the
    .appex
    nested within that app). These are are the entitlements that the system actually checks.
  • Configuration profile — Make sure your configuration profile is set up correctly. Pasted in below is an excerpt from the profile I use for my test project.

…
<dict>
    …
    <key>PayloadType</key>
    <string>com.apple.vpn.managed.applayer</string>
    <key>VPNType</key>
    <string>VPN</string>
    <key>VPNSubType</key>
    <string>com.example.apple-samplecode.QNE-iOS.AppProxy</string>
    <key>VPNUUID</key>
    <string>825886EA-BB00-4805-ADD6-1674C531669E</string>
    <key>VPN</key>
    <dict>
        <key>RemoteAddress</key>
        <string>example.com</string>
        <key>AuthenticationMethod</key>
        <string>Password</string>
        <key>AuthName</key>
        <string>mrgumby</string>
        <key>AuthPassword</key>
        <string>opendoor</string>
    </dict>
    <key>Proxies</key>
    <dict>
        …
    </dict>
</dict>
…

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Do I need to instantiate a NETunnelProviderManager?


Does the configuration profile need to be signed? I am installing it via Configurator 2.


I have set NETestAppMapping item in my container app Info.plist. Do I also need this in extension Info.plist?


I believe there is no problem with my entitlements, yet my AppProvider callbacks are not getting called.

Does the configuration profile need to be signed?

No.

I have set NETestAppMapping item in my container app Info.plist. Do I also need this in extension Info.plist?

No.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Ok, its working for me now! I had a bad line in the configuration profile. Doh! Now I can haz flowz


Thank you!

Hi Quinn,


I'm following the SimpleTunnel project for a research which I want to use NEAppProxyProvider without any server to listen TCP and UDP packets only in client side. My entitlements are ok and I prepared a configuration profile. I could enable to start NEAppProxyProvider but there are a few things that I want to be sure about. In my configuration profile there are VPN related key and values. Is this means there should be a VPN running at some place which ClientTunnel is going to connect?

  1. Can I prepare a configuration profile without VPN related stuff, and then I can start the NEAppProxyProvider?
  2. As I see AppProxyProvider starts a ClientTunnel and ClientTunnel tries to connect the remote ip that I set on configuration profile. I want to eliminate this and just want to listen packets, is this possible?


Thanks in advance.

In my configuration profile there are VPN related key and values. Is this means there should be a VPN running at some place which ClientTunnel is going to connect?

App proxy providers are a flavour of pre-app VPN, so a configuration profile that configures an app proxy provider is going to have to contain a lot of VPN-related stuff. That does not, however, mean that an app proxy provider must necessarily implement VPN, although it has to something with the traffic that comes in via the various

NEAppProxyFlow
objects.

Here’s an example of the payload I use to configure my test app proxy provider:

<dict>
    … PayloadUUID, PayloadIdentifier, PayloadDescription, PayloadDisplayName, PayloadVersion, UserDefinedName …
    <key>PayloadType</key>
    <string>com.apple.vpn.managed.applayer</string>
    <key>VPNType</key>
    <string>VPN</string>
    <key>VPNSubType</key>
    <string>com.example.apple-samplecode.QNE-iOS.AppProxy</string>
    <key>VPNUUID</key>
    <string>825886EA-BB00-4805-ADD6-1674C531669E</string>
    <key>VPN</key>
    <dict>
        <key>RemoteAddress</key>
        <string>example.com</string>
        <key>AuthenticationMethod</key>
        <string>Password</string>
        <key>AuthName</key>
        <string>mrgumby</string>
        <key>AuthPassword</key>
        <string>opendoor</string>
    </dict>
</dict>

Two things:

  • I’ve elided all the standard stuff,

    PayloadUUID
    ,
    PayloadIdentifier
    and so on.
  • I believe that everything within the

    VPN
    dictionary is optional except
    RemoteAddress
    . However, that address doesn’t need to point to anything meaningful. The system doesn’t try to connect to this.

Finally, be aware that the above is about the technical aspects of this issue. There are also business constraints on how you can use this technology (various legal agreements, App Review, and so on). If you take this beyond the research phase, you will have to consider those as well.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for your answer Quinn. My AppProxyProvider's startProxy function is like this and tries to open a tunnel connection.


override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) {
        let newTunnel = ClientTunnel()
        newTunnel.delegate = self

        if let error = newTunnel.startTunnel(self) {
            completionHandler(error as NSError)
            return
        }

        pendingStartCompletion = completionHandler
        tunnel = newTunnel
    }


On ClientTunnel startTunnel function


func startTunnel(_ provider: NETunnelProvider) -> SimpleTunnelError? {
  guard let serverAddress = provider.protocolConfiguration.serverAddress else {
      return .badConfiguration
  }
  let endpoint: NWEndpoint
  if let colonRange = serverAddress.rangeOfCharacter(from: CharacterSet(charactersIn: ":"), options: [], range: nil) {
      let hostname = String(serverAddress[serverAddress.startIndex..<colonRange.lowerBound])
      let portString = String(serverAddress[serverAddress.index(after: colonRange.lowerBound)..<serverAddress.endIndex])
  
      guard !hostname.isEmpty && !portString.isEmpty else {
            return .badConfiguration
      }
      endpoint = NWHostEndpoint(hostname:hostname, port:portString)
  }
  else {
      endpoint = NWBonjourServiceEndpoint(name: serverAddress, type:Tunnel.serviceType, domain:Tunnel.serviceDomain)
  }
  connection = provider.createTCPConnection(to: endpoint, enableTLS:false, tlsParameters:nil, delegate:nil)
  connection!.addObserver(self, forKeyPath: "state", options: .initial, context: &ClientTunnel.observerContext)
  return nil
}


With in this startTunnel function endpoint comes as example.com._tunnelserver._tcp.local and the connection state stays as connecting. As I read from NEAppProxyProvider documentation handleFlow function triggered by system whenever an app which matches the current App Proxy configuration’s app rules opens a new network connection. For the app rules do I need to set something on .mobileconfig file? If so could you give me some examples? Or there must be a open connection?

UPDATED PART:

I updated startProxy and stopProxy functions as below but still no luck on triggering handleFlow function.

    override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) {       
        completionHandler(nil)
    }
   
    override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {    
        completionHandler()
    }

Thanks,

Krypton

For the app rules do I need to set something on

.mobileconfig
file?

That depends on your deployment environment:

  • For development on iOS, you bind apps to your VPN configuration’s VPN UUID using the

    NETestAppMapping
    key in your
    Info.plist
    , as discussed in the Testing Per-App VPN section of the NETunnelProviderManager class reference
  • If you’re deploying to iOS, you can only target apps installed via MDM, and that installation process lets you set the VPN UUID.

  • On macOS, both for development and deployment, you bind apps to your VPN configuration’s VPN UUID using the App-to-Per-App VPN mapping payload (

    com.apple.vpn.managed.appmapping
    ) in a configuration profile.

I’ve run across a couple of gotchas here:

  • NETestAppMapping
    is incompatible with the
    SafariDomains
    property in the configuration profile. Test with one or the other.
  • The app proxy provider can’t catch traffic from the app hosting it. Use a completely independent test app for testing.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for you answer Quinn. I managed to trigger handleFlow by system. Now as a next step I would like to forward NEAppProxyTCPFlow to the destination url. Could you please tell me possible solutions?

Thanks,

Krypton

Now as a next step I would like to forward NEAppProxyTCPFlow to the destination url.

Ah, um, that’s hard to answer because it depends on the specifics of the VPN transport you’re trying to implement. In general you can connect to your VPN server via any TCP or UDP API, although the most obvious APIs to use in this case are

NWTCPConnection
and
NWUDPSession
. How you then tunnel requests over that transport is really up to you.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

My case is a bit different, there is not any VPN server. I want to read the flows in client side and then forward them to the destination url. In my extension which has a AppProxyProvider, I can catch requests over my application (hostnames and ports of flows). How can I forward the flows to their destinations?