Skip to content

Commit b94f841

Browse files
hhfrancoisclaude
andcommitted
fix(tun): Windows single-label DNS via an NRPT catch-all (scutil equivalent)
Setting the WinTUN adapter DNS alone didn't cover single-label cluster names — Windows devolution + interface ordering skip it, so getaddrinfo("my-service") never reached the in-stack resolver ("Could not resolve host" in real apps, though the selftest's direct query passed). Install a catch-all NRPT rule (namespace ".") pointing all resolution at 198.18.<N>.53 via the DnsClient cmdlets, removed on teardown. The Windows counterpart of the macOS scutil repoint / Linux resolv.conf. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d6332ba commit b94f841

1 file changed

Lines changed: 34 additions & 0 deletions

File tree

cli/internal/tun/route_windows.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package tun
55
import (
66
"fmt"
77
"net/netip"
8+
"os/exec"
89

910
"golang.org/x/sys/windows"
1011
wgtun "golang.zx2c4.com/wireguard/tun"
@@ -47,10 +48,43 @@ func configure(dev any, _, cidr, dnsIP string, log logfn) ([]string, string, fun
4748
log.f("tun[win]: set DNS: %v", e)
4849
}
4950
}
51+
// Adapter DNS alone doesn't cover SINGLE-LABEL names (my-service): Windows
52+
// devolution + interface ordering skip it, so getaddrinfo("my-service") never
53+
// reaches our resolver ("Could not resolve host"). An NRPT catch-all rule points
54+
// ALL name resolution at dnsIP — the Windows counterpart of the macOS scutil
55+
// repoint. Our in-stack DNS answers cluster names with a fake IP and forwards the
56+
// rest to the saved upstreams, so a machine-wide rule is safe.
57+
if err := setSystemNRPT(dnsIP); err != nil {
58+
log.f("tun[win]: NRPT repoint failed — single-label names may not resolve: %v", err)
59+
} else {
60+
log.f("tun[win]: system DNS repointed to %s (NRPT catch-all)", dnsIP)
61+
}
5062

5163
cleanup := func() {
64+
clearSystemNRPT(dnsIP)
5265
_ = luid.FlushRoutes(v4)
5366
_ = luid.FlushIPAddresses(v4)
5467
}
5568
return nil, "", cleanup, nil
5669
}
70+
71+
// setSystemNRPT installs a catch-all Name Resolution Policy Table rule (namespace
72+
// ".") sending every DNS query to dnsIP. This is what makes single-label cluster
73+
// names resolve on Windows — the equivalent of scutil on macOS / resolv.conf on
74+
// Linux. Driven through the DnsClient cmdlets, which encode the DnsPolicyConfig
75+
// registry correctly (the same table WireGuard-Windows writes by hand), and flush
76+
// the resolver cache so a prior negative answer ("Could not resolve") doesn't stick.
77+
func setSystemNRPT(dnsIP string) error {
78+
clearSystemNRPT(dnsIP) // drop a stale rule a crashed run may have left
79+
return psRun("Add-DnsClientNrptRule -Namespace '.' -NameServers '" + dnsIP + "'; Clear-DnsClientCache")
80+
}
81+
82+
// clearSystemNRPT removes the rule(s) pointing at dnsIP and flushes the cache. Keyed
83+
// on the server IP so it targets only ours, and tolerant of there being none.
84+
func clearSystemNRPT(dnsIP string) {
85+
_ = psRun("Get-DnsClientNrptRule | Where-Object { $_.NameServers -contains '" + dnsIP + "' } | Remove-DnsClientNrptRule -Force; Clear-DnsClientCache")
86+
}
87+
88+
func psRun(script string) error {
89+
return exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script).Run()
90+
}

0 commit comments

Comments
 (0)