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:
- Parse
scutil --dns via subprocess — no new dependencies, portable across macOS versions, but fragile against Apple changing the output format.
- 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?
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 withscutil --dns, which typically shows multiple resolvers:Resolvers #2 and #3 are split DNS entries — queries for
*.corp.example.commust go to10.0.0.53, not1.1.1.1./etc/resolv.confcontains only resolver #1. Applications usinggetaddrinfo(3)(through libSystem) see all three. Applications reading/etc/resolv.confdirectly — 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:
/etc/resolveron macOS (same root cause, closed after Go started using libSystem via cgo by default on Darwin)getaddrinfobehavior in pure Go resolverThe 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.confis not sufficientSome may ask: "doesn't macOS auto-populate
/etc/resolv.conf?" Yes — but only with the primary resolver (resolver #1 inscutil --dns). Per-domain resolvers created by/etc/resolver/<domain>files and by VPN clients pushing DNS configuration viaSCDynamicStoreSetValuenever appear in/etc/resolv.conf. This is by design:/etc/resolv.confhas 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.pyanalogous todns/win32util.py, with aget_dns_info()that returns resolver entries grouped by domain suffix. Two reasonable implementation paths:scutil --dnsviasubprocess— no new dependencies, portable across macOS versions, but fragile against Apple changing the output format.SystemConfigurationviactypes— callSCDynamicStoreCopyValue(store, "State:/Network/Global/DNS")and related keys fromlibSystem.dylib. Requires no third-party dependency (ctypesis stdlib), but more code.In
BaseResolver, add aread_darwin()method called from the platform dispatch alongsideread_registry(). Populateself.nameserverswith the primary resolver, and store per-domain resolvers on a new attribute (or extendself.search) thatresolve()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
sys.platform == "darwin", completely opt-out-able.Related dnspython issues
/etc/resolv.confbeing empty on macOS Tahoe. Different problem: there the issue was that even the primary resolver was missing; this request is about per-domain resolvers that/etc/resolv.confnever contains by design.I'd be happy to prototype the
scutil-parsing path as a starting point if the direction is acceptable. Thoughts?