Skip to content

macOS: read DNS configuration from SystemConfiguration to support split DNS (parity with Windows registry support) #1267

@timansky

Description

@timansky

Summary

On macOS, dnspython reads DNS configuration exclusively from /etc/resolv.conf, which contains only the primary resolver. Per-domain resolvers configured via Apple's SystemConfiguration framework — the mechanism used by every VPN client on macOS to implement split DNS — are invisible to dnspython. This causes resolution failures for any internal hostname that relies on split DNS while a VPN is active.

Since dnspython already has platform-specific DNS configuration reading on Windows (BaseResolver.read_registry()dns.win32util.get_dns_info()), adding an analogous path for Darwin would bring macOS to parity and fix a large class of real-world failures downstream.

Context

On macOS, the actual system resolver state is not /etc/resolv.conf — it's the SystemConfiguration dynamic store. You can inspect it with scutil --dns, which typically shows multiple resolvers:

resolver #1
  nameserver[0] : 1.1.1.1
  nameserver[1] : 8.8.8.8

resolver #2
  domain   : corp.example.com
  nameserver[0] : 10.0.0.53
  flags    : Request A records, Request AAAA records
  reach    : 0x00000002 (Reachable)

resolver #3
  domain   : internal.example.com
  nameserver[0] : 10.0.1.53

Resolvers #2 and #3 are split DNS entries — queries for *.corp.example.com must go to 10.0.0.53, not 1.1.1.1. /etc/resolv.conf contains only resolver #1. Applications using getaddrinfo(3) (through libSystem) see all three. Applications reading /etc/resolv.conf directly — including dnspython — see only the first.

This is a well-known, long-standing ecosystem issue affecting every tool that ships its own DNS stub resolver on macOS. Representative issues:

The Go ecosystem resolved this by switching to libSystem-backed resolution on Darwin when cgo is available. dnspython has no equivalent path today.

Why /etc/resolv.conf is not sufficient

Some may ask: "doesn't macOS auto-populate /etc/resolv.conf?" Yes — but only with the primary resolver (resolver #1 in scutil --dns). Per-domain resolvers created by /etc/resolver/<domain> files and by VPN clients pushing DNS configuration via SCDynamicStoreSetValue never appear in /etc/resolv.conf. This is by design: /etc/resolv.conf has no way to express per-domain routing. Reading the file gives a strictly incomplete picture, and on macOS there is no way to bridge that gap without reading SystemConfiguration.

This is analogous to the Windows situation, where the registry (not a text file) is the source of truth, and dnspython correctly uses dns.win32util.get_dns_info() via the registry rather than trying to synthesize a POSIX-style config.

Proposed approach

Introduce dns/darwinutil.py analogous to dns/win32util.py, with a get_dns_info() that returns resolver entries grouped by domain suffix. Two reasonable implementation paths:

  1. Parse scutil --dns via subprocess — no new dependencies, portable across macOS versions, but fragile against Apple changing the output format.
  2. Use SystemConfiguration via ctypes — call SCDynamicStoreCopyValue(store, "State:/Network/Global/DNS") and related keys from libSystem.dylib. Requires no third-party dependency (ctypes is stdlib), but more code.

In BaseResolver, add a read_darwin() method called from the platform dispatch alongside read_registry(). Populate self.nameservers with the primary resolver, and store per-domain resolvers on a new attribute (or extend self.search) that resolve() consults first when the query name matches a domain suffix.

A user-visible API: extend Resolver.resolve() to route per-domain using this new state, with an opt-out flag for users who prefer the current behavior.

Scope of the request

  • Read-only: dnspython would only consume SystemConfiguration state, never write to it.
  • No new runtime dependencies: both implementation paths use stdlib.
  • Behavior gated behind sys.platform == "darwin", completely opt-out-able.
  • Windows and Linux paths unchanged.

Related dnspython issues

I'd be happy to prototype the scutil-parsing path as a starting point if the direction is acceptable. Thoughts?

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions