summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrew Dunham <andrew@du.nham.ca>2023-03-02 16:07:41 -0500
committerAndrew Dunham <andrew@du.nham.ca>2023-03-02 16:22:11 -0500
commit9058121b0cb3ece39d4213d8424f3ad496acfe52 (patch)
treebe79a4c2d83899e2c1d3fbc4256656c9917d9f3d
parente1530cdfccee25973cdd03c08917e143fe42282f (diff)
downloadtailscale-andrew/util-dnsconfig.tar.xz
tailscale-andrew/util-dnsconfig.zip
util/dnsconfig: add new package to parse macOS DNS configurationandrew/util-dnsconfig
Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I271b401af1b5e626b3afe291c6f7f15b319c601d
-rw-r--r--util/dnsconfig/dnsconfig.go217
-rw-r--r--util/dnsconfig/dnsconfig_test.go30
-rw-r--r--util/dnsconfig/gen/gen.c114
3 files changed, 361 insertions, 0 deletions
diff --git a/util/dnsconfig/dnsconfig.go b/util/dnsconfig/dnsconfig.go
new file mode 100644
index 000000000..5873e7eb2
--- /dev/null
+++ b/util/dnsconfig/dnsconfig.go
@@ -0,0 +1,217 @@
+package dnsconfig
+
+/*
+#cgo LDFLAGS: -ldl
+
+#include <dlfcn.h>
+#include <stdlib.h>
+
+void* call_pointer(void* addr) {
+ void* (*fn)(void) = addr;
+ return fn();
+}
+
+void call_arg(void* addr, void* arg) {
+ void (*fn)(void*) = addr;
+ fn(arg);
+}
+*/
+import "C"
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "net/netip"
+ "sync"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ fptrOnce sync.Once
+ dnsConfigurationCopyPtr unsafe.Pointer
+ dnsConfigurationFreePtr unsafe.Pointer
+)
+
+func initPointers() {
+ fptrOnce.Do(func() {
+ sym := C.CString("dns_configuration_copy")
+ defer C.free(unsafe.Pointer(sym))
+ dnsConfigurationCopyPtr = C.dlsym(C.RTLD_DEFAULT, sym)
+
+ sym = C.CString("dns_configuration_free")
+ defer C.free(unsafe.Pointer(sym))
+ dnsConfigurationFreePtr = C.dlsym(C.RTLD_DEFAULT, sym)
+ })
+}
+
+var errSymbolNotFound = errors.New("symbol not found")
+
+func dnsConfigurationCopy() (*dnsConfig, error) {
+ initPointers()
+ if dnsConfigurationCopyPtr == nil {
+ return nil, errSymbolNotFound
+ }
+
+ // Call through cgo so that the Go runtime switches to a C stack.
+ ptr := C.call_pointer(dnsConfigurationCopyPtr)
+ return (*dnsConfig)(ptr), nil
+}
+
+func dnsConfigurationFree(p *dnsConfig) error {
+ initPointers()
+ if dnsConfigurationFreePtr == nil {
+ return errSymbolNotFound
+ }
+ // Call through cgo so that the Go runtime switches to a C stack.
+ C.call_arg(dnsConfigurationFreePtr, unsafe.Pointer(p))
+ return nil
+}
+
+// DNSConfig contains DNS configuration information as returned by macOS. It is
+// the Go version of the private dns_config_t type.
+type DNSConfig struct {
+ Resolvers []*DNSResolver
+ ScopedResolvers []*DNSResolver
+ Generation uint64
+ ServiceSpecificResolvers []*DNSResolver
+ Version uint32
+}
+
+// DNSResolver contains DNS resolver-specific information as returned by macOS.
+// It is the Go version of the private dns_resolver_t type.
+type DNSResolver struct {
+ Domain string
+ Nameservers []netip.AddrPort
+ Port uint16
+ Search []string
+ Options string
+ Timeout uint32
+ SearchOrder uint32
+ IfIndex uint32
+ Flags uint32
+ ReachFlags uint32
+ ServiceIdentifier uint32
+ CID string
+ IfName string
+
+ // TODO: SortAddr []any?
+}
+
+// Get returns this system's DNS configuration, or an error.
+func Get() (*DNSConfig, error) {
+ config, err := dnsConfigurationCopy()
+ if err != nil {
+ return nil, err
+ }
+ defer dnsConfigurationFree(config)
+
+ // Verify that the version is what we expect. On newer versions of
+ // macOS, we could check this and only load fields that are present,
+ // instead of failing outright.
+ version := binary.LittleEndian.Uint32(config.data[44 : 44+4])
+ if version != 20170629 {
+ return nil, fmt.Errorf("version mismatch: %d != 20170629", version)
+ }
+
+ ret := &DNSConfig{
+ Generation: binary.LittleEndian.Uint64(config.data[24 : 24+8]),
+ Version: version,
+ }
+
+ // Populate resolvers
+ for _, resolver := range getResolvers(config.data[:], 0, 4) {
+ ret.Resolvers = append(ret.Resolvers, parseResolver(resolver))
+ }
+ for _, resolver := range getResolvers(config.data[:], 12, 16) {
+ ret.ScopedResolvers = append(ret.ScopedResolvers, parseResolver(resolver))
+ }
+ for _, resolver := range getResolvers(config.data[:], 32, 36) {
+ ret.ServiceSpecificResolvers = append(ret.ServiceSpecificResolvers, parseResolver(resolver))
+ }
+
+ return ret, nil
+}
+
+func getResolvers(data []byte, numOff, arrOff int) []*dnsResolver {
+ n := int(binary.LittleEndian.Uint32(data[numOff : numOff+4]))
+ arr := unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(data[arrOff : arrOff+8])))
+ return unsafe.Slice((**dnsResolver)(arr), n)
+}
+
+func parseResolver(r *dnsResolver) *DNSResolver {
+ ret := &DNSResolver{
+ Domain: r.readCharPtr(0),
+ Port: binary.LittleEndian.Uint16(r.data[20 : 20+2]),
+ Options: r.readCharPtr(48),
+ Timeout: r.readUint32(56),
+ SearchOrder: r.readUint32(60),
+ IfIndex: r.readUint32(64),
+ Flags: r.readUint32(68),
+ ReachFlags: r.readUint32(72),
+ ServiceIdentifier: r.readUint32(76),
+ CID: r.readCharPtr(80),
+ IfName: r.readCharPtr(88),
+ }
+
+ // The actual nameservers for this DNS entry.
+ nNameservers := int(binary.LittleEndian.Uint32(r.data[8 : 8+4]))
+ arr := unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(r.data[12 : 12+8])))
+ for _, sockaddr := range unsafe.Slice((**syscall.RawSockaddr)(arr), nNameservers) {
+ switch sockaddr.Family {
+ case syscall.AF_INET:
+ sa := (*syscall.RawSockaddrInet4)(unsafe.Pointer(sockaddr))
+ ret.Nameservers = append(ret.Nameservers, netip.AddrPortFrom(
+ netip.AddrFrom4(sa.Addr),
+ sa.Port,
+ ))
+
+ case syscall.AF_INET6:
+ sa := (*syscall.RawSockaddrInet6)(unsafe.Pointer(sockaddr))
+ ret.Nameservers = append(ret.Nameservers, netip.AddrPortFrom(
+ netip.AddrFrom16(sa.Addr),
+ sa.Port,
+ ))
+
+ default:
+ // Skip unknown address families
+ // TODO: log?
+ }
+ }
+
+ // Search domains
+ nSearch := int(binary.LittleEndian.Uint32(r.data[24 : 24+4]))
+ arr = unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(r.data[28 : 28+8])))
+ for _, ss := range unsafe.Slice((**C.char)(arr), nSearch) {
+ ret.Search = append(ret.Search, C.GoString(ss))
+ }
+
+ return ret
+}
+
+// dnsConfig is the type returned from the dns_configuration_copy function. The
+// C header sets #pragma pack(4), which isn't easily represented in Go; we
+// instead use binary.Read to get fields from this structure.
+type dnsConfig struct {
+ data [48]byte
+}
+
+// dnsResolver is the dns_resolver_t type; as above, since we can't represent
+// it in Go, we read fields from the structure manually.
+type dnsResolver struct {
+ data [96]byte
+}
+
+func (d *dnsResolver) readCharPtr(off int) string {
+ ptr := unsafe.Pointer(uintptr(binary.LittleEndian.Uint64(d.data[off : off+8])))
+ return C.GoString((*C.char)(ptr))
+}
+
+func (d *dnsResolver) readInt32(off int) int32 {
+ return int32(binary.LittleEndian.Uint32(d.data[off : off+4]))
+}
+
+func (d *dnsResolver) readUint32(off int) uint32 {
+ return binary.LittleEndian.Uint32(d.data[off : off+4])
+}
diff --git a/util/dnsconfig/dnsconfig_test.go b/util/dnsconfig/dnsconfig_test.go
new file mode 100644
index 000000000..2b9c99b22
--- /dev/null
+++ b/util/dnsconfig/dnsconfig_test.go
@@ -0,0 +1,30 @@
+package dnsconfig
+
+import "testing"
+
+func TestGet(t *testing.T) {
+ config, err := Get()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(config.Resolvers) < 1 {
+ t.Fatal("wanted at least one resolver")
+ }
+
+ // Sensibility check: do we have at least one nameserver?
+ var nameservers int
+ for _, resolver := range config.Resolvers {
+ nameservers += len(resolver.Nameservers)
+ }
+ for _, resolver := range config.ScopedResolvers {
+ nameservers += len(resolver.Nameservers)
+ }
+ for _, resolver := range config.ServiceSpecificResolvers {
+ nameservers += len(resolver.Nameservers)
+ }
+
+ if nameservers == 0 {
+ t.Fatal("wanted at least one nameserver, got 0")
+ }
+}
diff --git a/util/dnsconfig/gen/gen.c b/util/dnsconfig/gen/gen.c
new file mode 100644
index 000000000..095ba8b83
--- /dev/null
+++ b/util/dnsconfig/gen/gen.c
@@ -0,0 +1,114 @@
+#include <sys/cdefs.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+/**************************************************
+ * BEGIN TYPES FROM APPLE HEADER
+ **************************************************/
+
+#define DNS_PTR(type, name) \
+ union { \
+ type name; \
+ uint64_t _ ## name ## _p; \
+ }
+
+#define DNS_VAR(type, name) \
+ type name
+
+
+#pragma pack(4)
+typedef struct {
+ struct in_addr address;
+ struct in_addr mask;
+} dns_sortaddr_t;
+#pragma pack()
+
+
+#pragma pack(4)
+typedef struct {
+ DNS_PTR(char *, domain); /* domain */
+ DNS_VAR(int32_t, n_nameserver); /* # nameserver */
+ DNS_PTR(struct sockaddr **, nameserver);
+ DNS_VAR(uint16_t, port); /* port (in host byte order) */
+ DNS_VAR(int32_t, n_search); /* # search */
+ DNS_PTR(char **, search);
+ DNS_VAR(int32_t, n_sortaddr); /* # sortaddr */
+ DNS_PTR(dns_sortaddr_t **, sortaddr);
+ DNS_PTR(char *, options); /* options */
+ DNS_VAR(uint32_t, timeout); /* timeout */
+ DNS_VAR(uint32_t, search_order); /* search_order */
+ DNS_VAR(uint32_t, if_index);
+ DNS_VAR(uint32_t, flags);
+ DNS_VAR(uint32_t, reach_flags); /* SCNetworkReachabilityFlags */
+ DNS_VAR(uint32_t, service_identifier);
+ DNS_PTR(char *, cid); /* configuration identifer */
+ DNS_PTR(char *, if_name); /* if_index interface name */
+} dns_resolver_t;
+#pragma pack()
+
+#pragma pack(4)
+typedef struct {
+ DNS_VAR(int32_t, n_resolver); /* resolver configurations */
+ DNS_PTR(dns_resolver_t **, resolver);
+ DNS_VAR(int32_t, n_scoped_resolver); /* "scoped" resolver configurations */
+ DNS_PTR(dns_resolver_t **, scoped_resolver);
+ DNS_VAR(uint64_t, generation);
+ DNS_VAR(int32_t, n_service_specific_resolver);
+ DNS_PTR(dns_resolver_t **, service_specific_resolver);
+ DNS_VAR(uint32_t, version);
+} dns_config_t;
+#pragma pack()
+
+/**************************************************
+ * END TYPES FROM APPLE HEADER
+ **************************************************/
+
+#define field_info(type, field) \
+ printf("%-15s\t%-30s\toffset=%lu\tsizeof=%lu\n", \
+ #type, \
+ #field, \
+ offsetof(type, field) , \
+ sizeof ((type *)0)->field \
+ )
+
+int main(void) {
+ printf("sizeof(dns_config_t)=%lu\n", sizeof(dns_config_t));
+ field_info(dns_config_t, n_resolver);
+ field_info(dns_config_t, resolver);
+ field_info(dns_config_t, n_scoped_resolver);
+ field_info(dns_config_t, scoped_resolver);
+ field_info(dns_config_t, generation);
+ field_info(dns_config_t, n_service_specific_resolver);
+ field_info(dns_config_t, service_specific_resolver);
+ field_info(dns_config_t, version);
+
+ printf("\n");
+
+ printf("sizeof(dns_resolver_t)=%lu\n", sizeof(dns_resolver_t));
+ field_info(dns_resolver_t, domain);
+ field_info(dns_resolver_t, n_nameserver);
+ field_info(dns_resolver_t, nameserver);
+ field_info(dns_resolver_t, port);
+ field_info(dns_resolver_t, n_search);
+ field_info(dns_resolver_t, search);
+ field_info(dns_resolver_t, n_sortaddr);
+ field_info(dns_resolver_t, sortaddr);
+ field_info(dns_resolver_t, options);
+ field_info(dns_resolver_t, timeout);
+ field_info(dns_resolver_t, search_order);
+ field_info(dns_resolver_t, if_index);
+ field_info(dns_resolver_t, flags);
+ field_info(dns_resolver_t, reach_flags);
+ field_info(dns_resolver_t, service_identifier);
+ field_info(dns_resolver_t, cid);
+ field_info(dns_resolver_t, if_name);
+
+ printf("\n");
+ field_info(struct sockaddr, sa_len);
+ field_info(struct sockaddr, sa_family);
+ field_info(struct sockaddr, sa_data);
+}