macOS DNS resolving change in Go 1.20
Nov 08, 2022
Summary
Starting in Go 1.201, DNS lookups when running on macOS will be done via the system instead of via Go’s built-in resolver. That’s even when cgo is not available, such as when cross-compiling from Linux.
That should reduce surprises for people using tools cross-compiled for macOS from Linux, such as CLIs like terraform and kubectl. It should also make it easier for developers of those tools as building on macOS may no longer be necessary.
The problem
When a Go program does something like:
resp, err := http.Get("https://www.google.com")
To make the HTTP request, the program needs to find IP addresses for www.google.com
.
For some time, there have been two ways it can do this:
- Via the system, similar to how a natively compiled C program would do it
- Via Go’s built-in resolver
Typically the built-in resolver is used when cgo is not available.
This is often the case when programs are cross-compiled, such as building with GOOS=darwin
on a Linux system.
For Linux the two methods are mostly interchangeable. The built-in resolver tries to mimic glibc’s behavior.
For macOS, though, the two can behave quite differently.
For example, macOS supports extra configuration under the /etc/resolver
directory.
This has no equivalent in glibc so the built-in resolver does not support it.
Further, macOS has other ways to configure DNS, such as when connected to a VPN or using Tailscale MagicDNS.
This extra configuration can be inspected somewhat via the scutil --dns
command but not as easily as a standard file like /etc/resolv.conf
.
These differences have led to a number of Go issues and discussions over the years, namely:
- Issue 12524: net: Support the /etc/resolver DNS resolution configuration hierarchy on OS X when cgo is disabled
- Issue 16345: net: revisit unconditional use of cgo lookups for darwin (opened by me!)
- Issue 22902: net: LookupHost shows different results between GODEBUG=netdns=cgo and go
These manifest as surprising behavior for users, such as these issues in projects.
The change
In CL 446178, Go was changed to use the system directly when resolving. This is done even when cgo is not available by making the necessary syscalls directly.
A similar technique is already used by the Go runtime and to do some certificate verification since Go 1.18.
This addresses issue 12524 above since always using the system to resolve means /etc/resolver
will be considered.
It means all those other ways of configuring DNS are now considered, too, even when cgo is not available.
Seeing it in action
This little program looks up a host on my tailnet.
Using just the name l1
relies on the system being used to resolve since the configuration to make it work is outside /etc/resolv.conf
.
package main
import (
"fmt"
"net"
)
func main() {
// l1 is a host on my tailnet
addrs, err := net.LookupHost("l1")
if err != nil {
panic(err)
}
fmt.Println(addrs)
}
First, I’ll build it for macOS on a Linux system using Go 1.19:
GOOS=darwin GOARCH=arm64 go build
And then run it on my macOS system:
> ./example
panic: lookup l1 on 192.168.1.1:53: no such host
This is because the extra macOS resolver configuration is not being considered.
If I build it again using Go built from tip, including CL 4461782, then run that:
> ./example
[100.123.252.39]
It works!
You can try it yourself now with gotip or wait for the upcoming Go 1.20 betas and release candidates.
Conclusion
I hope this resolves some long-standing confusion around macOS and DNS.
Thanks to the Go team for the change and to everyone who has kept up with these related issues over the years.