Accessing global proxy settings on iOS

Is there any way (private method or otherwise) to acccess the global proxy settings on an iOS device?


We are experiencing issues with the global proxy, set through an MDM profile, not being respected by iOS. This seems to have started with a late version of iOS 9 and is continuing with iOS 10. I would like to write an app to test for the presences of a proxy PAC file and then test a list of URLs to see how they are processed by the PAC file. I know using NSURL and the high-level APIs are supposed to automatically use the global proxy, but I am trying to write this app for testing purposes.

I have written an app using CFNetworkCopySystemProxySettings to retrieve the proxy settings, but it returns an empty dictionary on my test iOS device, even though the global proxy is set. Any help would be greatly appreciated.


Sincerely,
Doug Penny

Accepted Reply

The problem here is that

CFNetworkCopySystemProxySettings
hasn’t been audited for Swift happiness, and so it returns an
Unmanaged<CFDictionary>
(you can tell this by option clicking on the
proxySettings
identifier). You’ll need to convert the unmanaged reference to a managed reference like so:
import Foundation

if let myUrl = URL(string: "http://www.apple.com") { 
    if let proxySettingsUnmanaged = CFNetworkCopySystemProxySettings() {
        let proxySettings = proxySettingsUnmanaged.takeRetainedValue()
        let proxiesUnmanaged = CFNetworkCopyProxiesForURL(myUrl as CFURL, proxySettings)
        let proxies = proxiesUnmanaged.takeRetainedValue()
        print(proxies)
    } 
}

In this case I’m using

takeRetainedValue()
because both
CFNetworkCopySystemProxySettings
and
CFNetworkCopyProxiesForURL
return a +1 reference count per the CF retain/release rules (they have Copy or Create in the name).

In real code it’s better to just wrap routines like this with a function that follows Swift conventions. For example:

import Foundation

func QCFNetworkCopySystemProxySettings() -> CFDictionary? {
    guard let proxiesSettingsUnmanaged = CFNetworkCopySystemProxySettings() else {
        return nil
    }
    return proxiesSettingsUnmanaged.takeRetainedValue()
}

func QCFNetworkCopyProxiesForURL(_ url: URL, _ proxiesSettings: CFDictionary) -> [[String:AnyObject]] {
    let proxiesUnmanaged = CFNetworkCopyProxiesForURL(url as CFURL, proxiesSettings)
    let proxies = proxiesUnmanaged.takeRetainedValue()
    return proxies as! [[String:AnyObject]]
}

if let myUrl = URL(string: "http://www.apple.com") { 
    if let proxySettings = QCFNetworkCopySystemProxySettings() {
        let proxies = QCFNetworkCopyProxiesForURL(myUrl, proxySettings)
        print(proxies)
    } 
}

Share and Enjoy

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

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

Replies

Here is a bit more information after doing some additional testing. I'm doing a very basic function call to get the proxy settings on an iOS device:

let proxies = CFNetworkCopySystemProxySettings()
print("CFNetwork Proxies: \(proxies)")


If I enter the automatic proxy settings under WiFi in Settings.app these are the results:

CFNetwork Proxies: Optional(Swift.Unmanaged<__ObjC.CFDictionary>(_value: {
    ExceptionsList =     (
        "*.local",
        "169.254/16"
    );
    FTPPassive = 1;
    ProxyAutoConfigEnable = 1;
    ProxyAutoConfigURLString = "https://<proxy address removed>/proxy.pac";
    "__SCOPED__" =     {
        en0 =         {
            ExceptionsList =             (
                "*.local",
                "169.254/16"
            );
            FTPPassive = 1;
            ProxyAutoConfigEnable = 1;
            ProxyAutoConfigURLString = "https://<proxy address removed>/proxy.pac";
        };
    };
}))


If I push a global proxy configuration profile to the device via our MDM thse are the results:

CFNetwork Proxies: Optional(Swift.Unmanaged<__ObjC.CFDictionary>(_value: {
    ExceptionsList =     (
        "*.local",
        "169.254/16"
    );
    FTPPassive = 1;
    "__SCOPED__" =     {
        en0 =         {
            ExceptionsList =             (
                "*.local",
                "169.254/16"
            );
            FTPPassive = 1;
        };
    };
}))


Notice that the ProxyAutoConfig entries are missing. I can verify that the configuration profile is present on the device and can see the proxy settings in Settings.app under General > Device Management > Mobile Device Management.


So I guess the question is should CFNetworkCopySystemProxySettings return proxy settings applied through an MDM or only proxy settings entered for a specfic WiFi network?

Ah, proxies!, the gift that keeps on giving (-:

This seems to have started with a late version of iOS 9 and is continuing with iOS 10.

If you can confirm that there was a specific regression, you should definitely file a bug about that.

I would like to write an app to test for the presences of a proxy PAC file and then test a list of URLs to see how they are processed by the PAC file.

Have you tried calling

CFNetworkCopyProxiesForURL
? The proxy configuration can change by URL and, as such,
CFNetworkCopySystemProxySettings
is rarely useful. However, given your overall goạl it seems that
CFNetworkCopyProxiesForURL
is the right thing to call anyway. And hopefully it’ll work (-:

Share and Enjoy

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

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

I'll give

CFNetworkCopyProxiesForURL
a try and see what it is returning. Do I need to retreive the current proxy settings to pass to the function though? If so, wouldn't I just be using
CFNetworkCopySystemProxySettings
anyway?

Do I need to retreive the current proxy settings to pass to the function though? If so, wouldn't I just be using CFNetworkCopySystemProxySettings anyway?

Right. The point of this test is twofold:

  • It’s possible that

    CFNetworkCopyProxiesForURL
    will grab the global HTTP proxy via other means.
  • CFNetworkCopyProxiesForURL
    is the right way to test whether a particular URL will go through a proxy in general.

Share and Enjoy

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

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

First off, I admit that I am fairly new to Swift. I'm having trouble getting this to work as coded below:

if let myUrl = URL(string: "http://www.apple.com") {
  if let proxySettings = CFNetworkCopySystemProxySettings() {
       let proxies = CFNetworkCopyProxiesForURL(myUrl as CFURL, proxySettings as! CFDictionary)
       dump(proxies)
  }
}


I'm getting a EXC_BAD_INSTRUCTION error on line 3 when running in the simulator under iOS 10. This seems like fairly straight foroward code that should just work. I'm sure I'm missing something simple, but any help would be greatly appreciatated.


Thanks,

Doug

The problem here is that

CFNetworkCopySystemProxySettings
hasn’t been audited for Swift happiness, and so it returns an
Unmanaged<CFDictionary>
(you can tell this by option clicking on the
proxySettings
identifier). You’ll need to convert the unmanaged reference to a managed reference like so:
import Foundation

if let myUrl = URL(string: "http://www.apple.com") { 
    if let proxySettingsUnmanaged = CFNetworkCopySystemProxySettings() {
        let proxySettings = proxySettingsUnmanaged.takeRetainedValue()
        let proxiesUnmanaged = CFNetworkCopyProxiesForURL(myUrl as CFURL, proxySettings)
        let proxies = proxiesUnmanaged.takeRetainedValue()
        print(proxies)
    } 
}

In this case I’m using

takeRetainedValue()
because both
CFNetworkCopySystemProxySettings
and
CFNetworkCopyProxiesForURL
return a +1 reference count per the CF retain/release rules (they have Copy or Create in the name).

In real code it’s better to just wrap routines like this with a function that follows Swift conventions. For example:

import Foundation

func QCFNetworkCopySystemProxySettings() -> CFDictionary? {
    guard let proxiesSettingsUnmanaged = CFNetworkCopySystemProxySettings() else {
        return nil
    }
    return proxiesSettingsUnmanaged.takeRetainedValue()
}

func QCFNetworkCopyProxiesForURL(_ url: URL, _ proxiesSettings: CFDictionary) -> [[String:AnyObject]] {
    let proxiesUnmanaged = CFNetworkCopyProxiesForURL(url as CFURL, proxiesSettings)
    let proxies = proxiesUnmanaged.takeRetainedValue()
    return proxies as! [[String:AnyObject]]
}

if let myUrl = URL(string: "http://www.apple.com") { 
    if let proxySettings = QCFNetworkCopySystemProxySettings() {
        let proxies = QCFNetworkCopyProxiesForURL(myUrl, proxySettings)
        print(proxies)
    } 
}

Share and Enjoy

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

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

Thank you so much for patiently explaining this topic. I am able to now retrieve the proxy settings when they are set directly in Settings.app, but only get

kCFProxyTypeNone
when a global proxy is set via an MDM.


When providing a URL for a PAC file in the HTTP Proxy settings for a WiFi network, is it possible to get the returned value after the PAC file is parsed? Currently,

CFNetworkCopyProxiesForURL
only returns the URL for the PAC file, not the results of parsing the PAC file.


Again, thank you very much for your patience and assistance.


Sincerely,

Doug

When providing a URL for a PAC file in the HTTP Proxy settings for a WiFi network, is it possible to get the returned value after the PAC file is parsed?

I think you mean executed, not parsed, right? If so, you should take a look at

CFNetworkExecuteProxyAutoConfigurationScript
and
CFNetworkExecuteProxyAutoConfigurationURL
, which can run PAC scripts with a target URL and return you the result.

Calling those from Swift is ‘fun’. There’s a bunch of complexities here, including:

  • It’s async, so you have to deal with mapping a Swift object to a CF

    info
    pointer and back.
  • It’s run loop based, so you have target a specific thread (in my code I always target the main thread).

  • The API itself requires that you serialise requests.

The code pasted in below should get you started.

Share and Enjoy

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

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

class PACResolver {

    init(script: String) {
        self.script = script
    }

    let script: String

    enum Result {
        case error(error: CFError)
        case proxies(proxies: CFArray)
    }
    typealias Callback = (_ result: Result) -> Void

    private struct Request {
        let targetURL: URL
        let callback: Callback
    }
    private var requests: [Request] = []
    private var runLoopSource: CFRunLoopSource?

    func resolve(targetURL: URL, callback: @escaping Callback) {
        DispatchQueue.main.async {
            let wasEmpty = self.requests.isEmpty
            self.requests.append(Request(targetURL: targetURL, callback: callback))
            if wasEmpty {
                self.startNextRequest()
            }
        }
    }

    private func startNextRequest() {
        guard let request = self.requests.first else {
            return
        }

        var context = CFStreamClientContext()
        context.info = Unmanaged.passRetained(self).toOpaque()
        let rls = CFNetworkExecuteProxyAutoConfigurationScript(
            self.script as CFString,
            request.targetURL as CFURL,
            { (info, proxies, error) in
                let obj = Unmanaged<PACResolver>.fromOpaque(info).takeRetainedValue()
                if let error = error {
                    obj.resolveDidFinish(result: .error(error: error))
                } else {
                    obj.resolveDidFinish(result: .proxies(proxies: proxies))
                }
            },
            &context
        ).takeUnretainedValue()
        assert(self.runLoopSource == nil)
        self.runLoopSource = rls
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, .defaultMode)
    }

    private func resolveDidFinish(result: Result) {
        CFRunLoopSourceInvalidate(self.runLoopSource!)
        self.runLoopSource = nil
        let request = self.requests.removeFirst()

        request.callback(result)

        self.startNextRequest()
    }
}

This is a great help... this is really what I was trying to do from the beginning. However, I have been able to confirm what I think is a fairly serious regression in the global HTTP proxy settings. I'll open a bug report on this, but thought I would share here as well. Please let me know if you need the bug report ID#.


Scenario:

An iPad Air running iOS 9.3.4 and an iPad Air running iOS 10.1; both enrolled in JAMF Pro MDM with the same global HTTP proxy configuration profile installed


9.3.4 iPad returns:

kCFProxyTypeKey - kCFProxyTypeAutoConfigurationURL

kCFProxyAutoConfigurationURLKey - https://<proxy-pac-url>/proxy.pac


10.1 iPad returns:

kCFProxyTypeKey - kCFProxyTypeNone


I have had an opportunity to implement the PAC file execution yet, but am working on that now. I just got these results from CFNetworkCopyProxiesForURL

Sorry to hear that this is an real regression.

Please let me know if you need the bug report ID#.

Please do post the bug number; Future Quinn™ will appreciate that breadcrumb to follow.

Share and Enjoy

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

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

The bug number is 28986780.


I am really confused though. Even though the iPad running iOS 10.1 reports None for the proxy type, it is still pulling our proxy server IP address from somewhere. I have reset the network settings, but when off-campus, the iPad is being filtered through our proxy server. However, every request is being filtered through the proxy, not just the ones that don't have a match in the PAC file. Very strange.


Thanks for all of your help with the Quinn.


Sincerely,

Doug Penny

Where did you end up on this? We are having similar problems on our devices that have upgraded to iOS 10+. The GlobalProxy set from our MDM (MobileIron) policy on our supervised pads worked fine on everything up through 9.3.5 and then went poof when we jumped to iOS 10.x. We have an open case open but are still in the discovery phase.