18

I'm trying to change the DNS servers on my Mac (10.10.4) using PyObjC (3.0.4).

Everything seems to work: I get an authentication dialog prompting that my program is trying to change network settings, and the commit/apply commands return True, which would indicate they were successful.

However, the system settings aren't actually changed: they remain the same as before. Any idea why they don't "stick"?

The code (standalone, should work if you have a recent version of PyObjC installed):

#!/usr/bin/env python

import  objc
from    SystemConfiguration import *

# Open dynamic store and get primary interface
store = SCDynamicStoreCreate(None, 'MyApp', None, None)
primaryif = SCDynamicStoreCopyValue(store, 'State:/Network/Global/IPv4')['PrimaryInterface']
if primaryif:
    print "Using %s as primary interface" % primaryif
else:
    raise "Can't find primary interface"

# Load SecurityInterface framework to provide SFAuthorization
objc.initFrameworkWrapper(
    frameworkName       = "SecurityInterface",
    frameworkIdentifier = "com.apple.securityinterface",
    frameworkPath       = objc.pathForFramework("/System/Library/Frameworks/SecurityInterface.framework"),
    globals             = globals()
)

# Access system preferences
preferences = SCPreferencesCreateWithAuthorization(None, 'MyApp', None, SFAuthorization.authorization().authorizationRef())

# Lock preferences
SCPreferencesLock(preferences, True)

# Get list of network services
networkSet = SCNetworkSetCopyCurrent(preferences)
networkSetServices = SCNetworkSetCopyServices(networkSet)

# Find the network service that belongs to the primary interface
for networkService in networkSetServices:
    interface = SCNetworkServiceGetInterface(networkService)
    if primaryif != SCNetworkInterfaceGetBSDName(interface):
        continue

    # Load currently configured DNS servers
    networkProtocol = SCNetworkServiceCopyProtocol(networkService, kSCNetworkProtocolTypeDNS)
    DNSDict = SCNetworkProtocolGetConfiguration(networkProtocol) or {}

    # Set new DNS servers
    DNSDict[kSCPropNetDNSServerAddresses] = [ '192.168.23.12', '8.8.4.4' ]
    SCNetworkProtocolSetConfiguration(networkService, DNSDict)

    # Unlock, commit and apply preferences
    print "UL", SCPreferencesUnlock(preferences)
    print "CO", SCPreferencesCommitChanges(preferences)
    print "AP", SCPreferencesApplyChanges(preferences)

EDIT: most of the above code is based on this page, which also suggests "touching" the dynamic store to make the settings stick (the code to do this is commented out right at the end). However, it doesn't seem to do anything.

EDIT #2: by disassembling /usr/sbin/networksetup I'm getting the idea that I need a set of specific rights (system.services.systemconfiguration.network) before any changes are accepted.

robertklep
  • 198,204
  • 35
  • 394
  • 381
  • 1
    Have you tried running the script using `sudo`? – l'L'l Aug 23 '15 at 14:16
  • @l'L'l yeah, that doesn't change anything (literally) :-( – robertklep Aug 23 '15 at 14:54
  • I just tried the script and I get an error after it asks for authorization: `DNSDict[kSCPropNetDNSServerAddresses] = [ '192.168.23.12', '8.8.4.4' ] TypeError: 'NoneType' object does not support item assignment` – l'L'l Aug 23 '15 at 15:39
  • @l'L'l it may be that you need to change the interface (`en0` in my example). The example code is not performing error checking at every step. – robertklep Aug 23 '15 at 15:59
  • My `PyObjC` was ancient (which I think was the problem); I'll let you know what transpires after I update :) – l'L'l Aug 23 '15 at 16:19
  • 1
    @l'L'l I updated the example code so it tries to determine the primary interface. I also realized that `DNSDict` may be empty when you rely on DHCP to provide DNS server (I have them set manually) so I also fixed that. As for updating PyObjC, I would recommend using [`virtualenv`](https://virtualenv.pypa.io/en/latest/). My experience with updating the system-provided PyObjC are not very good. – robertklep Aug 23 '15 at 16:23
  • I discovered a few things about what is (not) taking place. It looks like three `.plist` files in `System Configuration` never get updated (or even accessed) with the networksetup changes. I'm not exactly sure why that's happening (maybe Apple has changed something?), however, I did some digging around github and found an [example](https://github.com/damln/dns.app) that uses Python's `os` cmd to change the setup and it did indeed work. I realize it's a slightly different approach, although maybe it can be of some use perhaps. – l'L'l Aug 23 '15 at 17:33
  • The three files that I mentioned are: `com.apple.networkextension.plist`, `preferences.plist`, and `resolv.conf`. If you run the `networksetup` utility in terminal and set the dns there the values get set in those preferences. – l'L'l Aug 23 '15 at 17:40
  • @l'L'l using `networksetup` is the approach I'm actually using now (see [this project](https://github.com/robertklep/ToggleProxy/blob/master/ToggleProxy.py#L153-L157)). However, it's not ideal and I prefer setting the proxies (similar to DNS) programmatically. I'm almost certain that my call to `SCPreferencesCreateWithAuthorization()` is incomplete: it has to request particular rights, however, I also found that it may not actually be possible to request those using PyObjC (yet). – robertklep Aug 23 '15 at 18:06
  • Yeah I figured that's what your thinking was (using SystemConfiguration Framework directly with PyObjC) — it's much cleaner than splicing `os` commands into everything that needs to be read/written. Your code looks like it should work from what I could tell. The only thing I wasn't sure about was how you were using lock/unlock - in other examples I have seen they appear to use lock set on False, and immediately after that unlock is called. When I have a chance I'll take a better look at the script and see if there's anything that might come to mind. – l'L'l Aug 23 '15 at 18:15
  • @l'L'l thanks :-) I'm pretty sure that the issue I'm having is not related to the (un)locking. Some more background [here](http://sourceforge.net/p/pyobjc/mailman/message/34386470/) (sorry for the SF link, that's where the mailinglist is hosted). – robertklep Aug 23 '15 at 18:18
  • No worries at all :) Also, Have you seen any working examples of the System Configuration Framework used via PyObjC where the preferences do stick? Just curious. I noticed [these test scripts](https://github.com/aosm/pyobjc/tree/87d2acf646981d3f766c3b46501284ad0787e4f3/2.5/pyobjc/pyobjc-framework-SystemConfiguration/PyObjCTest), so I'm wondering if they are functioning correctly. – l'L'l Aug 23 '15 at 18:30
  • @l'L'l I've based my code on examples that have supposedly worked at some point, but there have been changes in OS X that are now preventing it to work without getting the right permissions (for example, it used to be that you didn't have to authenticate using `networksetup` either, but that changed with Mavericks). I should probably make sure though that the Objective-C version of my code _is_ working as expected, I'll spend some time on that. – robertklep Aug 23 '15 at 20:23
  • I think you're onto to something there; it's possible some those functions might have been patched not to work as they once did also because of a serious [exploit](https://packetstormsecurity.com/files/131368/osxrootpipe-escalate.txt) that used `SFAuthorization`. – l'L'l Aug 23 '15 at 20:49
  • why not using dnspython? – gerosalesc Aug 28 '15 at 12:55
  • @GermanRosales the primary goal is to change system network settings (DNS, but eventually also proxies, etc) :-) – robertklep Aug 28 '15 at 13:00
  • @robertklep: There may be authorization `flags` that need to be set; If you adjust your code to accompany them it might be worth a try in that regard. – l'L'l Aug 28 '15 at 17:43
  • @l'L'l besides the flags I need to set the rights that are required as well, and that's where things are falling apart at the moment. According to the developer of PyObjC, that's just not possible to do at the moment :-( – robertklep Aug 28 '15 at 18:11
  • @robertklep: The flags are important because this is where the rights are actually determined. Another thing you might do is create a helper tool to obtain authorization and apply the settings. – l'L'l Aug 28 '15 at 18:33
  • @l'L'l but settings the flags isn't a problem, that's just an integer. I could create a helper tool, but in that case I might as well use `networksetup` like I'm doing now :-) – robertklep Aug 28 '15 at 18:35
  • @robertklep: I'm curious about the PyObjC developers comment; did they elaborate on why it's not possible at the moment? just curious. – l'L'l Aug 28 '15 at 18:37
  • @l'L'l see this thread on the PyObjC mailing list: http://sourceforge.net/p/pyobjc/mailman/message/34386470/ ("Ronald" is the PyObjC maintainer) – robertklep Aug 28 '15 at 18:41
  • Thanks for the link; It appears you'll be in a holding pattern until they support using a struct with an embedded pointer to a buffer apparently. – l'L'l Aug 28 '15 at 18:54
  • @l'L'l yeah, I'm glad this is just a hobby project ;-) – robertklep Aug 28 '15 at 18:55

1 Answers1

2

Looks like there are issues with PyObjC that cause this to not work, however you may be able to find a way around it by using a different solution. If I were you, and my situation allowed it, I would just call the system command line tools to set the DNS servers.

According to OSXDaily, you can do this with:

networksetup -setdnsservers (Network Service) (DNS IP)

If you have cross platform requirements this is obviously less than desirable.

jaypb
  • 1,544
  • 10
  • 23
  • Thanks for the suggestion :D I already used `networksetup` (see [this comment](http://stackoverflow.com/questions/32073573/changing-dns-settings-using-pyobjc/40538034#comment52226266_32073573)) and was looking for a pure PyObjC replacement. I never managed to get it working like that, but having to use `networksetup` is acceptable. – robertklep Nov 11 '16 at 07:25