mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
599 lines
20 KiB
Go
599 lines
20 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const scutilDNSFixture = `DNS configuration
|
|
|
|
resolver #1
|
|
search domain[0] : corp.example
|
|
nameserver[0] : 192.168.50.200
|
|
nameserver[1] : 192.168.50.1
|
|
if_index : 16 (en0)
|
|
flags : Request A records, Request AAAA records
|
|
reach : 0x00020002 (Reachable,Directly Reachable Address)
|
|
order : 200000
|
|
|
|
resolver #2
|
|
nameserver[0] : 100.100.100.100
|
|
if_index : 28 (utun4)
|
|
flags : Supplemental, Request A records, Request AAAA records
|
|
reach : 0x00000003 (Reachable,Transient Connection)
|
|
order : 101600
|
|
|
|
resolver #3
|
|
domain : local
|
|
options : mdns
|
|
timeout : 5
|
|
flags : Request A records
|
|
reach : 0x00000000 (Not Reachable)
|
|
order : 300000
|
|
|
|
DNS configuration (for scoped queries)
|
|
|
|
resolver #1
|
|
nameserver[0] : 192.168.50.200
|
|
if_index : 16 (en0)
|
|
flags : Scoped, Request A records
|
|
reach : 0x00020002 (Reachable,Directly Reachable Address)
|
|
`
|
|
|
|
const scutilDNSScopedOnlyFixture = `DNS configuration
|
|
|
|
DNS configuration (for scoped queries)
|
|
|
|
resolver #1
|
|
nameserver[0] : 192.168.50.200
|
|
if_index : 16 (en0)
|
|
flags : Scoped, Request A records
|
|
reach : 0x00020002 (Reachable,Directly Reachable Address)
|
|
`
|
|
|
|
var errStubResolverRun = errors.New("scutil failed")
|
|
|
|
func wifiResolverEntry() dnsResolverEntry {
|
|
return dnsResolverEntry{
|
|
Order: 200000,
|
|
Nameservers: []string{"192.168.50.200", "192.168.50.1"},
|
|
IfIndex: 16,
|
|
Interface: "en0",
|
|
Flags: "Request A records, Request AAAA records",
|
|
SearchDomains: []string{"corp.example"},
|
|
Reachable: "Reachable,Directly Reachable Address",
|
|
}
|
|
}
|
|
|
|
func vpnResolverEntry() dnsResolverEntry {
|
|
return dnsResolverEntry{
|
|
Order: 101600,
|
|
Nameservers: []string{"100.100.100.100"},
|
|
IfIndex: 28,
|
|
Interface: "utun4",
|
|
Flags: "Supplemental, Request A records, Request AAAA records",
|
|
Reachable: "Reachable,Transient Connection",
|
|
}
|
|
}
|
|
|
|
func mdnsResolverEntry() dnsResolverEntry {
|
|
return dnsResolverEntry{
|
|
Order: 300000,
|
|
Flags: "Request A records",
|
|
Reachable: "Not Reachable",
|
|
}
|
|
}
|
|
|
|
func Test_parseSCUtilDNS(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want []dnsResolverEntry
|
|
}{
|
|
{
|
|
name: "both tables",
|
|
input: scutilDNSFixture,
|
|
want: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry(), mdnsResolverEntry(), {
|
|
Nameservers: []string{"192.168.50.200"},
|
|
IfIndex: 16,
|
|
Interface: "en0",
|
|
Flags: "Scoped, Request A records",
|
|
Reachable: "Reachable,Directly Reachable Address",
|
|
Scoped: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "scoped table when the first is empty",
|
|
input: scutilDNSScopedOnlyFixture,
|
|
want: []dnsResolverEntry{{
|
|
Nameservers: []string{"192.168.50.200"},
|
|
IfIndex: 16,
|
|
Interface: "en0",
|
|
Flags: "Scoped, Request A records",
|
|
Reachable: "Reachable,Directly Reachable Address",
|
|
Scoped: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "no output",
|
|
input: "",
|
|
want: nil,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseSCUtilDNS(strings.NewReader(tc.input))
|
|
if err != nil {
|
|
t.Fatalf("parseSCUtilDNS() error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("parseSCUtilDNS() = %+v, want %+v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_diffResolverTables(t *testing.T) {
|
|
changedWiFi := wifiResolverEntry()
|
|
changedWiFi.Nameservers = []string{"10.0.0.1"}
|
|
changedWiFi.Action = resolverActionChanged
|
|
vanishedVPN := vpnResolverEntry()
|
|
vanishedVPN.Nameservers = nil
|
|
vanishedVPN.Action = resolverActionRemoved
|
|
appearedVPN := vpnResolverEntry()
|
|
appearedVPN.Action = resolverActionAdded
|
|
|
|
tests := []struct {
|
|
name string
|
|
before []dnsResolverEntry
|
|
after []dnsResolverEntry
|
|
want []dnsResolverEntry
|
|
}{
|
|
{
|
|
name: "no change",
|
|
before: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()},
|
|
after: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "one resolver changed",
|
|
before: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()},
|
|
after: []dnsResolverEntry{changedWiFi, vpnResolverEntry()},
|
|
want: []dnsResolverEntry{changedWiFi},
|
|
},
|
|
{
|
|
name: "one resolver vanished",
|
|
before: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()},
|
|
after: []dnsResolverEntry{wifiResolverEntry()},
|
|
want: []dnsResolverEntry{vanishedVPN},
|
|
},
|
|
{
|
|
name: "one resolver appeared",
|
|
before: []dnsResolverEntry{wifiResolverEntry()},
|
|
after: []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()},
|
|
want: []dnsResolverEntry{appearedVPN},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := diffResolverTables(tc.before, tc.after)
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("diffResolverTables() = %+v, want %+v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_diffResolverTablesKeepsTheInput(t *testing.T) {
|
|
before := []dnsResolverEntry{vpnResolverEntry()}
|
|
diffResolverTables(before, nil)
|
|
if !reflect.DeepEqual(before[0], vpnResolverEntry()) {
|
|
t.Fatalf("before entry = %+v, want %+v", before[0], vpnResolverEntry())
|
|
}
|
|
}
|
|
|
|
// resolverRunResult is one answer of the stub runner.
|
|
type resolverRunResult struct {
|
|
table []dnsResolverEntry
|
|
err error
|
|
}
|
|
|
|
// stubResolverRunner answers one result per call and repeats the last one.
|
|
type stubResolverRunner struct {
|
|
mu sync.Mutex
|
|
results []resolverRunResult
|
|
calls int
|
|
}
|
|
|
|
func (s *stubResolverRunner) run() ([]dnsResolverEntry, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
result := s.results[min(s.calls, len(s.results)-1)]
|
|
s.calls++
|
|
return result.table, result.err
|
|
}
|
|
|
|
func Test_dnsConfigPollerNextDelay(t *testing.T) {
|
|
base := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
|
tests := []struct {
|
|
name string
|
|
activity bool
|
|
elapsed time.Duration
|
|
want time.Duration
|
|
}{
|
|
{name: "nine minutes after activity", activity: true, elapsed: 9 * time.Minute, want: dnsConfigFastInterval},
|
|
{name: "eleven minutes after activity", activity: true, elapsed: 11 * time.Minute, want: dnsConfigSlowInterval},
|
|
{name: "no activity yet", elapsed: time.Minute, want: dnsConfigSlowInterval},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
poller := newDNSConfigPoller(func() ([]dnsResolverEntry, error) { return nil, nil })
|
|
if tc.activity {
|
|
poller.noteActivity(base)
|
|
}
|
|
if got := poller.nextDelay(base.Add(tc.elapsed)); got != tc.want {
|
|
t.Fatalf("nextDelay() = %s, want %s", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_dnsConfigPollerPollOnce(t *testing.T) {
|
|
now := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
|
baseline := []dnsResolverEntry{wifiResolverEntry(), vpnResolverEntry()}
|
|
changedWiFi := wifiResolverEntry()
|
|
changedWiFi.Nameservers = []string{"10.0.0.1"}
|
|
changedWiFi.Action = resolverActionChanged
|
|
runner := &stubResolverRunner{results: []resolverRunResult{
|
|
{table: baseline},
|
|
{err: errStubResolverRun},
|
|
{table: baseline},
|
|
{table: []dnsResolverEntry{changedWiFi, vpnResolverEntry()}},
|
|
}}
|
|
poller := newDNSConfigPoller(runner.run)
|
|
|
|
if got := poller.pollOnce(now); got != nil {
|
|
t.Fatalf("baseline poll = %+v, want nil", got)
|
|
}
|
|
if got := poller.pollOnce(now.Add(time.Minute)); got != nil {
|
|
t.Fatalf("failed poll = %+v, want nil", got)
|
|
}
|
|
if got := poller.pollOnce(now.Add(2 * time.Minute)); got != nil {
|
|
t.Fatalf("poll after a failure = %+v, want nil", got)
|
|
}
|
|
want := []dnsResolverEntry{changedWiFi}
|
|
if got := poller.pollOnce(now.Add(3 * time.Minute)); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("changed poll = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func Test_dnsConfigPollerLoop(t *testing.T) {
|
|
previous := dnsConfigDelayFn
|
|
dnsConfigDelayFn = func(*dnsConfigPoller, time.Time) time.Duration { return time.Millisecond }
|
|
t.Cleanup(func() { dnsConfigDelayFn = previous })
|
|
|
|
changedWiFi := wifiResolverEntry()
|
|
changedWiFi.Nameservers = []string{"10.0.0.1"}
|
|
changedWiFi.Action = resolverActionChanged
|
|
runner := &stubResolverRunner{results: []resolverRunResult{
|
|
{table: []dnsResolverEntry{wifiResolverEntry()}},
|
|
{table: []dnsResolverEntry{changedWiFi}},
|
|
}}
|
|
poller := newDNSConfigPoller(runner.run)
|
|
|
|
emitted := make(chan []dnsResolverEntry, 4)
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
poller.loop(stop, func(changed []dnsResolverEntry) { emitted <- changed })
|
|
}()
|
|
|
|
select {
|
|
case got := <-emitted:
|
|
want := []dnsResolverEntry{changedWiFi}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("emitted = %+v, want %+v", got, want)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop emitted no change")
|
|
}
|
|
|
|
close(stop)
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop did not stop")
|
|
}
|
|
}
|
|
|
|
// Test_dnsConfigPollerLoopWakesOnActivity proves that a network event does not
|
|
// wait for a sleeping timer. The delay is one hour until the event arrives, so
|
|
// a loop without the kick would poll after the test deadline.
|
|
func Test_dnsConfigPollerLoopWakesOnActivity(t *testing.T) {
|
|
var kicked atomic.Bool
|
|
parked := make(chan struct{}, 1)
|
|
previous := dnsConfigDelayFn
|
|
dnsConfigDelayFn = func(*dnsConfigPoller, time.Time) time.Duration {
|
|
if kicked.Load() {
|
|
return time.Millisecond
|
|
}
|
|
select {
|
|
case parked <- struct{}{}:
|
|
default:
|
|
}
|
|
return time.Hour
|
|
}
|
|
t.Cleanup(func() { dnsConfigDelayFn = previous })
|
|
|
|
changedWiFi := wifiResolverEntry()
|
|
changedWiFi.Nameservers = []string{"10.0.0.1"}
|
|
changedWiFi.Action = resolverActionChanged
|
|
runner := &stubResolverRunner{results: []resolverRunResult{
|
|
{table: []dnsResolverEntry{wifiResolverEntry()}},
|
|
{table: []dnsResolverEntry{changedWiFi}},
|
|
}}
|
|
poller := newDNSConfigPoller(runner.run)
|
|
|
|
emitted := make(chan []dnsResolverEntry, 4)
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
poller.loop(stop, func(changed []dnsResolverEntry) { emitted <- changed })
|
|
}()
|
|
t.Cleanup(func() {
|
|
close(stop)
|
|
<-done
|
|
})
|
|
|
|
// The kick matters only once the loop waits on the long delay.
|
|
select {
|
|
case <-parked:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop did not park on the long delay")
|
|
}
|
|
kicked.Store(true)
|
|
poller.noteActivity(time.Now())
|
|
|
|
select {
|
|
case got := <-emitted:
|
|
want := []dnsResolverEntry{changedWiFi}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("emitted = %+v, want %+v", got, want)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop polled no table after the activity")
|
|
}
|
|
}
|
|
|
|
// Test_dnsConfigPollerLoopTakesABaseline proves that the loop reads the table
|
|
// once at entry. Without that read the first change after the start reports
|
|
// the whole table.
|
|
func Test_dnsConfigPollerLoopTakesABaseline(t *testing.T) {
|
|
previous := dnsConfigDelayFn
|
|
dnsConfigDelayFn = func(*dnsConfigPoller, time.Time) time.Duration { return time.Hour }
|
|
t.Cleanup(func() { dnsConfigDelayFn = previous })
|
|
|
|
runner := &stubResolverRunner{results: []resolverRunResult{{table: []dnsResolverEntry{wifiResolverEntry()}}}}
|
|
poller := newDNSConfigPoller(runner.run)
|
|
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
poller.loop(stop, func([]dnsResolverEntry) { t.Error("the baseline read reported a change") })
|
|
}()
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
runner.mu.Lock()
|
|
calls := runner.calls
|
|
runner.mu.Unlock()
|
|
if calls > 0 {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
close(stop)
|
|
<-done
|
|
|
|
runner.mu.Lock()
|
|
defer runner.mu.Unlock()
|
|
if runner.calls != 1 {
|
|
t.Fatalf("reads at the loop entry: got %d, want 1", runner.calls)
|
|
}
|
|
if !poller.started {
|
|
t.Fatal("the loop took no baseline")
|
|
}
|
|
}
|
|
|
|
// oversizedSCUtilDNS is a table with one line that no scanner buffer holds. A
|
|
// stalled or hostile process produces output of this shape.
|
|
func oversizedSCUtilDNS() string {
|
|
return "DNS configuration\n\nresolver #1\n nameserver[0] : " + strings.Repeat("9", 70*1024) + "\n"
|
|
}
|
|
|
|
// Test_parseSCUtilDNSRejectsAnOversizedLine proves that a line the scanner
|
|
// cannot hold ends the read with an error. A part of a table must not pass for
|
|
// the table of the host.
|
|
func Test_parseSCUtilDNSRejectsAnOversizedLine(t *testing.T) {
|
|
got, err := parseSCUtilDNS(strings.NewReader(oversizedSCUtilDNS()))
|
|
if err == nil {
|
|
t.Fatal("parseSCUtilDNS accepted a 70 KiB line")
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("parseSCUtilDNS() = %+v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// Test_parseSCUtilDNSRejectsAnOversizedOutput proves that output past the
|
|
// bound ends the read with an error instead of a table cut in the middle.
|
|
func Test_parseSCUtilDNSRejectsAnOversizedOutput(t *testing.T) {
|
|
block := "resolver #1\n nameserver[0] : 192.168.50.200\n order : 200000\n"
|
|
repeats := scutilDNSOutputLimit/len(block) + 2
|
|
got, err := parseSCUtilDNS(strings.NewReader("DNS configuration\n\n" + strings.Repeat(block, repeats)))
|
|
if err == nil {
|
|
t.Fatal("parseSCUtilDNS accepted output past the bound")
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("parseSCUtilDNS() = %+v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// Test_dnsConfigPollerKeepsTheTableOnAnOversizedRead proves that a read the
|
|
// parser rejects reports nothing and keeps the table of the read before.
|
|
func Test_dnsConfigPollerKeepsTheTableOnAnOversizedRead(t *testing.T) {
|
|
now := time.Date(2026, 9, 17, 10, 0, 0, 0, time.UTC)
|
|
outputs := []string{scutilDNSFixture, oversizedSCUtilDNS(), scutilDNSFixture}
|
|
calls := 0
|
|
poller := newDNSConfigPoller(func() ([]dnsResolverEntry, error) {
|
|
output := outputs[min(calls, len(outputs)-1)]
|
|
calls++
|
|
return parseSCUtilDNS(strings.NewReader(output))
|
|
})
|
|
|
|
if got := poller.pollOnce(now); got != nil {
|
|
t.Fatalf("baseline poll = %+v, want nil", got)
|
|
}
|
|
if got := poller.pollOnce(now.Add(time.Minute)); got != nil {
|
|
t.Fatalf("oversized poll = %+v, want nil", got)
|
|
}
|
|
if got := poller.pollOnce(now.Add(2 * time.Minute)); got != nil {
|
|
t.Fatalf("poll after the oversized read = %+v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// scutilTwoTables renders a host with one unscoped resolver and one scoped
|
|
// resolver, the shape of a Mac with two active services.
|
|
func scutilTwoTables(unscoped, scoped string) string {
|
|
return "DNS configuration\n\nresolver #1\n nameserver[0] : " + unscoped + "\n if_index : 14 (en0)\n" +
|
|
" flags : Request A records\n reach : 0x00020002 (Reachable,Directly Reachable Address)\n\n" +
|
|
"DNS configuration (for scoped queries)\n\nresolver #1\n nameserver[0] : " + scoped + "\n if_index : 15 (en1)\n" +
|
|
" flags : Scoped, Request A records\n reach : 0x00020002 (Reachable,Directly Reachable Address)\n"
|
|
}
|
|
|
|
// Test_diffResolverTablesSeesAScopedChange changes the scoped resolver of a
|
|
// second service while the unscoped resolver stays. The diff must report the
|
|
// scoped change, because the two tables are not copies of each other.
|
|
func Test_diffResolverTablesSeesAScopedChange(t *testing.T) {
|
|
before, err := parseSCUtilDNS(strings.NewReader(scutilTwoTables("192.168.1.1", "10.0.0.1")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := parseSCUtilDNS(strings.NewReader(scutilTwoTables("192.168.1.1", "9.9.9.9")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed := diffResolverTables(before, after)
|
|
if len(changed) != 1 || len(changed[0].Nameservers) != 1 || changed[0].Nameservers[0] != "9.9.9.9" {
|
|
t.Fatalf("changed resolvers = %+v, want the scoped resolver with 9.9.9.9", changed)
|
|
}
|
|
if !changed[0].Scoped || before[0].Scoped || len(before) != 2 {
|
|
t.Fatalf("scope marks: changed %v, before %+v; want the scoped resolver marked and the unscoped one not", changed[0].Scoped, before)
|
|
}
|
|
}
|
|
|
|
// Test_dnsConfigPollerPollsUnderContinuousActivity kicks the poller faster
|
|
// than its fast interval. A kick must not push the read back again and again,
|
|
// or a host in a storm of transitions never reports a resolver change.
|
|
func Test_dnsConfigPollerPollsUnderContinuousActivity(t *testing.T) {
|
|
var reads atomic.Int64
|
|
changed := wifiResolverEntry()
|
|
changed.Nameservers = []string{"9.9.9.9"}
|
|
p := newDNSConfigPoller(func() ([]dnsResolverEntry, error) {
|
|
if reads.Add(1) == 1 {
|
|
return []dnsResolverEntry{wifiResolverEntry()}, nil
|
|
}
|
|
return []dnsResolverEntry{changed}, nil
|
|
})
|
|
origDelay := dnsConfigDelayFn
|
|
t.Cleanup(func() { dnsConfigDelayFn = origDelay })
|
|
dnsConfigDelayFn = func(*dnsConfigPoller, time.Time) time.Duration { return 50 * time.Millisecond }
|
|
|
|
var emits atomic.Int64
|
|
stop := make(chan struct{})
|
|
done := p.startLoop(stop, func([]dnsResolverEntry) { emits.Add(1) })
|
|
deadline := time.Now().Add(500 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
p.noteActivity(time.Now())
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
close(stop)
|
|
<-done
|
|
|
|
if emits.Load() == 0 {
|
|
t.Fatalf("no resolver event during 500 ms of activity (reads %d)", reads.Load())
|
|
}
|
|
}
|
|
|
|
// scutilScopedTable renders the scoped table the way macOS prints it: one
|
|
// resolver per interface and no order line.
|
|
func scutilScopedTable(resolvers ...[2]string) string {
|
|
var b strings.Builder
|
|
b.WriteString("DNS configuration\n\nresolver #1\n nameserver[0] : 100.100.100.100\n if_index : 28 (utun4)\n flags : Request A records, Request AAAA records\n reach : 0x00000003 (Reachable,Transient Connection)\n order : 200000\n\n")
|
|
b.WriteString("DNS configuration (for scoped queries)\n\n")
|
|
for i, resolver := range resolvers {
|
|
fmt.Fprintf(&b, "resolver #%d\n nameserver[0] : %s\n if_index : %s\n flags : Scoped, Request A records\n reach : 0x00020002 (Reachable,Directly Reachable Address)\n\n", i+1, resolver[1], resolver[0])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// Test_diffResolverTablesKeysScopedResolversByInterface inserts a scoped
|
|
// resolver between two others and removes it again. The scoped table has no
|
|
// order line, so the interface is the identity, and the diff must name the
|
|
// resolver that came and went, not its neighbors.
|
|
func Test_diffResolverTablesKeysScopedResolversByInterface(t *testing.T) {
|
|
en0 := [2]string{"16 (en0)", "192.168.50.200"}
|
|
en7 := [2]string{"30 (en7)", "10.9.9.1"}
|
|
utun := [2]string{"28 (utun4)", "100.100.100.100"}
|
|
before, err := parseSCUtilDNS(strings.NewReader(scutilScopedTable(en0, utun)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := parseSCUtilDNS(strings.NewReader(scutilScopedTable(en0, en7, utun)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
inserted := diffResolverTables(before, after)
|
|
if len(inserted) != 1 || inserted[0].Interface != "en7" || inserted[0].Nameservers == nil {
|
|
t.Fatalf("insert of en7 reported %+v, want en7 alone with its nameserver", inserted)
|
|
}
|
|
removed := diffResolverTables(after, before)
|
|
if len(removed) != 1 || removed[0].Interface != "en7" || removed[0].Nameservers != nil {
|
|
t.Fatalf("removal of en7 reported %+v, want en7 alone without nameservers", removed)
|
|
}
|
|
}
|
|
|
|
// Test_diffResolverTablesNamesTheAction tells a resolver that came from one
|
|
// that went. An mDNS resolver has no nameservers, so without the action the
|
|
// two events read the same.
|
|
func Test_diffResolverTablesNamesTheAction(t *testing.T) {
|
|
mdns := "DNS configuration\n\nresolver #1\n domain : local\n options : mdns\n timeout : 5\n flags : Request A records, Request AAAA records\n reach : 0x00000000 (Not Reachable)\n order : 300000\n"
|
|
table, err := parseSCUtilDNS(strings.NewReader(mdns))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appeared := diffResolverTables(nil, table)
|
|
vanished := diffResolverTables(table, nil)
|
|
if len(appeared) != 1 || appeared[0].Action != resolverActionAdded {
|
|
t.Fatalf("appeared = %+v, want one resolver with action added", appeared)
|
|
}
|
|
if len(vanished) != 1 || vanished[0].Action != resolverActionRemoved {
|
|
t.Fatalf("vanished = %+v, want one resolver with action removed", vanished)
|
|
}
|
|
moved := wifiResolverEntry()
|
|
moved.Nameservers = []string{"9.9.9.9"}
|
|
changed := diffResolverTables([]dnsResolverEntry{wifiResolverEntry()}, []dnsResolverEntry{moved})
|
|
if len(changed) != 1 || changed[0].Action != resolverActionChanged {
|
|
t.Fatalf("changed = %+v, want one resolver with action changed", changed)
|
|
}
|
|
}
|