b2ad8f4844
Completes the mDNS local-discovery feature on Windows (was Linux-only since v2.1.2). Three parts: 1. Windows hosts override (hosts_override_windows.go): %SystemRoot%...\\hosts, CRLF-aware read/write, ipconfig /flushdns after each change. The agent runs as a SYSTEM service so elevation is a non-issue. hosts_override.go split into shared rewrite logic + platform files; the hosts tests now run the real Windows write path on CI instead of skipping. 2. Local route pinning (local_route*.go): the hosts override only fixes name resolution -- the packet path is the routing table's job. If the WG mesh tunnel is up with AllowedIPs covering the LAN (or full-tunnel 0.0.0.0/0) it swallows the direct connection. Discovery now pins a /32 host route via the owning local interface (route.exe metric 1 on Windows, ip route replace on Linux) and drops it on revert. Closes a gap in the shipped Linux path too. 3. Prompt reconnect: apply/revert signals the WS loop so it reconnects immediately instead of waiting out the 5s backoff. Route/hosts code is injectable + unit tested; go test passes natively on Windows (this machine), and linux/amd64 + windows/arm64 cross-builds are clean.
151 lines
4.9 KiB
Go
151 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/mdns"
|
|
)
|
|
|
|
func withTempHostsFile(t *testing.T, initial string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "hosts")
|
|
if initial != "" {
|
|
if err := os.WriteFile(path, []byte(initial), 0644); err != nil {
|
|
t.Fatalf("seeding temp hosts file: %v", err)
|
|
}
|
|
}
|
|
// setTestHostsPath redirects the platform hosts path at this temp file and
|
|
// restores it on cleanup. Runs on every OS: Windows hosts tests use the
|
|
// real Windows write path (minus the ipconfig flush, which the injected
|
|
// path suppresses), so this is where the CRLF/Windows behavior is guarded.
|
|
restore := setTestHostsPath(path)
|
|
t.Cleanup(restore)
|
|
return path
|
|
}
|
|
|
|
func TestApplyHostsOverride_AddsManagedBlock(t *testing.T) {
|
|
path := withTempHostsFile(t, "127.0.0.1\tlocalhost\n")
|
|
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.5"}); err != nil {
|
|
t.Fatalf("applyHostsOverride: %v", err)
|
|
}
|
|
|
|
got, _ := os.ReadFile(path)
|
|
s := string(got)
|
|
if !strings.Contains(s, "127.0.0.1\tlocalhost") {
|
|
t.Errorf("existing content was clobbered: %q", s)
|
|
}
|
|
if !strings.Contains(s, hostsBlockBegin) || !strings.Contains(s, hostsBlockEnd) {
|
|
t.Errorf("managed block markers missing: %q", s)
|
|
}
|
|
if !strings.Contains(s, "10.0.0.5\tsso.example.com") {
|
|
t.Errorf("override entry missing: %q", s)
|
|
}
|
|
}
|
|
|
|
func TestApplyHostsOverride_ReplacesPriorBlockRatherThanStacking(t *testing.T) {
|
|
path := withTempHostsFile(t, "")
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.5"}); err != nil {
|
|
t.Fatalf("first apply: %v", err)
|
|
}
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.9"}); err != nil {
|
|
t.Fatalf("second apply: %v", err)
|
|
}
|
|
|
|
got, _ := os.ReadFile(path)
|
|
s := string(got)
|
|
if strings.Count(s, hostsBlockBegin) != 1 {
|
|
t.Fatalf("expected exactly one managed block, got content: %q", s)
|
|
}
|
|
if strings.Contains(s, "10.0.0.5") {
|
|
t.Errorf("stale override (10.0.0.5) should have been replaced, got: %q", s)
|
|
}
|
|
if !strings.Contains(s, "10.0.0.9") {
|
|
t.Errorf("new override missing, got: %q", s)
|
|
}
|
|
}
|
|
|
|
func TestApplyHostsOverride_EmptyEntriesRemovesBlockEntirely(t *testing.T) {
|
|
path := withTempHostsFile(t, "127.0.0.1\tlocalhost\n")
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.5"}); err != nil {
|
|
t.Fatalf("apply: %v", err)
|
|
}
|
|
if err := applyHostsOverride(map[string]string{}); err != nil {
|
|
t.Fatalf("clear: %v", err)
|
|
}
|
|
|
|
got, _ := os.ReadFile(path)
|
|
s := string(got)
|
|
if strings.Contains(s, hostsBlockBegin) || strings.Contains(s, "10.0.0.5") {
|
|
t.Errorf("expected no discovery trace left after clearing, got: %q", s)
|
|
}
|
|
if !strings.Contains(s, "127.0.0.1\tlocalhost") {
|
|
t.Errorf("pre-existing content should survive a full clear, got: %q", s)
|
|
}
|
|
}
|
|
|
|
func TestApplyHostsOverride_CRLFWindowsHostsFile(t *testing.T) {
|
|
// Windows hosts files use CRLF. The rewrite must (a) match the block
|
|
// markers on a CRLF file, (b) write back with the platform EOL, and (c)
|
|
// not double up \r\r\n from the read side.
|
|
path := withTempHostsFile(t, "127.0.0.1\tlocalhost\r\n192.168.1.5\tsomeotherhost\r\n")
|
|
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.5"}); err != nil {
|
|
t.Fatalf("apply: %v", err)
|
|
}
|
|
if err := applyHostsOverride(map[string]string{"sso.example.com": "10.0.0.9"}); err != nil {
|
|
t.Fatalf("reapply: %v", err)
|
|
}
|
|
|
|
got, _ := os.ReadFile(path)
|
|
s := string(got)
|
|
if strings.Contains(s, "\r\r\n") {
|
|
t.Fatalf("doubled CR detected (CRLF handled wrong): %q", s)
|
|
}
|
|
if strings.Contains(s, "10.0.0.5") {
|
|
t.Errorf("stale override should be replaced on a CRLF file, got: %q", s)
|
|
}
|
|
if !strings.Contains(s, "10.0.0.9\tsso.example.com") {
|
|
t.Errorf("override entry missing on CRLF file, got: %q", s)
|
|
}
|
|
if strings.Count(s, hostsBlockBegin) != 1 {
|
|
t.Errorf("expected exactly one managed block, got: %q", s)
|
|
}
|
|
for _, want := range []string{"127.0.0.1\tlocalhost", "192.168.1.5\tsomeotherhost"} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("pre-existing content %q was clobbered, got: %q", want, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHostFromURL(t *testing.T) {
|
|
cases := map[string]string{
|
|
"https://sso.example.com:443/api": "sso.example.com",
|
|
"http://sso.example.com": "sso.example.com",
|
|
"not a url at all": "",
|
|
"": "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := hostFromURL(in); got != want {
|
|
t.Errorf("hostFromURL(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryAnnouncesHost(t *testing.T) {
|
|
entry := &mdns.ServiceEntry{InfoFields: []string{"hosts=sso.example.com,proxy.example.com"}}
|
|
if !entryAnnouncesHost(entry, "sso.example.com") {
|
|
t.Error("expected match for sso.example.com")
|
|
}
|
|
if !entryAnnouncesHost(entry, "proxy.example.com") {
|
|
t.Error("expected match for proxy.example.com")
|
|
}
|
|
if entryAnnouncesHost(entry, "jump.example.com") {
|
|
t.Error("expected no match for a host not in the TXT record")
|
|
}
|
|
}
|