mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
381 lines
14 KiB
Go
381 lines
14 KiB
Go
//go:build darwin
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNativeCLATTargetPreservesNonOwnedLoopback(t *testing.T) {
|
|
for _, static := range [][]string{{"::1"}, {"::1", "2001:db8::53"}, {"127.0.0.1"}, {"127.0.0.1", "::1"}} {
|
|
t.Run(strings.Join(static, ","), func(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no DHCPv4")
|
|
h.dns["Wi-Fi"] = slices.Clone(static)
|
|
backup := filepath.Join(t.TempDir(), ".dns_Wi-Fi")
|
|
interceptSaveStaticDNSSnapshotFn = func(_ *net.Interface, snapshot []string, owned string) error {
|
|
return saveTargetStaticDNSSnapshot(backup, snapshot, owned)
|
|
}
|
|
p := newInterceptTargetProg()
|
|
p.cfg.Listener["0"].Port = 53
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if len(h.setCalls) != 0 || len(h.resetCalls) != 0 || p.interceptDNSTargetService != "" {
|
|
t.Fatalf("local resolver overwritten: set=%v reset=%v owner=%q", h.setCalls, h.resetCalls, p.interceptDNSTargetService)
|
|
}
|
|
p.removeInterceptDNSTarget("shutdown")
|
|
if !slices.Equal(h.dns["Wi-Fi"], static) {
|
|
t.Fatalf("lost user DNS: got %v, want %v", h.dns["Wi-Fi"], static)
|
|
}
|
|
if _, err := os.Stat(backup); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("backup unexpectedly changed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetRejectsLoopbackBeforeSnapshot(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no DHCPv4")
|
|
h.dns["Wi-Fi"] = []string{"::1", "2001:db8::53"}
|
|
h.dns["Old service"] = []string{"127.0.0.53"}
|
|
interceptSaveStaticDNSSnapshotFn = func(*net.Interface, []string, string) error {
|
|
t.Fatal("non-owned IPv6 loopback admitted to snapshot")
|
|
return nil
|
|
}
|
|
p := newInterceptTargetProg()
|
|
persistInterceptTargetForTest(t, p, "Old service", "127.0.0.53")
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if len(h.setCalls) != 0 || len(h.resetCalls) != 0 || p.interceptDNSTargetService != "Old service" {
|
|
t.Fatal("rejected admission changed old ownership")
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetLoopbackKeepsIPv4CleanupFirst(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no DHCPv4")
|
|
h.dns["Wi-Fi"] = []string{"::1", "127.0.0.1"}
|
|
h.dns["Old service"] = []string{"127.0.0.53"}
|
|
p := newInterceptTargetProg()
|
|
persistInterceptTargetForTest(t, p, "Old service", "127.0.0.53")
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if !slices.Equal(h.resetCalls, []string{"Old service"}) || len(h.setCalls) != 0 || p.interceptDNSTargetService != "" {
|
|
t.Fatalf("IPv4 cleanup skipped: reset=%v set=%v owner=%q", h.resetCalls, h.setCalls, p.interceptDNSTargetService)
|
|
}
|
|
if !slices.Equal(h.dns["Wi-Fi"], []string{"::1", "127.0.0.1"}) {
|
|
t.Fatal("external local resolvers changed")
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetSnapshotRejectsNonOwnedLoopback(t *testing.T) {
|
|
for _, snapshot := range [][]string{{"::1"}, {"2001:db8::53", "::1"}, {"127.0.0.1"}, {"127.0.0.53", "::1"}} {
|
|
t.Run(strings.Join(snapshot, ","), func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "backup")
|
|
const original = "2001:db8::54"
|
|
if err := os.WriteFile(path, []byte(original), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := saveTargetStaticDNSSnapshot(path, snapshot, "127.0.0.53"); err == nil {
|
|
t.Fatal("accepted unrestorable loopback snapshot")
|
|
}
|
|
if got, err := os.ReadFile(path); err != nil || string(got) != original {
|
|
t.Fatalf("failed snapshot changed backup: %q %v", got, err)
|
|
}
|
|
})
|
|
}
|
|
for _, owned := range []string{"127.0.0.53", "::1", "10.0.0.53"} {
|
|
t.Run("owned "+owned, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "backup")
|
|
const original = "2001:db8::54"
|
|
if err := os.WriteFile(path, []byte(original), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := saveTargetStaticDNSSnapshot(path, []string{owned}, owned); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := os.ReadFile(path); err != nil || string(got) != original {
|
|
t.Fatalf("owned target replaced backup: %q %v", got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetFallback(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
clat bool
|
|
nativeErr, readErr, saveErr error
|
|
static, dhcp []string
|
|
want bool
|
|
}{
|
|
{name: "CLAT", clat: true, want: true},
|
|
{name: "unknown"},
|
|
{name: "native read error", clat: true, nativeErr: errors.New("read failed")},
|
|
{name: "static read error", clat: true, readErr: errors.New("read failed")},
|
|
{name: "backup error", clat: true, saveErr: errors.New("disk full")},
|
|
{name: "static IPv4", clat: true, static: []string{"9.9.9.9"}},
|
|
{name: "partial DHCP IPv4", clat: true, dhcp: []string{"1.1.1.1"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.dhcpErr = errors.New("getoption and getpacket exit 1")
|
|
h.nativeCLAT, h.nativeErr, h.readErr, h.saveErr = tc.clat, tc.nativeErr, tc.readErr, tc.saveErr
|
|
h.dns["Wi-Fi"], h.dhcp = tc.static, tc.dhcp
|
|
p := newInterceptTargetProg()
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if (len(h.setCalls) > 0) != tc.want || len(h.resetCalls) != 0 {
|
|
t.Fatalf("set=%v reset=%v", h.setCalls, h.resetCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetUnknownPreservesStaticIPv4Cleanup(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.dhcpErr = errors.New("DHCP read failed")
|
|
h.nativeErr = errors.New("native state unavailable")
|
|
h.dns["Wi-Fi"] = []string{"192.0.2.53"}
|
|
h.dns["Old service"] = []string{"127.0.0.53"}
|
|
p := newInterceptTargetProg()
|
|
persistInterceptTargetForTest(t, p, "Old service", "127.0.0.53")
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if p.interceptDNSTargetService != "" || !slices.Equal(h.resetCalls, []string{"Old service"}) || len(h.setCalls) != 0 || !slices.Equal(h.dns["Wi-Fi"], []string{"192.0.2.53"}) {
|
|
t.Fatalf("existing static IPv4 cleanup changed: owner=%q reset=%v set=%v", p.interceptDNSTargetService, h.resetCalls, h.setCalls)
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetTransitionAndReadFailure(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
p := newInterceptTargetProg()
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no DHCPv4 packet")
|
|
h.dns["Wi-Fi"] = []string{"2001:db8::53"}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if p.interceptDNSTargetService != "Wi-Fi" {
|
|
t.Fatal("no target")
|
|
}
|
|
h.nativeErr = errors.New("store unavailable")
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if p.interceptDNSTargetService != "Wi-Fi" || len(h.setCalls) != 1 || len(h.resetCalls) != 0 {
|
|
t.Fatal("unknown changed ownership")
|
|
}
|
|
h.nativeErr = nil
|
|
h.dhcpErr = nil
|
|
h.dhcp = []string{"192.168.1.1"}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if p.interceptDNSTargetService != "" || !slices.Equal(h.dns["Wi-Fi"], []string{"2001:db8::53"}) {
|
|
t.Fatal("dual stack did not restore IPv6 static DNS")
|
|
}
|
|
}
|
|
|
|
// Use the real evidence reader: en1 has two configured services, and the
|
|
// second is primary. Legacy first-match mapping must not even be consulted.
|
|
func TestNativeCLATTargetExactPrimaryService(t *testing.T) {
|
|
for _, static := range [][]string{{"9.9.9.9"}, {"2001:db8::53"}} {
|
|
t.Run(strings.Join(static, ","), func(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.dhcpErr = errors.New("no packet")
|
|
h.serviceByDev["en1"] = "First Wi-Fi"
|
|
h.dns["First Wi-Fi"] = nil
|
|
h.dns["Primary Wi-Fi"] = slices.Clone(static)
|
|
var calls []string
|
|
outputs := append(nativeTargetTestOutputs(), nativeTargetTestOutputs()...)
|
|
r := nativeTargetTestReader(t, outputs, &calls)
|
|
interceptNativeCLATDefaultServiceFn = r.defaultService
|
|
interceptPatchNetIfaceNameFn = func(*net.Interface) (bool, error) {
|
|
t.Fatal("used first-match mapping for native fallback")
|
|
return false, nil
|
|
}
|
|
var reads []string
|
|
read := interceptNativeStaticDNSFn
|
|
interceptNativeStaticDNSFn = func(ctx context.Context, iface *net.Interface) ([]string, error) {
|
|
reads = append(reads, iface.Name)
|
|
return read(ctx, iface)
|
|
}
|
|
p := newInterceptTargetProg()
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if !slices.Equal(reads, []string{"Primary Wi-Fi", "Primary Wi-Fi"}) || len(calls) != 8 {
|
|
t.Fatalf("reads=%v calls=%v", reads, calls)
|
|
}
|
|
if len(h.dns["First Wi-Fi"]) != 0 {
|
|
t.Fatal("inactive service mutated")
|
|
}
|
|
if hasIPv4DNS(static) {
|
|
if !slices.Equal(h.dns["Primary Wi-Fi"], static) || len(h.setCalls) != 0 || len(h.resetCalls) != 0 {
|
|
t.Fatal("primary static IPv4 overwritten")
|
|
}
|
|
} else {
|
|
if !slices.Equal(h.setCalls, []string{"Primary Wi-Fi"}) || p.interceptDNSTargetService != "Primary Wi-Fi" {
|
|
t.Fatalf("set=%v ownership=%q", h.setCalls, p.interceptDNSTargetService)
|
|
}
|
|
p.removeInterceptDNSTarget("test shutdown")
|
|
if !slices.Equal(h.dns["Primary Wi-Fi"], static) {
|
|
t.Fatal("original static IPv6 not restored")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetIdentityRecheck(t *testing.T) {
|
|
for _, field := range []string{"UUID", "name", "device", "read error", "static DNS"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no packet")
|
|
p := newInterceptTargetProg()
|
|
persistInterceptTargetForTest(t, p, "Old service", "127.0.0.53")
|
|
h.dns["Old service"] = []string{"127.0.0.53"}
|
|
read := interceptNativeCLATDefaultServiceFn
|
|
calls := 0
|
|
var shared context.Context
|
|
interceptNativeCLATDefaultServiceFn = func(ctx context.Context, device string) (nativeTargetService, error) {
|
|
calls++
|
|
if calls == 1 {
|
|
shared = ctx
|
|
} else if shared != ctx {
|
|
t.Fatal("budget renewed on recheck")
|
|
}
|
|
svc, err := read(ctx, device)
|
|
if calls == 2 {
|
|
switch field {
|
|
case "UUID":
|
|
svc.ID = nativeTargetOtherServiceID
|
|
case "name":
|
|
svc.Name = "Other Wi-Fi"
|
|
case "device":
|
|
svc.Device = "en9"
|
|
case "read error":
|
|
err = errors.New("unknown")
|
|
case "static DNS":
|
|
h.dns["Wi-Fi"] = []string{"2001:db8::53"}
|
|
}
|
|
}
|
|
return svc, err
|
|
}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if calls != 2 || len(h.setCalls) != 0 || len(h.resetCalls) != 0 || p.interceptDNSTargetService != "Old service" {
|
|
t.Fatalf("uncertain recheck mutated state: calls=%d set=%v reset=%v owner=%q", calls, h.setCalls, h.resetCalls, p.interceptDNSTargetService)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetRestartListenerChangePreservesBackup(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no packet")
|
|
backup := filepath.Join(t.TempDir(), ".dns_Wi-Fi")
|
|
original := "2001:db8::53"
|
|
if err := os.WriteFile(backup, []byte(original), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(h.statePath, []byte(`{"service":"Wi-Fi","value":"10.0.0.53"}`), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.dns["Wi-Fi"] = []string{"10.0.0.53"}
|
|
var snapshots [][]string
|
|
interceptSaveStaticDNSSnapshotFn = func(_ *net.Interface, snapshot []string, owned string) error {
|
|
snapshots = append(snapshots, slices.Clone(snapshot))
|
|
return saveTargetStaticDNSSnapshot(backup, snapshot, owned)
|
|
}
|
|
interceptSaveCurrentStaticDNSFn = func(*net.Interface) error { t.Fatal("legacy unvalidated snapshot read"); return nil }
|
|
interceptSavedStaticNameserversFn = func(*net.Interface) []string {
|
|
data, err := os.ReadFile(backup)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return strings.Split(string(data), ",")
|
|
}
|
|
p := newInterceptTargetProg()
|
|
p.cfg.Listener["0"].IP, p.cfg.Listener["0"].Port = "10.0.0.54", 53
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if !slices.Equal(h.dns["Wi-Fi"], []string{"10.0.0.54"}) || p.interceptDNSTargetSetValue != "10.0.0.54" || len(snapshots) != 1 {
|
|
t.Fatalf("listener migration failed: dns=%v target=%q snapshots=%v", h.dns["Wi-Fi"], p.interceptDNSTargetSetValue, snapshots)
|
|
}
|
|
if got, err := os.ReadFile(backup); err != nil || string(got) != original {
|
|
t.Fatalf("original backup lost: %q %v", got, err)
|
|
}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if len(h.setCalls) != 2 {
|
|
t.Fatalf("target churn after reconciliation: %v", h.setCalls)
|
|
}
|
|
// A second restart must recover new ownership and still restore the original.
|
|
restarted := newInterceptTargetProg()
|
|
restarted.removeInterceptDNSTarget("shutdown")
|
|
if !slices.Equal(h.dns["Wi-Fi"], []string{original}) || restarted.interceptDNSTargetService != "" {
|
|
t.Fatalf("shutdown restored owned target: %v", h.dns["Wi-Fi"])
|
|
}
|
|
if _, err := os.Stat(h.statePath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("ownership not removed", err)
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetSnapshotFailureRetainsOwnership(t *testing.T) {
|
|
for _, mode := range []string{"write", "remove", "cleanup"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no packet")
|
|
p := newInterceptTargetProg()
|
|
persistInterceptTargetForTest(t, p, "Old service", "10.0.0.53")
|
|
h.dns["Old service"] = []string{"10.0.0.53"}
|
|
path := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(path, "block"), nil, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mode == "write" {
|
|
h.dns["Wi-Fi"] = []string{"2001:db8::53"}
|
|
}
|
|
if mode == "cleanup" {
|
|
path = filepath.Join(t.TempDir(), "backup")
|
|
h.resetErr = errors.New("cleanup failed")
|
|
}
|
|
interceptSaveStaticDNSSnapshotFn = func(_ *net.Interface, snapshot []string, owned string) error {
|
|
return saveTargetStaticDNSSnapshot(path, snapshot, owned)
|
|
}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if p.interceptDNSTargetService != "Old service" || len(h.setCalls) != 0 {
|
|
t.Fatal("failed backup/cleanup lost old ownership")
|
|
}
|
|
if mode != "cleanup" && len(h.resetCalls) != 0 {
|
|
t.Fatal("backup failure attempted cleanup")
|
|
}
|
|
if got := h.dns["Old service"]; !slices.Equal(got, []string{"10.0.0.53"}) {
|
|
t.Fatal("old service changed", got)
|
|
}
|
|
if _, err := os.Stat(h.statePath); err != nil {
|
|
t.Fatal("ownership file lost", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeCLATTargetRouteChange(t *testing.T) {
|
|
h := newInterceptTargetHarness(t)
|
|
p := newInterceptTargetProg()
|
|
h.nativeCLAT = true
|
|
h.dhcpErr = errors.New("no packet")
|
|
calls := 0
|
|
interceptDefaultRouteInterfaceFn = func() (string, error) {
|
|
calls++
|
|
if calls > 1 {
|
|
return "en9", nil
|
|
}
|
|
return "en1", nil
|
|
}
|
|
p.ensureInterceptDNSTarget([]string{})
|
|
if len(h.setCalls) != 0 || len(h.resetCalls) != 0 {
|
|
t.Fatal("route change mutated DNS")
|
|
}
|
|
}
|