From 220964fc02c625623b9c85ffb01c9a971f3a82f5 Mon Sep 17 00:00:00 2001
From: Sam <sam@samtherapy.net>
Date: Thu, 30 Jun 2022 22:37:08 +0000
Subject: [PATCH] Windows and Plan 9 DNS Config (#17)

Closes #14, #15

Co-authored-by: Sam Therapy <sam@samtherapy.net>
Reviewed-on: https://git.froth.zone/sam/awl/pulls/17
---
 .drone.yml         |  2 +-
 cli.go             | 10 ++++---
 cli_test.go        |  1 +
 conf/docs.go       |  6 ++++
 conf/notwin.go     | 25 +++++++++++++++++
 conf/plan9.go      | 40 ++++++++++++++++++++++++++
 conf/plan9_test.go | 45 +++++++++++++++++++++++++++++
 conf/win.go        | 70 ++++++++++++++++++++++++++++++++++++++++++++++
 go.mod             |  2 +-
 9 files changed, 195 insertions(+), 6 deletions(-)
 create mode 100644 conf/docs.go
 create mode 100644 conf/notwin.go
 create mode 100644 conf/plan9.go
 create mode 100644 conf/plan9_test.go
 create mode 100644 conf/win.go

diff --git a/.drone.yml b/.drone.yml
index 10c02d1..1ccb75b 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -2,7 +2,7 @@ kind: pipeline
 type: docker
 name: default
 steps:
-  - name: test
+  - name: Test
     image: golang
     commands:
       - go test ./...
\ No newline at end of file
diff --git a/cli.go b/cli.go
index 6b18497..948991f 100644
--- a/cli.go
+++ b/cli.go
@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: BSD-3-Clause
+
 package main
 
 import (
@@ -7,7 +8,9 @@ import (
 	"runtime"
 	"strings"
 
+	"git.froth.zone/sam/awl/conf"
 	"git.froth.zone/sam/awl/util"
+
 	"github.com/miekg/dns"
 	"github.com/urfave/cli/v2"
 	"golang.org/x/net/idna"
@@ -111,7 +114,7 @@ func prepareCLI() *cli.App {
 			},
 			&cli.BoolFlag{
 				Name:  "tc",
-				Usage: "set tc (TrunCated) flag (default: not set)",
+				Usage: "set TC (TrunCated) flag (default: not set)",
 			},
 			&cli.BoolFlag{
 				Name:  "z",
@@ -182,9 +185,8 @@ func parseArgs(args []string) (util.Answers, error) {
 		}
 	}
 	if resp.Answers.Server == "" {
-		resolv, err := dns.ClientConfigFromFile("/etc/resolv.conf")
-		if err != nil { // Query Google by default, needed for Windows since the DNS library doesn't support Windows
-			// TODO: Actually find where windows stuffs its dns resolvers
+		resolv, err := conf.GetDNSConfig()
+		if err != nil { // Query Google by default
 			resp.Answers.Server = "8.8.4.4"
 		} else {
 			resp.Answers.Server = resolv.Servers[rand.Intn(len(resolv.Servers))]
diff --git a/cli_test.go b/cli_test.go
index dce74a8..b496ce2 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: BSD-3-Clause
+
 package main
 
 import (
diff --git a/conf/docs.go b/conf/docs.go
new file mode 100644
index 0000000..c57cfb5
--- /dev/null
+++ b/conf/docs.go
@@ -0,0 +1,6 @@
+/*
+Helper functions for getting local nameservers
+
+Currently supported: Unix, Windows, Plan 9 (tested on 9front)
+*/
+package conf
diff --git a/conf/notwin.go b/conf/notwin.go
new file mode 100644
index 0000000..7f8aa7a
--- /dev/null
+++ b/conf/notwin.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build !windows
+// +build !windows
+
+package conf
+
+import (
+	"os"
+	"runtime"
+
+	"github.com/miekg/dns"
+)
+
+// Get the DNS configuration, either from /etc/resolv.conf or somewhere else
+func GetDNSConfig() (*dns.ClientConfig, error) {
+	if runtime.GOOS == "plan9" {
+		dat, err := os.ReadFile("/net/ndb")
+		if err != nil {
+			return nil, err
+		}
+		return getPlan9Config(string(dat))
+	} else {
+		return dns.ClientConfigFromFile("/etc/resolv.conf")
+	}
+}
diff --git a/conf/plan9.go b/conf/plan9.go
new file mode 100644
index 0000000..cb507fb
--- /dev/null
+++ b/conf/plan9.go
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+package conf
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/miekg/dns"
+)
+
+// Plan 9 stores its network data in /net/ndb, which seems to be formatted a specific way
+// Yoink it and use it.
+//
+// See ndb(7).
+func getPlan9Config(str string) (*dns.ClientConfig, error) {
+	str = strings.ReplaceAll(str, "\n", "")
+	spl := strings.FieldsFunc(str, splitChars)
+	var servers []string
+	for _, option := range spl {
+		if strings.HasPrefix(option, "dns=") {
+			servers = append(servers, strings.TrimPrefix(option, "dns="))
+		}
+	}
+	if len(servers) == 0 {
+		return nil, fmt.Errorf("plan9: no DNS servers found")
+	}
+
+	// TODO: read more about how customizable Plan 9 is
+	return &dns.ClientConfig{
+		Servers: servers,
+		Search:  []string{},
+		Port:    "53",
+	}, nil
+}
+
+// Split the string at either space or tabs
+func splitChars(r rune) bool {
+	return r == ' ' || r == '\t'
+}
diff --git a/conf/plan9_test.go b/conf/plan9_test.go
new file mode 100644
index 0000000..fa81fd2
--- /dev/null
+++ b/conf/plan9_test.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+package conf
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetPlan9Config(t *testing.T) {
+	ndbs := []struct {
+		in   string
+		want string
+	}{
+		{`ip=192.168.122.45 ipmask=255.255.255.0 ipgw=192.168.122.1
+	sys=chog9
+	dns=192.168.122.1`, "192.168.122.1"},
+		{`ipnet=murray-hill ip=135.104.0.0 ipmask=255.255.0.0
+	dns=135.104.10.1
+	ntp=ntp.cs.bell-labs.com
+	ipnet=plan9 ip=135.104.9.0 ipmask=255.255.255.0
+	ntp=oncore.cs.bell-labs.com
+	smtp=smtp1.cs.bell-labs.com
+	ip=135.104.9.6 sys=anna dom=anna.cs.bell-labs.com
+	smtp=smtp2.cs.bell-labs.com`, "135.104.10.1"},
+	}
+
+	for _, ndb := range ndbs {
+		act, err := getPlan9Config(ndb.in)
+		assert.Nil(t, err)
+		assert.Equal(t, ndb.want, act.Servers[0])
+	}
+
+	invalid := `sys = spindle
+	dom=spindle.research.bell-labs.com
+	bootf=/mips/9powerboot
+	ip=135.104.117.32 ether=080069020677
+	proto=il`
+
+	act, err := getPlan9Config(invalid)
+	assert.ErrorContains(t, err, "no DNS servers found")
+	assert.Nil(t, act)
+
+}
diff --git a/conf/win.go b/conf/win.go
new file mode 100644
index 0000000..2e5d23c
--- /dev/null
+++ b/conf/win.go
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build windows
+
+package conf
+
+import (
+	"strings"
+	"unsafe"
+
+	"github.com/miekg/dns"
+	"golang.org/x/sys/windows"
+)
+
+/*
+"Stolen" from
+https://gist.github.com/moloch--/9fb1c8497b09b45c840fe93dd23b1e98
+*/
+
+// WindowsDnsClientConfig - returns all DNS server addresses using windows fuckery.
+func GetDNSConfig() (*dns.ClientConfig, error) {
+	l := uint32(20000)
+	b := make([]byte, l)
+
+	// Windows is an utter fucking trash fire of an operating system.
+	if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l); err != nil {
+		return nil, err
+	}
+	var addresses []*windows.IpAdapterAddresses
+	for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); addr != nil; addr = addr.Next {
+		addresses = append(addresses, addr)
+	}
+
+	resolvers := map[string]bool{}
+	for _, addr := range addresses {
+		for next := addr.FirstUnicastAddress; next != nil; next = next.Next {
+			if addr.OperStatus != windows.IfOperStatusUp {
+				continue
+			}
+			if next.Address.IP() != nil {
+				for dnsServer := addr.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next {
+					ip := dnsServer.Address.IP()
+					if ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
+						continue
+					}
+					if ip.To16() != nil && strings.HasPrefix(ip.To16().String(), "fec0:") {
+						continue
+					}
+					resolvers[ip.String()] = true
+				}
+				break
+			}
+		}
+	}
+
+	// Take unique values only
+	servers := []string{}
+	for server := range resolvers {
+		servers = append(servers, server)
+	}
+
+	// TODO: Make configurable, based on defaults in https://github.com/miekg/dns/blob/master/clientconfig.go
+	return &dns.ClientConfig{
+		Servers:  servers,
+		Search:   []string{},
+		Port:     "53",
+		Ndots:    1,
+		Timeout:  5,
+		Attempts: 1,
+	}, nil
+}
diff --git a/go.mod b/go.mod
index 1ce1512..ebe8a60 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
-	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
+	golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/tools v0.1.11 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
