Compare commits
24 Commits
aaron/dnsa
...
aaron/logl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96188ffd2f | ||
|
|
486059589b | ||
|
|
59f4f33f60 | ||
|
|
ac8e69b713 | ||
|
|
0f3b55c299 | ||
|
|
4691e012a9 | ||
|
|
e133bb570b | ||
|
|
adc97e9c4d | ||
|
|
d24a8f7b5a | ||
|
|
8dbda1a722 | ||
|
|
cced414c7d | ||
|
|
cab5c46481 | ||
|
|
63cd581c3f | ||
|
|
a5235e165c | ||
|
|
c8829b742b | ||
|
|
39ffa16853 | ||
|
|
b59e7669c1 | ||
|
|
21741e111b | ||
|
|
7b9c7bc42b | ||
|
|
affc4530a2 | ||
|
|
485bcdc951 | ||
|
|
878a20df29 | ||
|
|
a28d280b95 | ||
|
|
9f867ad2c5 |
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@@ -2,13 +2,17 @@
|
||||
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "go.mod:"
|
||||
open-pull-requests-limit: 100
|
||||
## Disabled between releases. We reenable it briefly after every
|
||||
## stable release, pull in all changes, and close it again so that
|
||||
## the tree remains more stable during development and the upstream
|
||||
## changes have time to soak before the next release.
|
||||
# - package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
# commit-message:
|
||||
# prefix: "go.mod:"
|
||||
# open-pull-requests-limit: 100
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
@@ -8,11 +8,12 @@ Private WireGuard® networks made easy
|
||||
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
|
||||
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
|
||||
The Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
|
||||
@@ -38,6 +38,9 @@ var (
|
||||
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
|
||||
TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
|
||||
TailscaledSocketSetExplicitly bool
|
||||
|
||||
// TailscaledDialer is the DialContext func that connects to the local machine's
|
||||
// tailscaled or equivalent.
|
||||
TailscaledDialer = defaultDialer
|
||||
@@ -47,7 +50,8 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// TODO: make this part of a safesocket.ConnectionStrategy
|
||||
if !TailscaledSocketSetExplicitly {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
@@ -56,7 +60,11 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
|
||||
// The user provided a non-default tailscaled socket address.
|
||||
// Connect only to exactly what they provided.
|
||||
s.UseFallback(false)
|
||||
return safesocket.Connect(s)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -164,6 +164,11 @@ change in the future.
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
rootfs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "socket" {
|
||||
tailscale.TailscaledSocketSetExplicitly = true
|
||||
}
|
||||
})
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -191,7 +196,8 @@ var rootArgs struct {
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
|
||||
@@ -316,6 +316,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
@@ -332,7 +333,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
goos: "openbsd",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
@@ -546,6 +547,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -65,6 +66,19 @@ func effectiveGOOS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
||||
// a function of the platform it's running on.
|
||||
func acceptRouteDefault(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
@@ -76,7 +90,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
|
||||
@@ -3,6 +3,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -91,7 +92,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
|
||||
@@ -63,6 +63,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -180,7 +181,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
W tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
|
||||
@@ -178,8 +178,7 @@ func main() {
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +300,7 @@ func run() error {
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
@@ -310,8 +309,7 @@ func run() error {
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
@@ -324,7 +322,7 @@ func run() error {
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
if useNetstack {
|
||||
@@ -380,13 +378,11 @@ func run() error {
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
logf("ipnserver.StateStore: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.StateStore: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
@@ -401,8 +397,7 @@ func run() error {
|
||||
err = srv.Run(ctx, ln)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -80,7 +80,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
// Make a logger without a date prefix, as filelogger
|
||||
// and logtail both already add their own. All we really want
|
||||
// from the log package is the automatic newline.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
// We start with log.Default().Writer(), which is the logtail
|
||||
// writer that logpolicy already installed as the global
|
||||
// output.
|
||||
logger := log.New(log.Default().Writer(), "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
@@ -116,6 +119,9 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file logggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
|
||||
1
go.mod
1
go.mod
@@ -19,6 +19,7 @@ require (
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
|
||||
2
go.sum
2
go.sum
@@ -387,6 +387,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
|
||||
@@ -232,32 +232,11 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "android_does_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
Routes: map[string][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
CorpDNS: true,
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4:53"},
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4:53"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
|
||||
// Android had cases where it needed FallbackResolvers. This was the
|
||||
// negative test for the case where Override-local-DNS was set, so the
|
||||
// fallback resolvers did not need to be used. This test is still valid
|
||||
// so we keep it, but the fallback test has been removed.
|
||||
name: "android_does_NOT_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
|
||||
@@ -417,16 +417,9 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr4 string
|
||||
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
|
||||
for _, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
|
||||
if addr.IP().Is4() && tailAddr4 == "" {
|
||||
// The peer struct previously only allowed a single
|
||||
// Tailscale IP address. For compatibility for a few releases starting
|
||||
// with 1.8, keep it pulled out as IPv4-only for a bit.
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
@@ -434,21 +427,20 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
return r.Bits() == 0
|
||||
})
|
||||
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailAddrDeprecated: tailAddr4,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
ExitNodeOption: exitNodeOption,
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
ExitNodeOption: exitNodeOption,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1805,10 +1797,9 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
})
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd", "illumos", "darwin":
|
||||
case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
|
||||
// These are the platforms currently supported by
|
||||
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
|
||||
// TODO(bradfitz): add windows once it's done there.
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: tailcfg.PeerAPIDNS,
|
||||
Port: 1, // version
|
||||
@@ -2091,9 +2082,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
case len(dcfg.Routes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case versionOS == "android":
|
||||
// We don't support split DNS at all on Android yet.
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
}
|
||||
|
||||
return dcfg
|
||||
|
||||
@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
@@ -117,8 +117,8 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/ipn/store/aws"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -869,14 +868,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(args) != 2 && args[0] != "/subproc" {
|
||||
panic(fmt.Sprintf("unexpected arguments %q", args))
|
||||
}
|
||||
logID := args[1]
|
||||
logf = filelogger.New("tailscale-service", logID, logf)
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
|
||||
@@ -33,10 +33,11 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
|
||||
s := safesocket.DefaultConnectionStrategy(socketPath)
|
||||
connect := func() {
|
||||
for i := 1; i <= 2; i++ {
|
||||
logf("connect %d ...", i)
|
||||
c, err := safesocket.Connect(socketPath, 0)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
t.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -88,8 +88,7 @@ type PeerStatus struct {
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
|
||||
// Endpoints:
|
||||
Addrs []string
|
||||
@@ -244,9 +243,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if v := st.UserID; v != 0 {
|
||||
e.UserID = v
|
||||
}
|
||||
if v := st.TailAddrDeprecated; v != "" {
|
||||
e.TailAddrDeprecated = v
|
||||
}
|
||||
if v := st.TailscaleIPs; v != nil {
|
||||
e.TailscaleIPs = v
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package filelogger
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@@ -26,30 +27,30 @@ const (
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// New returns a Writer that appends to local disk log
|
||||
// files on Windows, rotating old log files as needed to stay under
|
||||
// file count & byte limits.
|
||||
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
func New(fileBasePrefix, logID string, inner *log.Logger) io.Writer {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
if inner == nil {
|
||||
panic("nil inner logger")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
inner.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return inner.Writer()
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
inner.Printf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
wrappedLogf: inner.Printf,
|
||||
}
|
||||
return lfw.Logf
|
||||
return logger.FuncWriter(lfw.Logf)
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
|
||||
@@ -525,7 +525,7 @@ func New(collection string) *Policy {
|
||||
}
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
log.SetOutput(maybeWrapForPlatform(lw, cmdName, newc.PublicID.String()))
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
|
||||
16
logpolicy/logpolicy_notwindows.go
Normal file
16
logpolicy/logpolicy_notwindows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package logpolicy
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
|
||||
return lw
|
||||
}
|
||||
26
logpolicy/logpolicy_windows.go
Normal file
26
logpolicy/logpolicy_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package logpolicy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"tailscale.com/log/filelogger"
|
||||
)
|
||||
|
||||
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
|
||||
if cmdName != "tailscaled" {
|
||||
return lw
|
||||
}
|
||||
|
||||
isSvc, err := svc.IsWindowsService()
|
||||
if err != nil || !isSvc {
|
||||
return lw
|
||||
}
|
||||
|
||||
return filelogger.New("tailscale-service", logID, log.New(lw, "", 0))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package dns
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -132,12 +134,20 @@ func isResolvedRunning() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func restartResolved() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
@@ -394,7 +404,12 @@ func (m *directManager) Close() error {
|
||||
}
|
||||
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
m.logf("restarting systemd-resolved...")
|
||||
if err := restartResolved(); err != nil {
|
||||
m.logf("restart of systemd-resolved failed: %v", err)
|
||||
} else {
|
||||
m.logf("restarted systemd-resolved")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -26,8 +26,8 @@ func TestParseIni(t *testing.T) {
|
||||
[network] # trailing comment
|
||||
generateResolvConf = false # trailing comment`,
|
||||
want: map[string]map[string]string{
|
||||
"automount": map[string]string{"enabled": "true", "root": "/mnt/"},
|
||||
"network": map[string]string{"generateResolvConf": "false"},
|
||||
"automount": {"enabled": "true", "root": "/mnt/"},
|
||||
"network": {"generateResolvConf": "false"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -360,7 +360,8 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
||||
case "windows":
|
||||
// TODO: use DnsQueryEx and write to ch.
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsqueryex.
|
||||
return nil, errors.New("TODO: windows exit node suport")
|
||||
// For now just use the net package:
|
||||
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
|
||||
case "darwin":
|
||||
// /etc/resolv.conf is a lie and only says one upstream DNS
|
||||
// but for now that's probably good enough. Later we'll
|
||||
@@ -404,6 +405,106 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
||||
}
|
||||
}
|
||||
|
||||
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
|
||||
// return a reply (for the ExitDNS DoH service) using the net package's
|
||||
// native APIs. This is only used on Windows for now.
|
||||
//
|
||||
// If resolver is nil, the net.Resolver zero value is used.
|
||||
//
|
||||
// response contains the pre-serialized response, which notably
|
||||
// includes the original question and its header.
|
||||
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, resolver *net.Resolver, resp *response) (res []byte, err error) {
|
||||
if resp.Question.Class != dns.ClassINET {
|
||||
return nil, errors.New("unsupported class")
|
||||
}
|
||||
|
||||
r := resolver
|
||||
if r == nil {
|
||||
r = new(net.Resolver)
|
||||
}
|
||||
name := resp.Question.Name.String()
|
||||
|
||||
handleError := func(err error) (res []byte, _ error) {
|
||||
if isGoNoSuchHostError(err) {
|
||||
resp.Header.RCode = dns.RCodeNameError
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
// TODO: map other errors to RCodeServerFailure?
|
||||
// Or I guess our caller should do that?
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Header.RCode = dns.RCodeSuccess // unless changed below
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
network := "ip4"
|
||||
if resp.Question.Type == dns.TypeAAAA {
|
||||
network = "ip6"
|
||||
}
|
||||
ips, err := r.LookupIP(ctx, network, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
for _, stdIP := range ips {
|
||||
if ip, ok := netaddr.FromStdIP(stdIP); ok {
|
||||
resp.IPs = append(resp.IPs, ip)
|
||||
}
|
||||
}
|
||||
case dns.TypeTXT:
|
||||
strs, err := r.LookupTXT(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.TXT = strs
|
||||
case dns.TypePTR:
|
||||
ipStr, ok := unARPA(name)
|
||||
if !ok {
|
||||
// TODO: is this RCodeFormatError?
|
||||
return nil, errors.New("bogus PTR name")
|
||||
}
|
||||
addrs, err := r.LookupAddr(ctx, ipStr)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
resp.Name, _ = dnsname.ToFQDN(addrs[0])
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
cname, err := r.LookupCNAME(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.CNAME = cname
|
||||
case dns.TypeSRV:
|
||||
// Thanks, Go: "To accommodate services publishing SRV
|
||||
// records under non-standard names, if both service
|
||||
// and proto are empty strings, LookupSRV looks up
|
||||
// name directly."
|
||||
_, srvs, err := r.LookupSRV(ctx, "", "", name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.SRVs = srvs
|
||||
case dns.TypeNS:
|
||||
nss, err := r.LookupNS(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.NSs = nss
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
|
||||
}
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
func isGoNoSuchHostError(err error) bool {
|
||||
if de, ok := err.(*net.DNSError); ok {
|
||||
return de.IsNotFound
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type resolvConfCache struct {
|
||||
mod time.Time
|
||||
size int64
|
||||
@@ -604,10 +705,27 @@ func (r *Resolver) handleQuery(pkt packet) {
|
||||
type response struct {
|
||||
Header dns.Header
|
||||
Question dns.Question
|
||||
|
||||
// Name is the response to a PTR query.
|
||||
Name dnsname.FQDN
|
||||
// IP is the response to an A, AAAA, or ALL query.
|
||||
IP netaddr.IP
|
||||
|
||||
// IP and IPs are the responses to an A, AAAA, or ALL query.
|
||||
// Either/both/neither can be populated.
|
||||
IP netaddr.IP
|
||||
IPs []netaddr.IP
|
||||
|
||||
// TXT is the response to a TXT query.
|
||||
// Each one is its own RR with one string.
|
||||
TXT []string
|
||||
|
||||
// CNAME is the response to a CNAME query.
|
||||
CNAME string
|
||||
|
||||
// SRVs are the responses to a SRV query.
|
||||
SRVs []*net.SRV
|
||||
|
||||
// NSs are the responses to an NS query.
|
||||
NSs []*net.NS
|
||||
}
|
||||
|
||||
var dnsParserPool = &sync.Pool{
|
||||
@@ -683,6 +801,16 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
|
||||
return builder.AAAAResource(answerHeader, answer)
|
||||
}
|
||||
|
||||
func marshalIP(name dns.Name, ip netaddr.IP, builder *dns.Builder) error {
|
||||
if ip.Is4() {
|
||||
return marshalARecord(name, ip, builder)
|
||||
}
|
||||
if ip.Is6() {
|
||||
return marshalAAAARecord(name, ip, builder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalPTRRecord serializes a PTR record into an active builder.
|
||||
// The caller may continue using the builder following the call.
|
||||
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
|
||||
@@ -702,6 +830,83 @@ func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builde
|
||||
return builder.PTRResource(answerHeader, answer)
|
||||
}
|
||||
|
||||
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
|
||||
for _, txt := range txts {
|
||||
if err := builder.TXTResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.TXTResource{
|
||||
TXT: []string{txt},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
|
||||
if cname == "" {
|
||||
return nil
|
||||
}
|
||||
name, err := dns.NewName(cname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return builder.CNAMEResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.CNAMEResource{
|
||||
CNAME: name,
|
||||
})
|
||||
}
|
||||
|
||||
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
|
||||
for _, ns := range nss {
|
||||
name, err := dns.NewName(ns.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = builder.NSResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.NSResource{NS: name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
|
||||
for _, s := range srvs {
|
||||
srvName, err := dns.NewName(s.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = builder.SRVResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.SRVResource{
|
||||
Target: srvName,
|
||||
Priority: s.Priority,
|
||||
Port: s.Port,
|
||||
Weight: s.Weight,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalResponse serializes the DNS response into a new buffer.
|
||||
func marshalResponse(resp *response) ([]byte, error) {
|
||||
resp.Header.Response = true
|
||||
@@ -712,6 +917,14 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
builder := dns.NewBuilder(nil, resp.Header)
|
||||
|
||||
// TODO(bradfitz): I'm not sure why this wasn't enabled
|
||||
// before, but for now (2021-12-09) enable it at least when
|
||||
// there's more than 1 record (which was never the case
|
||||
// before), where it really helps.
|
||||
if len(resp.IPs) > 1 {
|
||||
builder.EnableCompression()
|
||||
}
|
||||
|
||||
isSuccess := resp.Header.RCode == dns.RCodeSuccess
|
||||
|
||||
if resp.Question.Type != 0 || isSuccess {
|
||||
@@ -738,13 +951,24 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
if resp.IP.Is4() {
|
||||
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
|
||||
} else if resp.IP.Is6() {
|
||||
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
|
||||
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ip := range resp.IPs {
|
||||
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case dns.TypePTR:
|
||||
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
|
||||
case dns.TypeTXT:
|
||||
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
|
||||
case dns.TypeCNAME:
|
||||
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
|
||||
case dns.TypeSRV:
|
||||
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
|
||||
case dns.TypeNS:
|
||||
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -926,6 +1150,37 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
|
||||
func unARPA(a string) (ipStr string, ok bool) {
|
||||
const suf4 = ".in-addr.arpa."
|
||||
if strings.HasSuffix(a, suf4) {
|
||||
s := strings.TrimSuffix(a, suf4)
|
||||
// Parse and reverse octets.
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil || !ip.Is4() {
|
||||
return "", false
|
||||
}
|
||||
a4 := ip.As4()
|
||||
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
|
||||
}
|
||||
const suf6 = ".ip6.arpa."
|
||||
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
|
||||
strings.HasSuffix(a, suf6) {
|
||||
var hx [32]byte
|
||||
var a16 [16]byte
|
||||
for i := range hx {
|
||||
hx[31-i] = a[i*2]
|
||||
if a[i*2+1] != '.' {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
hex.Decode(a16[:], hx[:])
|
||||
return netaddr.IPFrom16(a16).String(), true
|
||||
}
|
||||
return "", false
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
|
||||
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -179,6 +180,129 @@ var resolveToNXDOMAIN = dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg)
|
||||
w.WriteMsg(m)
|
||||
})
|
||||
|
||||
// weirdoGoCNAMEHandler returns a DNS handler that satisfies
|
||||
// Go's weird Resolver.LookupCNAME (read its godoc carefully!).
|
||||
//
|
||||
// This doesn't even return a CNAME record, because that's not
|
||||
// what Go looks for.
|
||||
func weirdoGoCNAMEHandler(target string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
question := req.Question[0]
|
||||
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
m.Answer = append(m.Answer, &dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: target,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
},
|
||||
Target: target,
|
||||
})
|
||||
case dns.TypeAAAA:
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: target,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
},
|
||||
AAAA: net.ParseIP("1::2"),
|
||||
})
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
// dnsHandler returns a handler that replies with the answers/options
|
||||
// provided.
|
||||
//
|
||||
// Types supported: netaddr.IP.
|
||||
func dnsHandler(answers ...interface{}) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
m.RecursionAvailable = true // to stop net package's errLameReferral on empty replies
|
||||
|
||||
question := req.Question[0]
|
||||
for _, a := range answers {
|
||||
switch a := a.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported dnsHandler arg %T", a))
|
||||
case netaddr.IP:
|
||||
ip := a
|
||||
if ip.Is4() {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ip.IPAddr().IP,
|
||||
})
|
||||
} else if ip.Is6() {
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ip.IPAddr().IP,
|
||||
})
|
||||
}
|
||||
case dns.PTR:
|
||||
ptr := a
|
||||
ptr.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &ptr)
|
||||
case dns.CNAME:
|
||||
c := a
|
||||
c.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
}
|
||||
m.Answer = append(m.Answer, &c)
|
||||
case dns.TXT:
|
||||
txt := a
|
||||
txt.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &txt)
|
||||
case dns.SRV:
|
||||
srv := a
|
||||
srv.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &srv)
|
||||
case dns.NS:
|
||||
rr := a
|
||||
rr.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &rr)
|
||||
}
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
func serveDNS(tb testing.TB, addr string, records ...interface{}) *dns.Server {
|
||||
if len(records)%2 != 0 {
|
||||
panic("must have an even number of record values")
|
||||
|
||||
@@ -6,16 +6,22 @@ package resolver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
miekdns "github.com/miekg/dns"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -35,14 +41,16 @@ var (
|
||||
|
||||
var dnsCfg = Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"test1.ipn.dev.": []netaddr.IP{testipv4},
|
||||
"test2.ipn.dev.": []netaddr.IP{testipv6},
|
||||
"test1.ipn.dev.": {testipv4},
|
||||
"test2.ipn.dev.": {testipv6},
|
||||
},
|
||||
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
|
||||
}
|
||||
|
||||
const noEdns = 0
|
||||
|
||||
const dnsHeaderLen = 12
|
||||
|
||||
func dnspacket(domain dnsname.FQDN, tp dns.Type, ednsSize uint16) []byte {
|
||||
var dnsHeader dns.Header
|
||||
question := dns.Question{
|
||||
@@ -1093,3 +1101,383 @@ func TestForwardLinkSelection(t *testing.T) {
|
||||
type linkSelFunc func(ip netaddr.IP) string
|
||||
|
||||
func (f linkSelFunc) PickLink(ip netaddr.IP) string { return f(ip) }
|
||||
|
||||
func TestHandleExitNodeDNSQueryWithNetPkg(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows; waiting for golang.org/issue/33097")
|
||||
}
|
||||
|
||||
records := []interface{}{
|
||||
"no-records.test.",
|
||||
dnsHandler(),
|
||||
|
||||
"one-a.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1.2.3.4")),
|
||||
|
||||
"two-a.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("5.6.7.8")),
|
||||
|
||||
"one-aaaa.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1::2")),
|
||||
|
||||
"two-aaaa.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1::2"), netaddr.MustParseIP("3::4")),
|
||||
|
||||
"nx-domain.test.",
|
||||
resolveToNXDOMAIN,
|
||||
|
||||
"4.3.2.1.in-addr.arpa.",
|
||||
dnsHandler(miekdns.PTR{Ptr: "foo.com."}),
|
||||
|
||||
"cname.test.",
|
||||
weirdoGoCNAMEHandler("the-target.foo."),
|
||||
|
||||
"txt.test.",
|
||||
dnsHandler(
|
||||
miekdns.TXT{Txt: []string{"txt1=one"}},
|
||||
miekdns.TXT{Txt: []string{"txt2=two"}},
|
||||
miekdns.TXT{Txt: []string{"txt3=three"}},
|
||||
),
|
||||
|
||||
"srv.test.",
|
||||
dnsHandler(
|
||||
miekdns.SRV{
|
||||
Priority: 1,
|
||||
Weight: 2,
|
||||
Port: 3,
|
||||
Target: "foo.com.",
|
||||
},
|
||||
miekdns.SRV{
|
||||
Priority: 4,
|
||||
Weight: 5,
|
||||
Port: 6,
|
||||
Target: "bar.com.",
|
||||
},
|
||||
),
|
||||
|
||||
"ns.test.",
|
||||
dnsHandler(miekdns.NS{Ns: "ns1.foo."}, miekdns.NS{Ns: "ns2.bar."}),
|
||||
}
|
||||
v4server := serveDNS(t, "127.0.0.1:0", records...)
|
||||
defer v4server.Shutdown()
|
||||
|
||||
// backendResolver is the resolver between
|
||||
// handleExitNodeDNSQueryWithNetPkg and its upstream resolver,
|
||||
// which in this test's case is the miekg/dns test DNS server
|
||||
// (v4server).
|
||||
backResolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "udp", v4server.PacketConn.LocalAddr().String())
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("no_such_host", func(t *testing.T) {
|
||||
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
|
||||
Header: dnsmessage.Header{
|
||||
ID: 123,
|
||||
Response: true,
|
||||
OpCode: 0, // query
|
||||
},
|
||||
Question: dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName("nx-domain.test."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res) < dnsHeaderLen {
|
||||
t.Fatal("short reply")
|
||||
}
|
||||
rcode := dns.RCode(res[3] & 0x0f)
|
||||
if rcode != dns.RCodeNameError {
|
||||
t.Errorf("RCode = %v; want dns.RCodeNameError", rcode)
|
||||
t.Logf("Response was: %q", res)
|
||||
}
|
||||
})
|
||||
|
||||
matchPacked := func(want string) func(t testing.TB, got []byte) {
|
||||
return func(t testing.TB, got []byte) {
|
||||
if string(got) == want {
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected reply.\n got: %q\nwant: %q\n", got, want)
|
||||
t.Errorf("\nin hex:\n got: % 2x\nwant: % 2x\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Type dnsmessage.Type
|
||||
Name string
|
||||
Check func(t testing.TB, got []byte)
|
||||
}{
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "one-a.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05one-a\x04test\x00\x00\x01\x00\x01\x05one-a\x04test\x00\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "two-a.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x05two-a\x04test\x00\x00\x01\x00\x01\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x05\x06\a\b"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "one-aaaa.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\bone-aaaa\x04test\x00\x00\x1c\x00\x01\bone-aaaa\x04test\x00\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "two-aaaa.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\btwo-aaaa\x04test\x00\x00\x1c\x00\x01\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypePTR,
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x00\x00\x02X\x00\t\x03foo\x03com\x00"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Name: "cname.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05cname\x04test\x00\x00\x05\x00\x01\x05cname\x04test\x00\x00\x05\x00\x01\x00\x00\x02X\x00\x10\nthe-target\x03foo\x00"),
|
||||
},
|
||||
|
||||
// No records of various types
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x01\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x1c\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x05\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeSRV,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00!\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeTXT,
|
||||
Name: "txt.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x03\x00\x00\x00\x00\x03txt\x04test\x00\x00\x10\x00\x01\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt1=one\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt2=two\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\v\ntxt3=three"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeSRV,
|
||||
Name: "srv.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x03srv\x04test\x00\x00!\x00\x01\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x01\x00\x02\x00\x03\x03foo\x03com\x00\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x04\x00\x05\x00\x06\x03bar\x03com\x00"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeNS,
|
||||
Name: "ns.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x02ns\x04test\x00\x00\x02\x00\x01\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns1\x03foo\x00\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns2\x03bar\x00"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%v_%v", tt.Type, strings.Trim(tt.Name, ".")), func(t *testing.T) {
|
||||
got, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
|
||||
Header: dnsmessage.Header{
|
||||
ID: 123,
|
||||
Response: true,
|
||||
OpCode: 0, // query
|
||||
},
|
||||
Question: dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName(tt.Name),
|
||||
Type: tt.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) < dnsHeaderLen {
|
||||
t.Errorf("short record")
|
||||
}
|
||||
if tt.Check != nil {
|
||||
tt.Check(t, got)
|
||||
if t.Failed() {
|
||||
t.Errorf("Got: %q\nIn hex: % 02x", got, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wrapRes := newWrapResolver(backResolver)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("wrap_ip_a", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "two-a.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ips, []net.IP{
|
||||
net.ParseIP("1.2.3.4").To4(),
|
||||
net.ParseIP("5.6.7.8").To4(),
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupIP = %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ip_aaaa", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "two-aaaa.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ips, []net.IP{
|
||||
net.ParseIP("1::2"),
|
||||
net.ParseIP("3::4"),
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupIP(v6) = %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ip_nx", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "nx-domain.test.")
|
||||
if !isGoNoSuchHostError(err) {
|
||||
t.Errorf("no NX domain = (%v, %v); want no host error", ips, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_srv", func(t *testing.T) {
|
||||
_, srvs, err := wrapRes.LookupSRV(ctx, "", "", "srv.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := srvs, []*net.SRV{
|
||||
{
|
||||
Target: "foo.com.",
|
||||
Priority: 1,
|
||||
Weight: 2,
|
||||
Port: 3,
|
||||
},
|
||||
{
|
||||
Target: "bar.com.",
|
||||
Priority: 4,
|
||||
Weight: 5,
|
||||
Port: 6,
|
||||
},
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
jgot, _ := json.Marshal(got)
|
||||
jwant, _ := json.Marshal(want)
|
||||
t.Errorf("SRV = %s; want %s", jgot, jwant)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_txt", func(t *testing.T) {
|
||||
txts, err := wrapRes.LookupTXT(ctx, "txt.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := txts, []string{"txt1=one", "txt2=two", "txt3=three"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("TXT = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ns", func(t *testing.T) {
|
||||
nss, err := wrapRes.LookupNS(ctx, "ns.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := nss, []*net.NS{
|
||||
{Host: "ns1.foo."},
|
||||
{Host: "ns2.bar."},
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
jgot, _ := json.Marshal(got)
|
||||
jwant, _ := json.Marshal(want)
|
||||
t.Errorf("NS = %s; want %s", jgot, jwant)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// newWrapResolver returns a resolver that uses r (via handleExitNodeDNSQueryWithNetPkg)
|
||||
// to make DNS requests.
|
||||
func newWrapResolver(r *net.Resolver) *net.Resolver {
|
||||
if runtime.GOOS == "windows" {
|
||||
panic("doesn't work on Windows") // golang.org/issue/33097
|
||||
}
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return &wrapResolverConn{ctx: ctx, r: r}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type wrapResolverConn struct {
|
||||
ctx context.Context
|
||||
r *net.Resolver
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
var _ net.PacketConn = (*wrapResolverConn)(nil)
|
||||
|
||||
func (*wrapResolverConn) Close() error { return nil }
|
||||
func (*wrapResolverConn) LocalAddr() net.Addr { return fakeAddr{} }
|
||||
func (*wrapResolverConn) RemoteAddr() net.Addr { return fakeAddr{} }
|
||||
func (*wrapResolverConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (*wrapResolverConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (*wrapResolverConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (a *wrapResolverConn) Read(p []byte) (n int, err error) {
|
||||
n, _, err = a.ReadFrom(p)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
n, err = a.buf.Read(p)
|
||||
return n, fakeAddr{}, err
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) Write(packet []byte) (n int, err error) {
|
||||
return a.WriteTo(packet, fakeAddr{})
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) WriteTo(q []byte, _ net.Addr) (n int, err error) {
|
||||
resp := parseExitNodeQuery(q)
|
||||
if resp == nil {
|
||||
return 0, errors.New("bad query")
|
||||
}
|
||||
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), a.r, resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
a.buf.Write(res)
|
||||
return len(q), nil
|
||||
}
|
||||
|
||||
type fakeAddr struct{}
|
||||
|
||||
func (fakeAddr) Network() string { return "unused" }
|
||||
func (fakeAddr) String() string { return "unused-todoAddr" }
|
||||
|
||||
func TestUnARPA(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"bad", ""},
|
||||
{"4.4.8.8.in-addr.arpa.", "8.8.4.4"},
|
||||
{".in-addr.arpa.", ""},
|
||||
{"e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.", "2607:f8b0:400a:80b::200e"},
|
||||
{".ip6.arpa.", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, ok := unARPA(tt.in)
|
||||
if ok != (got != "") {
|
||||
t.Errorf("inconsistent results for %q: (%q, %v)", tt.in, got, ok)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("unARPA(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
314
net/dnscache/messagecache.go
Normal file
314
net/dnscache/messagecache.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dnscache
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
// MessageCache is a cache that works at the DNS message layer,
|
||||
// with its cache keyed on a DNS wire-level question, and capable
|
||||
// of replying to DNS messages.
|
||||
//
|
||||
// Its zero value is ready for use with a default cache size.
|
||||
// Use SetMaxCacheSize to specify the cache size.
|
||||
//
|
||||
// It's safe for concurrent use.
|
||||
type MessageCache struct {
|
||||
// Clock is a clock, for testing.
|
||||
// If nil, time.Now is used.
|
||||
Clock func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
cacheSizeSet int // 0 means default
|
||||
cache lru.Cache // msgQ => *msgCacheValue
|
||||
}
|
||||
|
||||
func (c *MessageCache) now() time.Time {
|
||||
if c.Clock != nil {
|
||||
return c.Clock()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// SetMaxCacheSize sets the maximum number of DNS cache entries that
|
||||
// can be stored.
|
||||
func (c *MessageCache) SetMaxCacheSize(n int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cacheSizeSet = n
|
||||
c.pruneLocked()
|
||||
}
|
||||
|
||||
// Flush clears the cache.
|
||||
func (c *MessageCache) Flush() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cache.Clear()
|
||||
}
|
||||
|
||||
// pruneLocked prunes down the cache size to the configured (or
|
||||
// default) max size.
|
||||
func (c *MessageCache) pruneLocked() {
|
||||
max := c.cacheSizeSet
|
||||
if max == 0 {
|
||||
max = 500
|
||||
}
|
||||
for c.cache.Len() > max {
|
||||
c.cache.RemoveOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// msgQ is the MessageCache cache key.
|
||||
//
|
||||
// It's basically a golang.org/x/net/dns/dnsmessage#Question but the
|
||||
// Class is omitted (we only cache ClassINET) and we store a Go string
|
||||
// instead of a 256 byte dnsmessage.Name array.
|
||||
type msgQ struct {
|
||||
Name string
|
||||
Type dnsmessage.Type // A, AAAA, MX, etc
|
||||
}
|
||||
|
||||
// A *msgCacheValue is the cached value for a msgQ (question) key.
|
||||
//
|
||||
// Despite using pointers for storage and methods, the value is
|
||||
// immutable once placed in the cache.
|
||||
type msgCacheValue struct {
|
||||
Expires time.Time
|
||||
|
||||
// Answers are the minimum data to reconstruct a DNS response
|
||||
// message. TTLs are added later when converting to a
|
||||
// dnsmessage.Resource.
|
||||
Answers []msgResource
|
||||
}
|
||||
|
||||
type msgResource struct {
|
||||
Name string
|
||||
Type dnsmessage.Type // dnsmessage.UnknownResource.Type
|
||||
Data []byte // dnsmessage.UnknownResource.Data
|
||||
}
|
||||
|
||||
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
|
||||
// when the request can not be satisified from cache.
|
||||
var ErrCacheMiss = errors.New("cache miss")
|
||||
|
||||
var parserPool = &sync.Pool{
|
||||
New: func() interface{} { return new(dnsmessage.Parser) },
|
||||
}
|
||||
|
||||
// ReplyFromCache writes a DNS reply to w for the provided DNS query message,
|
||||
// which must begin with the two ID bytes of a DNS message.
|
||||
//
|
||||
// If there's a cache miss, the message is invalid or unexpected,
|
||||
// ErrCacheMiss is returned. On cache hit, either nil or an error from
|
||||
// a w.Write call is returned.
|
||||
func (c *MessageCache) ReplyFromCache(w io.Writer, dnsQueryMessage []byte) error {
|
||||
cacheKey, txID, ok := getDNSQueryCacheKey(dnsQueryMessage)
|
||||
if !ok {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
now := c.now()
|
||||
|
||||
c.mu.Lock()
|
||||
cacheEntI, _ := c.cache.Get(cacheKey)
|
||||
v, ok := cacheEntI.(*msgCacheValue)
|
||||
if ok && now.After(v.Expires) {
|
||||
c.cache.Remove(cacheKey)
|
||||
ok = false
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
|
||||
ttl := uint32(v.Expires.Sub(now).Seconds())
|
||||
|
||||
packedRes, err := packDNSResponse(cacheKey, txID, ttl, v.Answers)
|
||||
if err != nil {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
_, err = w.Write(packedRes)
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
errNotCacheable = errors.New("question not cacheable")
|
||||
)
|
||||
|
||||
// AddCacheEntry adds a cache entry to the cache.
|
||||
// It returns an error if the entry could not be cached.
|
||||
func (c *MessageCache) AddCacheEntry(qPacket, res []byte) error {
|
||||
cacheKey, qID, ok := getDNSQueryCacheKey(qPacket)
|
||||
if !ok {
|
||||
return errNotCacheable
|
||||
}
|
||||
now := c.now()
|
||||
v := &msgCacheValue{}
|
||||
|
||||
p := parserPool.Get().(*dnsmessage.Parser)
|
||||
defer parserPool.Put(p)
|
||||
|
||||
resh, err := p.Start(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading header in response: %w", err)
|
||||
}
|
||||
if resh.ID != qID {
|
||||
return fmt.Errorf("response ID doesn't match query ID")
|
||||
}
|
||||
q, err := p.Question()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading 1st question in response: %w", err)
|
||||
}
|
||||
if _, err := p.Question(); err != dnsmessage.ErrSectionDone {
|
||||
if err == nil {
|
||||
return errors.New("unexpected 2nd question in response")
|
||||
}
|
||||
return fmt.Errorf("after reading 1st question in response: %w", err)
|
||||
}
|
||||
if resName := asciiLowerName(q.Name).String(); resName != cacheKey.Name {
|
||||
return fmt.Errorf("response question name %q != question name %q", resName, cacheKey.Name)
|
||||
}
|
||||
for {
|
||||
rh, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading answer: %w", err)
|
||||
}
|
||||
res, err := p.UnknownResource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading resource: %w", err)
|
||||
}
|
||||
if rh.Class != dnsmessage.ClassINET {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set the cache entry's expiration to the soonest
|
||||
// we've seen. (They should all be the same, though)
|
||||
expires := now.Add(time.Duration(rh.TTL) * time.Second)
|
||||
if v.Expires.IsZero() || expires.Before(v.Expires) {
|
||||
v.Expires = expires
|
||||
}
|
||||
v.Answers = append(v.Answers, msgResource{
|
||||
Name: rh.Name.String(),
|
||||
Type: rh.Type,
|
||||
Data: res.Data, // doesn't alias; a copy from dnsmessage.unpackUnknownResource
|
||||
})
|
||||
}
|
||||
c.addCacheValue(cacheKey, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MessageCache) addCacheValue(cacheKey msgQ, v *msgCacheValue) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cache.Add(cacheKey, v)
|
||||
c.pruneLocked()
|
||||
}
|
||||
|
||||
func getDNSQueryCacheKey(msg []byte) (cacheKey msgQ, txID uint16, ok bool) {
|
||||
p := parserPool.Get().(*dnsmessage.Parser)
|
||||
defer parserPool.Put(p)
|
||||
h, err := p.Start(msg)
|
||||
const dnsHeaderSize = 12
|
||||
if err != nil || h.OpCode != 0 || h.Response || h.Truncated ||
|
||||
len(msg) < dnsHeaderSize { // p.Start checks this anyway, but to be explicit for slicing below
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
var (
|
||||
numQ = binary.BigEndian.Uint16(msg[4:6])
|
||||
numAns = binary.BigEndian.Uint16(msg[6:8])
|
||||
numAuth = binary.BigEndian.Uint16(msg[8:10])
|
||||
numAddn = binary.BigEndian.Uint16(msg[10:12])
|
||||
)
|
||||
_ = numAddn // ignore this for now; do client OSes send EDNS additional? assume so, ignore.
|
||||
if !(numQ == 1 && numAns == 0 && numAuth == 0) {
|
||||
// Something weird. We don't want to deal with it.
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
q, err := p.Question()
|
||||
if err != nil {
|
||||
// Already verified numQ == 1 so shouldn't happen, but:
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
if q.Class != dnsmessage.ClassINET {
|
||||
// We only cache the Internet class.
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
return msgQ{Name: asciiLowerName(q.Name).String(), Type: q.Type}, h.ID, true
|
||||
}
|
||||
|
||||
func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
|
||||
nb := n.Data[:]
|
||||
if int(n.Length) < len(n.Data) {
|
||||
nb = nb[:n.Length]
|
||||
}
|
||||
for i, b := range nb {
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
n.Data[i] += 0x20
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// packDNSResponse builds a DNS response for the given question and
|
||||
// transaction ID. The response resource records will have have the
|
||||
// same provided TTL.
|
||||
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
|
||||
var baseMem []byte // TODO: guess a max size based on looping over answers?
|
||||
b := dnsmessage.NewBuilder(baseMem, dnsmessage.Header{
|
||||
ID: txID,
|
||||
Response: true,
|
||||
OpCode: 0,
|
||||
Authoritative: false,
|
||||
Truncated: false,
|
||||
RCode: dnsmessage.RCodeSuccess,
|
||||
})
|
||||
name, err := dnsmessage.NewName(q.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.Question(dnsmessage.Question{
|
||||
Name: name,
|
||||
Type: q.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range answers {
|
||||
name, err := dnsmessage.NewName(r.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.UnknownResource(dnsmessage.ResourceHeader{
|
||||
Name: name,
|
||||
Type: r.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: ttl,
|
||||
}, dnsmessage.UnknownResource{
|
||||
Type: r.Type,
|
||||
Data: r.Data,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b.Finish()
|
||||
}
|
||||
292
net/dnscache/messagecache_test.go
Normal file
292
net/dnscache/messagecache_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dnscache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestMessageCache(t *testing.T) {
|
||||
clock := &tstest.Clock{
|
||||
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
mc := &MessageCache{Clock: clock.Now}
|
||||
mc.SetMaxCacheSize(2)
|
||||
clock.Advance(time.Second)
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := mc.ReplyFromCache(&out, makeQ(1, "foo.com.")); err != ErrCacheMiss {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.AddCacheEntry(
|
||||
makeQ(2, "foo.com."),
|
||||
makeRes(2, "FOO.COM.", ttlOpt(10),
|
||||
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}},
|
||||
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Expect cache hit, with 10 seconds remaining.
|
||||
out.Reset()
|
||||
if err := mc.ReplyFromCache(&out, makeQ(3, "foo.com.")); err != nil {
|
||||
t.Fatalf("expected cache hit; got: %v", err)
|
||||
}
|
||||
if p := mustParseResponse(t, out.Bytes()); p.TxID != 3 {
|
||||
t.Errorf("TxID = %v; want %v", p.TxID, 3)
|
||||
} else if p.TTL != 10 {
|
||||
t.Errorf("TTL = %v; want 10", p.TTL)
|
||||
}
|
||||
|
||||
// One second elapses, expect a cache hit, with 9 seconds
|
||||
// remaining.
|
||||
clock.Advance(time.Second)
|
||||
out.Reset()
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.")); err != nil {
|
||||
t.Fatalf("expected cache hit; got: %v", err)
|
||||
}
|
||||
if p := mustParseResponse(t, out.Bytes()); p.TxID != 4 {
|
||||
t.Errorf("TxID = %v; want %v", p.TxID, 4)
|
||||
} else if p.TTL != 9 {
|
||||
t.Errorf("TTL = %v; want 9", p.TTL)
|
||||
}
|
||||
|
||||
// Expect cache miss on MX record.
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.TypeMX)); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss on MX; got: %v", err)
|
||||
}
|
||||
// Expect cache miss on CHAOS class.
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.ClassCHAOS)); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss on CHAOS; got: %v", err)
|
||||
}
|
||||
|
||||
// Ten seconds elapses; expect a cache miss.
|
||||
clock.Advance(10 * time.Second)
|
||||
if err := mc.ReplyFromCache(&out, makeQ(5, "foo.com.")); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type parsedMeta struct {
|
||||
TxID uint16
|
||||
TTL uint32
|
||||
}
|
||||
|
||||
func mustParseResponse(t testing.TB, r []byte) (ret parsedMeta) {
|
||||
t.Helper()
|
||||
var p dnsmessage.Parser
|
||||
h, err := p.Start(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ret.TxID = h.ID
|
||||
qq, err := p.AllQuestions()
|
||||
if err != nil {
|
||||
t.Fatalf("AllQuestions: %v", err)
|
||||
}
|
||||
if len(qq) != 1 {
|
||||
t.Fatalf("num questions = %v; want 1", len(qq))
|
||||
}
|
||||
aa, err := p.AllAnswers()
|
||||
if err != nil {
|
||||
t.Fatalf("AllAnswers: %v", err)
|
||||
}
|
||||
for _, r := range aa {
|
||||
if ret.TTL == 0 {
|
||||
ret.TTL = r.Header.TTL
|
||||
}
|
||||
if ret.TTL != r.Header.TTL {
|
||||
t.Fatal("mixed TTLs")
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type responseOpt bool
|
||||
|
||||
type ttlOpt uint32
|
||||
|
||||
func makeQ(txID uint16, name string, opt ...interface{}) []byte {
|
||||
opt = append(opt, responseOpt(false))
|
||||
return makeDNSPkt(txID, name, opt...)
|
||||
}
|
||||
|
||||
func makeRes(txID uint16, name string, opt ...interface{}) []byte {
|
||||
opt = append(opt, responseOpt(true))
|
||||
return makeDNSPkt(txID, name, opt...)
|
||||
}
|
||||
|
||||
func makeDNSPkt(txID uint16, name string, opt ...interface{}) []byte {
|
||||
typ := dnsmessage.TypeA
|
||||
class := dnsmessage.ClassINET
|
||||
var response bool
|
||||
var answers []dnsmessage.ResourceBody
|
||||
var ttl uint32 = 1 // one second by default
|
||||
for _, o := range opt {
|
||||
switch o := o.(type) {
|
||||
case dnsmessage.Type:
|
||||
typ = o
|
||||
case dnsmessage.Class:
|
||||
class = o
|
||||
case responseOpt:
|
||||
response = bool(o)
|
||||
case dnsmessage.ResourceBody:
|
||||
answers = append(answers, o)
|
||||
case ttlOpt:
|
||||
ttl = uint32(o)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown opt type %T", o))
|
||||
}
|
||||
}
|
||||
qname := dnsmessage.MustNewName(name)
|
||||
msg := dnsmessage.Message{
|
||||
Header: dnsmessage.Header{ID: txID, Response: response},
|
||||
Questions: []dnsmessage.Question{
|
||||
{
|
||||
Name: qname,
|
||||
Type: typ,
|
||||
Class: class,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, rb := range answers {
|
||||
msg.Answers = append(msg.Answers, dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{
|
||||
Name: qname,
|
||||
Type: typ,
|
||||
Class: class,
|
||||
TTL: ttl,
|
||||
},
|
||||
Body: rb,
|
||||
})
|
||||
}
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func TestASCIILowerName(t *testing.T) {
|
||||
n := asciiLowerName(dnsmessage.MustNewName("Foo.COM."))
|
||||
if got, want := n.String(), "foo.com."; got != want {
|
||||
t.Errorf("got = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDNSQueryCacheKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkt []byte
|
||||
want msgQ
|
||||
txID uint16
|
||||
anyTX bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "a",
|
||||
pkt: makeQ(123, "foo.com."),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeA},
|
||||
txID: 123,
|
||||
},
|
||||
{
|
||||
name: "aaaa",
|
||||
pkt: makeQ(6, "foo.com.", dnsmessage.TypeAAAA),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeAAAA},
|
||||
txID: 6,
|
||||
},
|
||||
{
|
||||
name: "normalize_case",
|
||||
pkt: makeQ(123, "FoO.CoM."),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeA},
|
||||
txID: 123,
|
||||
},
|
||||
{
|
||||
name: "ignore_response",
|
||||
pkt: makeRes(123, "foo.com."),
|
||||
},
|
||||
{
|
||||
name: "ignore_question_with_answers",
|
||||
pkt: makeQ(2, "foo.com.", &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}),
|
||||
},
|
||||
{
|
||||
name: "whatever_go_generates", // in case Go's net package grows functionality we don't handle
|
||||
pkt: getGoNetPacketDNSQuery("from-go.foo."),
|
||||
want: msgQ{"from-go.foo.", dnsmessage.TypeA},
|
||||
anyTX: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, gotTX, ok := getDNSQueryCacheKey(tt.pkt)
|
||||
if !ok {
|
||||
if tt.txID == 0 && got == (msgQ{}) {
|
||||
return
|
||||
}
|
||||
t.Fatal("failed")
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %+v, want %+v", got, tt.want)
|
||||
}
|
||||
if gotTX != tt.txID && !tt.anyTX {
|
||||
t.Errorf("got tx %v, want %v", gotTX, tt.txID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getGoNetPacketDNSQuery(name string) []byte {
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, Go's net.Resolver doesn't use the DNS client.
|
||||
// See https://github.com/golang/go/issues/33097 which
|
||||
// was approved but not yet implemented.
|
||||
// For now just pretend it's implemented to make this test
|
||||
// pass on Windows with complicated the caller.
|
||||
return makeQ(123, name)
|
||||
}
|
||||
res := make(chan []byte, 1)
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return goResolverConn(res), nil
|
||||
},
|
||||
}
|
||||
r.LookupIP(context.Background(), "ip4", name)
|
||||
return <-res
|
||||
}
|
||||
|
||||
type goResolverConn chan<- []byte
|
||||
|
||||
func (goResolverConn) Close() error { return nil }
|
||||
func (goResolverConn) LocalAddr() net.Addr { return todoAddr{} }
|
||||
func (goResolverConn) RemoteAddr() net.Addr { return todoAddr{} }
|
||||
func (goResolverConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) Read([]byte) (int, error) { return 0, errors.New("boom") }
|
||||
func (c goResolverConn) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case c <- p[2:]: // skip 2 byte length for TCP mode DNS query
|
||||
default:
|
||||
}
|
||||
return 0, errors.New("boom")
|
||||
}
|
||||
|
||||
type todoAddr struct{}
|
||||
|
||||
func (todoAddr) Network() string { return "unused" }
|
||||
func (todoAddr) String() string { return "unused-todoAddr" }
|
||||
@@ -39,6 +39,16 @@ type Header interface {
|
||||
Marshal(buf []byte) error
|
||||
}
|
||||
|
||||
// HeaderChecksummer is implemented by Header implementations that
|
||||
// need to do a checksum over their paylods.
|
||||
type HeaderChecksummer interface {
|
||||
Header
|
||||
|
||||
// WriteCheck writes the correct checksum into buf, which should
|
||||
// be be the already-marshalled header and payload.
|
||||
WriteChecksum(buf []byte)
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given Header and
|
||||
// payload. This function allocates memory, see Header.Marshal for an
|
||||
// allocation-free option.
|
||||
@@ -49,5 +59,9 @@ func Generate(h Header, payload []byte) []byte {
|
||||
copy(buf[hlen:], payload)
|
||||
h.Marshal(buf)
|
||||
|
||||
if hc, ok := h.(HeaderChecksummer); ok {
|
||||
hc.WriteChecksum(buf)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
@@ -42,3 +48,120 @@ type ICMP6Code uint8
|
||||
const (
|
||||
ICMP6NoCode ICMP6Code = 0
|
||||
)
|
||||
|
||||
// ICMP6Header is an IPv4+ICMPv4 header.
|
||||
type ICMP6Header struct {
|
||||
IP6Header
|
||||
Type ICMP6Type
|
||||
Code ICMP6Code
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h ICMP6Header) Len() int {
|
||||
return h.IP6Header.Len() + icmp6HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h ICMP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = ipproto.ICMPv6
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
const o = ip6HeaderLength // start offset of ICMPv6 header
|
||||
buf[o+0] = uint8(h.Type)
|
||||
buf[o+1] = uint8(h.Code)
|
||||
buf[o+2] = 0 // checksum, to be filled in later
|
||||
buf[o+3] = 0 // checksum, to be filled in later
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header. TODO: it doesn't implement it
|
||||
// correctly, instead it statically generates an ICMP Echo Reply
|
||||
// packet.
|
||||
func (h *ICMP6Header) ToResponse() {
|
||||
// TODO: this doesn't implement ToResponse correctly, as it
|
||||
// assumes the ICMP request type.
|
||||
h.Type = ICMP6EchoReply
|
||||
h.Code = ICMP6NoCode
|
||||
h.IP6Header.ToResponse()
|
||||
}
|
||||
|
||||
// WriteChecksum implements HeaderChecksummer, writing just the checksum bytes
|
||||
// into the otherwise fully marshaled ICMP6 packet p (which should include the
|
||||
// IPv6 header, ICMPv6 header, and payload).
|
||||
func (h ICMP6Header) WriteChecksum(p []byte) {
|
||||
const payOff = ip6HeaderLength + icmp6HeaderLength
|
||||
xsum := icmp6Checksum(p[ip6HeaderLength:payOff], h.Src.As16(), h.Dst.As16(), p[payOff:])
|
||||
binary.BigEndian.PutUint16(p[ip6HeaderLength+2:], xsum)
|
||||
}
|
||||
|
||||
// Adapted from gVisor:
|
||||
|
||||
// icmp6Checksum calculates the ICMP checksum over the provided ICMPv6
|
||||
// header (without the IPv6 header), IPv6 src/dst addresses and the
|
||||
// payload.
|
||||
//
|
||||
// The header's existing checksum must be zeroed.
|
||||
func icmp6Checksum(header []byte, src, dst [16]byte, payload []byte) uint16 {
|
||||
// Calculate the IPv6 pseudo-header upper-layer checksum.
|
||||
xsum := checksumBytes(src[:], 0)
|
||||
xsum = checksumBytes(dst[:], xsum)
|
||||
|
||||
var scratch [4]byte
|
||||
binary.BigEndian.PutUint32(scratch[:], uint32(len(header)+len(payload)))
|
||||
xsum = checksumBytes(scratch[:], xsum)
|
||||
xsum = checksumBytes(append(scratch[:0], 0, 0, 0, uint8(ipproto.ICMPv6)), xsum)
|
||||
xsum = checksumBytes(payload, xsum)
|
||||
|
||||
var hdrz [icmp6HeaderLength]byte
|
||||
copy(hdrz[:], header)
|
||||
// Zero out the header.
|
||||
hdrz[2] = 0
|
||||
hdrz[3] = 0
|
||||
xsum = ^checksumBytes(hdrz[:], xsum)
|
||||
return xsum
|
||||
}
|
||||
|
||||
// checksumCombine combines the two uint16 to form their
|
||||
// checksum. This is done by adding them and the carry.
|
||||
//
|
||||
// Note that checksum a must have been computed on an even number of
|
||||
// bytes.
|
||||
func checksumCombine(a, b uint16) uint16 {
|
||||
v := uint32(a) + uint32(b)
|
||||
return uint16(v + v>>16)
|
||||
}
|
||||
|
||||
// checksumBytes calculates the checksum (as defined in RFC 1071) of
|
||||
// the bytes in buf.
|
||||
//
|
||||
// The initial checksum must have been computed on an even number of bytes.
|
||||
func checksumBytes(buf []byte, initial uint16) uint16 {
|
||||
v := uint32(initial)
|
||||
|
||||
odd := len(buf)%2 == 1
|
||||
if odd {
|
||||
v += uint32(buf[0])
|
||||
buf = buf[1:]
|
||||
}
|
||||
|
||||
n := len(buf)
|
||||
odd = n&1 != 0
|
||||
if odd {
|
||||
n--
|
||||
v += uint32(buf[n]) << 8
|
||||
}
|
||||
|
||||
for i := 0; i < n; i += 2 {
|
||||
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
|
||||
}
|
||||
|
||||
return checksumCombine(uint16(v), uint16(v>>16))
|
||||
}
|
||||
|
||||
80
net/packet/icmp6_test.go
Normal file
80
net/packet/icmp6_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
func TestICMPv6PingResponse(t *testing.T) {
|
||||
pingHdr := ICMP6Header{
|
||||
IP6Header: IP6Header{
|
||||
Src: netaddr.MustParseIP("1::1"),
|
||||
Dst: netaddr.MustParseIP("2::2"),
|
||||
IPProto: ipproto.ICMPv6,
|
||||
},
|
||||
Type: ICMP6EchoRequest,
|
||||
Code: ICMP6NoCode,
|
||||
}
|
||||
|
||||
// echoReqLen is 2 bytes identifier + 2 bytes seq number.
|
||||
// https://datatracker.ietf.org/doc/html/rfc4443#section-4.1
|
||||
// Packet.IsEchoRequest verifies that these 4 bytes are present.
|
||||
const echoReqLen = 4
|
||||
buf := make([]byte, pingHdr.Len()+echoReqLen)
|
||||
if err := pingHdr.Marshal(buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var p Parsed
|
||||
p.Decode(buf)
|
||||
if !p.IsEchoRequest() {
|
||||
t.Fatalf("not an echo request, got: %+v", p)
|
||||
}
|
||||
|
||||
pingHdr.ToResponse()
|
||||
buf = make([]byte, pingHdr.Len()+echoReqLen)
|
||||
if err := pingHdr.Marshal(buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Decode(buf)
|
||||
if p.IsEchoRequest() {
|
||||
t.Fatalf("unexpectedly still an echo request: %+v", p)
|
||||
}
|
||||
if !p.IsEchoResponse() {
|
||||
t.Fatalf("not an echo response: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestICMPv6Checksum(t *testing.T) {
|
||||
const req = "\x60\x0f\x07\x00\x00\x10\x3a\x40\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
|
||||
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
|
||||
"\x00\x00\x00\x00\x00\x00\x20\x0e\x80\x00\x4a\x9a\x2e\xea\x00\x02" +
|
||||
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
|
||||
// The packet that we'd originally generated incorrectly, but with the checksum
|
||||
// bytes fixed per WireShark's correct calculation:
|
||||
const wantRes = "\x60\x00\xf8\xff\x00\x10\x3a\x40\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
|
||||
"\x00\x00\x00\x00\x00\x00\x20\x0e\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
|
||||
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x81\x00\x49\x9a\x2e\xea\x00\x02" +
|
||||
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
|
||||
|
||||
var p Parsed
|
||||
p.Decode([]byte(req))
|
||||
if !p.IsEchoRequest() {
|
||||
t.Fatalf("not an echo request, got: %+v", p)
|
||||
}
|
||||
|
||||
h := p.ICMP6Header()
|
||||
h.ToResponse()
|
||||
pong := Generate(&h, p.Payload())
|
||||
|
||||
if string(pong) != wantRes {
|
||||
t.Errorf("wrong packet\n\n got: %x\nwant: %x", pong, wantRes)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (h *IP6Header) ToResponse() {
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums.
|
||||
func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
@@ -72,6 +72,6 @@ func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
buf[36] = 0
|
||||
buf[37] = 0
|
||||
buf[38] = 0
|
||||
buf[39] = 17 // NextProto
|
||||
buf[39] = byte(proto) // NextProto
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (p *Parsed) String() string {
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
// It performs extremely simple packet decoding for basic IPv4 packet types.
|
||||
// It performs extremely simple packet decoding for basic IPv4 and IPv6 packet types.
|
||||
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
|
||||
// and shouldn't need any memory allocation.
|
||||
func (q *Parsed) Decode(b []byte) {
|
||||
@@ -339,9 +339,6 @@ func (q *Parsed) IP6Header() IP6Header {
|
||||
}
|
||||
|
||||
func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return ICMP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
Type: ICMP4Type(q.b[q.subofs+0]),
|
||||
@@ -349,10 +346,15 @@ func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
func (q *Parsed) ICMP6Header() ICMP6Header {
|
||||
return ICMP6Header{
|
||||
IP6Header: q.IP6Header(),
|
||||
Type: ICMP6Type(q.b[q.subofs+0]),
|
||||
Code: ICMP6Code(q.b[q.subofs+1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.Src.Port(),
|
||||
@@ -410,7 +412,7 @@ func (q *Parsed) IsEchoRequest() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *Parsed) IsEchoResponse() bool {
|
||||
switch q.IPProto {
|
||||
case ipproto.ICMPv4:
|
||||
|
||||
@@ -40,7 +40,7 @@ func (h UDP6Header) Marshal(buf []byte) error {
|
||||
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP6Header.marshalPseudo(buf)
|
||||
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
|
||||
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
@@ -13,15 +13,18 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
)
|
||||
|
||||
// dohConn is a net.PacketConn suitable for returning from
|
||||
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
|
||||
// ExitDNS DoH proxy service.
|
||||
type dohConn struct {
|
||||
ctx context.Context
|
||||
baseURL string
|
||||
hc *http.Client // if nil, default is used
|
||||
ctx context.Context
|
||||
baseURL string
|
||||
hc *http.Client // if nil, default is used
|
||||
dnsCache *dnscache.MessageCache
|
||||
|
||||
rbuf bytes.Buffer
|
||||
}
|
||||
@@ -52,6 +55,15 @@ func (c *dohConn) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *dohConn) Write(packet []byte) (n int, err error) {
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
|
||||
if err == nil {
|
||||
// Cache hit.
|
||||
// TODO(bradfitz): add clientmetric
|
||||
return len(packet), nil
|
||||
}
|
||||
c.rbuf.Reset()
|
||||
}
|
||||
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -77,6 +89,9 @@ func (c *dohConn) Write(packet []byte) (n int, err error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
|
||||
}
|
||||
return len(packet), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
@@ -48,7 +50,8 @@ type Dialer struct {
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
linkMon *monitor.Mon
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
|
||||
}
|
||||
|
||||
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
||||
@@ -76,7 +79,16 @@ func (d *Dialer) TUNName() string {
|
||||
func (d *Dialer) SetExitDNSDoH(doh string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.exitDNSDoHBase == doh {
|
||||
return
|
||||
}
|
||||
d.exitDNSDoHBase = doh
|
||||
if doh != "" && d.dnsCache == nil {
|
||||
d.dnsCache = new(dnscache.MessageCache)
|
||||
}
|
||||
if d.dnsCache != nil {
|
||||
d.dnsCache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
|
||||
@@ -149,12 +161,14 @@ func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (net
|
||||
}
|
||||
|
||||
var r net.Resolver
|
||||
if exitDNSDoH != "" {
|
||||
if exitDNSDoH != "" && runtime.GOOS != "windows" { // Windows: https://github.com/golang/go/issues/33097
|
||||
r.PreferGo = true
|
||||
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return &dohConn{
|
||||
ctx: ctx,
|
||||
baseURL: exitDNSDoH,
|
||||
hc: d.PeerAPIHTTPClient(),
|
||||
ctx: ctx,
|
||||
baseURL: exitDNSDoH,
|
||||
hc: d.PeerAPIHTTPClient(),
|
||||
dnsCache: d.dnsCache,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +119,13 @@ func ensureStateDirPerms(dirPath string) error {
|
||||
// We configure the DACL such that any files or directories created within
|
||||
// dirPath will also inherit this DACL.
|
||||
explicitAccess := []windows.EXPLICIT_ACCESS{
|
||||
windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
userTrustee,
|
||||
},
|
||||
windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestParsePort(t *testing.T) {
|
||||
expect int
|
||||
}
|
||||
tests := []InOut{
|
||||
InOut{"1.2.3.4:5678", 5678},
|
||||
InOut{"0.0.0.0.999", 999},
|
||||
InOut{"1.2.3.4:*", 0},
|
||||
InOut{"5.5.5.5:0", 0},
|
||||
InOut{"[1::2]:5", 5},
|
||||
InOut{"[1::2].5", 5},
|
||||
InOut{"gibberish", -1},
|
||||
{"1.2.3.4:5678", 5678},
|
||||
{"0.0.0.0.999", 999},
|
||||
{"1.2.3.4:*", 0},
|
||||
{"5.5.5.5:0", 0},
|
||||
{"[1::2]:5", 5},
|
||||
{"[1::2].5", 5},
|
||||
{"gibberish", -1},
|
||||
}
|
||||
|
||||
for _, io := range tests {
|
||||
|
||||
@@ -48,7 +48,9 @@ func TestBasics(t *testing.T) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c, err := Connect(sock, port)
|
||||
s := DefaultConnectionStrategy(sock)
|
||||
s.UsePort(port)
|
||||
c, err := Connect(s)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -57,10 +57,65 @@ func tailscaledStillStarting() bool {
|
||||
return tailscaledProcExists()
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
// A ConnectionStrategy is a plan for how to connect to tailscaled or equivalent (e.g. IPNExtension on macOS).
|
||||
type ConnectionStrategy struct {
|
||||
// For now, a ConnectionStrategy is just a unix socket path, a TCP port,
|
||||
// and a flag indicating whether to try fallback connections options.
|
||||
path string
|
||||
port uint16
|
||||
fallback bool
|
||||
// Longer term, a ConnectionStrategy should be an ordered list of things to attempt,
|
||||
// with just the information required to connection for each.
|
||||
//
|
||||
// We have at least these cases to consider (see issue 3530):
|
||||
//
|
||||
// tailscale sandbox | tailscaled sandbox | OS | connection
|
||||
// ------------------|--------------------|---------|-----------
|
||||
// no | no | unix | unix socket
|
||||
// no | no | Windows | TCP/port
|
||||
// no | no | wasm | memconn
|
||||
// no | Network Extension | macOS | TCP/port/token, port/token from lsof
|
||||
// no | System Extension | macOS | TCP/port/token, port/token from lsof
|
||||
// yes | Network Extension | macOS | TCP/port/token, port/token from readdir
|
||||
// yes | System Extension | macOS | TCP/port/token, port/token from readdir
|
||||
//
|
||||
// Note e.g. that port is only relevant as an input to Connect on Windows,
|
||||
// that path is not relevant to Windows, and that neither matters to wasm.
|
||||
}
|
||||
|
||||
// DefaultConnectionStrategy returns a default connection strategy.
|
||||
// The default strategy is to attempt to connect in as many ways as possible.
|
||||
// It uses path as the unix socket path, when applicable,
|
||||
// and defaults to WindowsLocalPort for the TCP port when applicable.
|
||||
// It falls back to auto-discovery across sandbox boundaries on macOS.
|
||||
// TODO: maybe take no arguments, since path is irrelevant on Windows? Discussion in PR 3499.
|
||||
func DefaultConnectionStrategy(path string) *ConnectionStrategy {
|
||||
return &ConnectionStrategy{path: path, port: WindowsLocalPort, fallback: true}
|
||||
}
|
||||
|
||||
// UsePort modifies s to use port for the TCP port when applicable.
|
||||
// UsePort is only applicable on Windows, and only then
|
||||
// when not using the default for Windows.
|
||||
func (s *ConnectionStrategy) UsePort(port uint16) {
|
||||
s.port = port
|
||||
}
|
||||
|
||||
// UseFallback modifies s to set whether it should fall back
|
||||
// to connecting to the macOS GUI's tailscaled
|
||||
// if the Unix socket path wasn't reachable.
|
||||
func (s *ConnectionStrategy) UseFallback(b bool) {
|
||||
s.fallback = b
|
||||
}
|
||||
|
||||
// ExactPath returns a connection strategy that only attempts to connect via path.
|
||||
func ExactPath(path string) *ConnectionStrategy {
|
||||
return &ConnectionStrategy{path: path, fallback: false}
|
||||
}
|
||||
|
||||
// Connect connects to tailscaled using s
|
||||
func Connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
for {
|
||||
c, err := connect(path, port)
|
||||
c, err := connect(s)
|
||||
if err != nil && tailscaledStillStarting() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
continue
|
||||
|
||||
@@ -17,6 +17,6 @@ func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error)
|
||||
return ln, 1, err
|
||||
}
|
||||
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
func connect(_ *ConnectionStrategy) (net.Conn, error) {
|
||||
return memconn.Dial("memu", memName)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,5 @@ func init() {
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,19 +23,19 @@ import (
|
||||
)
|
||||
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
if runtime.GOOS == "js" {
|
||||
return nil, errors.New("safesocket.Connect not yet implemented on js/wasm")
|
||||
}
|
||||
if runtime.GOOS == "darwin" && path == "" && port == 0 {
|
||||
if runtime.GOOS == "darwin" && s.fallback && s.path == "" && s.port == 0 {
|
||||
return connectMacOSAppSandbox()
|
||||
}
|
||||
pipe, err := net.Dial("unix", path)
|
||||
pipe, err := net.Dial("unix", s.path)
|
||||
if err != nil {
|
||||
if runtime.GOOS == "darwin" {
|
||||
if runtime.GOOS == "darwin" && s.fallback {
|
||||
extConn, extErr := connectMacOSAppSandbox()
|
||||
if extErr != nil {
|
||||
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", path, err, extErr)
|
||||
return nil, fmt.Errorf("safesocket: failed to connect to %v: %v; failed to connect to Tailscale IPNExtension: %v", s.path, err, extErr)
|
||||
}
|
||||
return extConn, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ main() {
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|pop|neon)
|
||||
ubuntu|pop|neon|zorin|elementary|linuxmint)
|
||||
OS="ubuntu"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
@@ -68,6 +68,23 @@ main() {
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
kali)
|
||||
OS="debian"
|
||||
PACKAGETYPE="apt"
|
||||
YEAR="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
APT_SYSTEMCTL_START=true
|
||||
# Third-party keyrings became the preferred method of
|
||||
# installation in Debian 11 (Bullseye), which Kali switched
|
||||
# to in roughly 2021.x releases
|
||||
if [ "$YEAR" -lt 2021 ]; then
|
||||
# Kali VERSION_ID is "kali-rolling", which isn't distinguishing
|
||||
VERSION="buster"
|
||||
APT_KEY_TYPE="legacy"
|
||||
else
|
||||
VERSION="bullseye"
|
||||
APT_KEY_TYPE="keyring"
|
||||
fi
|
||||
;;
|
||||
centos)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_ID"
|
||||
@@ -94,6 +111,11 @@ main() {
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
rocky)
|
||||
OS="fedora"
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
amzn)
|
||||
OS="amazon-linux"
|
||||
VERSION="$VERSION_ID"
|
||||
@@ -386,6 +408,10 @@ main() {
|
||||
esac
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install tailscale
|
||||
if [ "$APT_SYSTEMCTL_START" = "true" ]; then
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
$SUDO systemctl start tailscaled
|
||||
fi
|
||||
set +x
|
||||
;;
|
||||
yum)
|
||||
@@ -399,7 +425,7 @@ main() {
|
||||
dnf)
|
||||
set -x
|
||||
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO dnf install tailscale
|
||||
$SUDO dnf install -y tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
|
||||
@@ -43,38 +43,68 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// Binaries are the paths to a tailscaled and tailscale binary.
|
||||
// These can be shared by multiple nodes.
|
||||
type Binaries struct {
|
||||
Dir string // temp dir for tailscale & tailscaled
|
||||
Daemon string // tailscaled
|
||||
CLI string // tailscale
|
||||
}
|
||||
|
||||
// BuildTestBinaries builds tailscale and tailscaled, failing the test
|
||||
// if they fail to compile.
|
||||
func BuildTestBinaries(t testing.TB) *Binaries {
|
||||
td := t.TempDir()
|
||||
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
return &Binaries{
|
||||
Dir: td,
|
||||
Daemon: filepath.Join(td, "tailscaled"+exe()),
|
||||
CLI: filepath.Join(td, "tailscale"+exe()),
|
||||
// CleanupBinaries cleans up any resources created by calls to BinaryDir, TailscaleBinary, or TailscaledBinary.
|
||||
// It should be called from TestMain after all tests have completed.
|
||||
func CleanupBinaries() {
|
||||
buildOnce.Do(func() {})
|
||||
if binDir != "" {
|
||||
os.RemoveAll(binDir)
|
||||
}
|
||||
}
|
||||
|
||||
// buildMu limits our use of "go build" to one at a time, so we don't
|
||||
// fight Go's built-in caching trying to do the same build concurrently.
|
||||
var buildMu sync.Mutex
|
||||
// BinaryDir returns a directory containing test tailscale and tailscaled binaries.
|
||||
// If any test calls BinaryDir, there must be a TestMain function that calls
|
||||
// CleanupBinaries after all tests are complete.
|
||||
func BinaryDir(tb testing.TB) string {
|
||||
buildOnce.Do(func() {
|
||||
binDir, buildErr = buildTestBinaries()
|
||||
})
|
||||
if buildErr != nil {
|
||||
tb.Fatal(buildErr)
|
||||
}
|
||||
return binDir
|
||||
}
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
// TailscaleBinary returns the path to the test tailscale binary.
|
||||
// If any test calls TailscaleBinary, there must be a TestMain function that calls
|
||||
// CleanupBinaries after all tests are complete.
|
||||
func TailscaleBinary(tb testing.TB) string {
|
||||
return filepath.Join(BinaryDir(tb), "tailscale"+exe())
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
// TailscaledBinary returns the path to the test tailscaled binary.
|
||||
// If any test calls TailscaleBinary, there must be a TestMain function that calls
|
||||
// CleanupBinaries after all tests are complete.
|
||||
func TailscaledBinary(tb testing.TB) string {
|
||||
return filepath.Join(BinaryDir(tb), "tailscaled"+exe())
|
||||
}
|
||||
|
||||
goBin := findGo(t)
|
||||
var (
|
||||
buildOnce sync.Once
|
||||
buildErr error
|
||||
binDir string
|
||||
)
|
||||
|
||||
// buildTestBinaries builds tailscale and tailscaled.
|
||||
// It returns the dir containing the binaries.
|
||||
func buildTestBinaries() (string, error) {
|
||||
bindir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = build(bindir, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
if err != nil {
|
||||
os.RemoveAll(bindir)
|
||||
return "", err
|
||||
}
|
||||
return bindir, nil
|
||||
}
|
||||
|
||||
func build(outDir string, targets ...string) error {
|
||||
goBin, err := findGo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
@@ -83,7 +113,7 @@ func build(t testing.TB, outDir string, targets ...string) {
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
@@ -92,25 +122,25 @@ func build(t testing.TB, outDir string, targets ...string) {
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
return fmt.Errorf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
func findGo(t testing.TB) string {
|
||||
func findGo() (string, error) {
|
||||
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
|
||||
if fi, err := os.Stat(goBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("failed to find go at %v", goBin)
|
||||
return "", fmt.Errorf("failed to find go at %v", goBin)
|
||||
}
|
||||
t.Fatalf("looking for go binary: %v", err)
|
||||
return "", fmt.Errorf("looking for go binary: %v", err)
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
return "", fmt.Errorf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
return goBin
|
||||
return goBin, nil
|
||||
}
|
||||
|
||||
func exe() string {
|
||||
|
||||
@@ -52,6 +52,7 @@ func TestMain(m *testing.M) {
|
||||
os.Setenv("TS_DISABLE_UPNP", "true")
|
||||
flag.Parse()
|
||||
v := m.Run()
|
||||
CleanupBinaries()
|
||||
if v != 0 {
|
||||
os.Exit(v)
|
||||
}
|
||||
@@ -62,11 +63,9 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
func TestOneNodeUpNoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
@@ -83,9 +82,7 @@ func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
|
||||
func TestOneNodeExpiredKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
@@ -121,12 +118,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
|
||||
func TestCollectPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n := newTestNode(t, env)
|
||||
|
||||
cmd := exec.Command(n.env.Binaries.Daemon, "--cleanup")
|
||||
cmd := exec.Command(env.daemon, "--cleanup")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TS_PLEASE_PANIC=1",
|
||||
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
|
||||
@@ -135,7 +130,7 @@ func TestCollectPanic(t *testing.T) {
|
||||
t.Logf("initial run: %s", got)
|
||||
|
||||
// Now we run it again, and on start, it will upload the logs to logcatcher.
|
||||
cmd = exec.Command(n.env.Binaries.Daemon, "--cleanup")
|
||||
cmd = exec.Command(env.daemon, "--cleanup")
|
||||
cmd.Env = append(os.Environ(), "TS_LOG_TARGET="+n.env.LogCatcherServer.URL)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("cleanup failed: %v: %q", err, out)
|
||||
@@ -154,9 +149,7 @@ func TestCollectPanic(t *testing.T) {
|
||||
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
|
||||
func TestStateSavedOnStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
@@ -190,11 +183,9 @@ func TestStateSavedOnStart(t *testing.T) {
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
func TestOneNodeUp_Auth(t *testing.T) {
|
||||
func TestOneNodeUpAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins, configureControl(func(control *testcontrol.Server) {
|
||||
env := newTestEnv(t, configureControl(func(control *testcontrol.Server) {
|
||||
control.RequireAuth = true
|
||||
}))
|
||||
|
||||
@@ -237,9 +228,7 @@ func TestOneNodeUp_Auth(t *testing.T) {
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
@@ -285,9 +274,7 @@ func TestTwoNodes(t *testing.T) {
|
||||
|
||||
func TestNodeAddressIPFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
|
||||
@@ -313,9 +300,7 @@ func TestNodeAddressIPFields(t *testing.T) {
|
||||
|
||||
func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.StartDaemon(t)
|
||||
|
||||
@@ -369,9 +354,7 @@ func TestAddPingRequest(t *testing.T) {
|
||||
// be connected to control.
|
||||
func TestNoControlConnWhenDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
@@ -412,9 +395,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
|
||||
// without the GUI to kick off a Start.
|
||||
func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := BuildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.upFlagGOOS = "windows"
|
||||
|
||||
@@ -431,8 +412,9 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
// testEnv contains the test environment (set of servers) used by one
|
||||
// or more nodes.
|
||||
type testEnv struct {
|
||||
t testing.TB
|
||||
Binaries *Binaries
|
||||
t testing.TB
|
||||
cli string
|
||||
daemon string
|
||||
|
||||
LogCatcher *LogCatcher
|
||||
LogCatcherServer *httptest.Server
|
||||
@@ -456,7 +438,7 @@ func (f configureControl) modifyTestEnv(te *testEnv) {
|
||||
|
||||
// newTestEnv starts a bunch of services and returns a new test environment.
|
||||
// newTestEnv arranges for the environment's resources to be cleaned up on exit.
|
||||
func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
|
||||
func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
@@ -469,7 +451,8 @@ func newTestEnv(t testing.TB, bins *Binaries, opts ...testEnvOpt) *testEnv {
|
||||
trafficTrap := new(trafficTrap)
|
||||
e := &testEnv{
|
||||
t: t,
|
||||
Binaries: bins,
|
||||
cli: TailscaleBinary(t),
|
||||
daemon: TailscaledBinary(t),
|
||||
LogCatcher: logc,
|
||||
LogCatcherServer: httptest.NewServer(logc),
|
||||
Control: control,
|
||||
@@ -666,7 +649,7 @@ func (n *testNode) StartDaemon(t testing.TB) *Daemon {
|
||||
}
|
||||
|
||||
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
|
||||
cmd := exec.Command(n.env.Binaries.Daemon,
|
||||
cmd := exec.Command(n.env.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
@@ -700,8 +683,11 @@ func (n *testNode) MustUp(extraArgs ...string) {
|
||||
"--login-server=" + n.env.ControlServer.URL,
|
||||
}
|
||||
args = append(args, extraArgs...)
|
||||
t.Logf("Running %v ...", args)
|
||||
if b, err := n.Tailscale(args...).CombinedOutput(); err != nil {
|
||||
cmd := n.Tailscale(args...)
|
||||
t.Logf("Running %v ...", cmd)
|
||||
cmd.Stdout = nil // in case --verbose-tailscale was set
|
||||
cmd.Stderr = nil // in case --verbose-tailscale was set
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("up: %v, %v", string(b), err)
|
||||
}
|
||||
}
|
||||
@@ -717,8 +703,10 @@ func (n *testNode) MustDown() {
|
||||
// AwaitListening waits for the tailscaled to be serving local clients
|
||||
// over its localhost IPC mechanism. (Unix socket, etc)
|
||||
func (n *testNode) AwaitListening(t testing.TB) {
|
||||
s := safesocket.DefaultConnectionStrategy(n.sockFile)
|
||||
s.UseFallback(false) // connect only to the tailscaled that we started
|
||||
if err := tstest.WaitFor(20*time.Second, func() (err error) {
|
||||
c, err := safesocket.Connect(n.sockFile, safesocket.WindowsLocalPort)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -805,7 +793,7 @@ func (n *testNode) AwaitNeedsLogin(t testing.TB) {
|
||||
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
|
||||
// It does not start the process.
|
||||
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
cmd := exec.Command(n.env.Binaries.CLI, "--socket="+n.sockFile)
|
||||
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
|
||||
cmd.Args = append(cmd.Args, arg...)
|
||||
cmd.Dir = n.dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
|
||||
@@ -35,7 +35,9 @@ import (
|
||||
type Harness struct {
|
||||
testerDialer proxy.Dialer
|
||||
testerDir string
|
||||
bins *integration.Binaries
|
||||
binaryDir string
|
||||
cli string
|
||||
daemon string
|
||||
pubKey string
|
||||
signer ssh.Signer
|
||||
cs *testcontrol.Server
|
||||
@@ -134,11 +136,11 @@ func newHarness(t *testing.T) *Harness {
|
||||
loginServer := fmt.Sprintf("http://%s", ln.Addr())
|
||||
t.Logf("loginServer: %s", loginServer)
|
||||
|
||||
bins := integration.BuildTestBinaries(t)
|
||||
|
||||
h := &Harness{
|
||||
pubKey: string(pubkey),
|
||||
bins: bins,
|
||||
binaryDir: integration.BinaryDir(t),
|
||||
cli: integration.TailscaleBinary(t),
|
||||
daemon: integration.TailscaledBinary(t),
|
||||
signer: signer,
|
||||
loginServerURL: loginServer,
|
||||
cs: cs,
|
||||
@@ -146,7 +148,7 @@ func newHarness(t *testing.T) *Harness {
|
||||
ipMap: ipMap,
|
||||
}
|
||||
|
||||
h.makeTestNode(t, bins, loginServer)
|
||||
h.makeTestNode(t, loginServer)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -156,7 +158,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
|
||||
|
||||
args = append([]string{"--socket=" + filepath.Join(h.testerDir, "sock")}, args...)
|
||||
|
||||
cmd := exec.Command(h.bins.CLI, args...)
|
||||
cmd := exec.Command(h.cli, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -169,7 +171,7 @@ func (h *Harness) Tailscale(t *testing.T, args ...string) []byte {
|
||||
// enables us to make connections to and from the tailscale network being
|
||||
// tested. This mutates the Harness to allow tests to dial into the tailscale
|
||||
// network as well as control the tester's tailscaled.
|
||||
func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, controlURL string) {
|
||||
func (h *Harness) makeTestNode(t *testing.T, controlURL string) {
|
||||
dir := t.TempDir()
|
||||
h.testerDir = dir
|
||||
|
||||
@@ -179,7 +181,7 @@ func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, control
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
bins.Daemon,
|
||||
h.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+filepath.Join(dir, "state.json"),
|
||||
"--socket="+filepath.Join(dir, "sock"),
|
||||
@@ -222,7 +224,7 @@ outer:
|
||||
}
|
||||
}
|
||||
|
||||
run(t, dir, bins.CLI,
|
||||
run(t, dir, h.cli,
|
||||
"--socket="+filepath.Join(dir, "sock"),
|
||||
"up",
|
||||
"--login-server="+controlURL,
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -153,15 +152,15 @@ in {
|
||||
systemd.services.tailscaled.environment."TS_LOG_TARGET" = "{{.LogTarget}}";
|
||||
}`
|
||||
|
||||
func copyUnit(t *testing.T, bins *integration.Binaries) {
|
||||
func (h *Harness) copyUnit(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile("../../../cmd/tailscaled/tailscaled.service")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.MkdirAll(filepath.Join(bins.Dir, "systemd"), 0755)
|
||||
err = os.WriteFile(filepath.Join(bins.Dir, "systemd", "tailscaled.service"), data, 0666)
|
||||
os.MkdirAll(filepath.Join(h.binaryDir, "systemd"), 0755)
|
||||
err = os.WriteFile(filepath.Join(h.binaryDir, "systemd", "tailscaled.service"), data, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -172,7 +171,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
|
||||
t.Skip("https://github.com/NixOS/nixpkgs/issues/131098")
|
||||
}
|
||||
|
||||
copyUnit(t, h.bins)
|
||||
h.copyUnit(t)
|
||||
dir := t.TempDir()
|
||||
fname := filepath.Join(dir, d.Name+".nix")
|
||||
fout, err := os.Create(fname)
|
||||
@@ -185,7 +184,7 @@ func (h *Harness) makeNixOSImage(t *testing.T, d Distro, cdir string) string {
|
||||
BinPath string
|
||||
LogTarget string
|
||||
}{
|
||||
BinPath: h.bins.Dir,
|
||||
BinPath: h.binaryDir,
|
||||
LogTarget: h.loginServerURL,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -290,7 +290,6 @@ func checkCachedImageHash(t *testing.T, d Distro, cacheDir string) string {
|
||||
}
|
||||
|
||||
func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
|
||||
bins := h.bins
|
||||
if strings.HasPrefix(d.Name, "nixos") {
|
||||
return
|
||||
}
|
||||
@@ -305,8 +304,8 @@ func (h *Harness) copyBinaries(t *testing.T, d Distro, conn *ssh.Client) {
|
||||
mkdir(t, cli, "/etc/default")
|
||||
mkdir(t, cli, "/var/lib/tailscale")
|
||||
|
||||
copyFile(t, cli, bins.Daemon, "/usr/sbin/tailscaled")
|
||||
copyFile(t, cli, bins.CLI, "/usr/bin/tailscale")
|
||||
copyFile(t, cli, h.daemon, "/usr/sbin/tailscaled")
|
||||
copyFile(t, cli, h.cli, "/usr/bin/tailscale")
|
||||
|
||||
// TODO(Xe): revisit this assumption before it breaks the test.
|
||||
copyFile(t, cli, "../../../cmd/tailscaled/tailscaled.defaults", "/etc/default/tailscaled")
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"golang.org/x/sync/semaphore"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -52,6 +53,13 @@ var (
|
||||
}()
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
v := m.Run()
|
||||
integration.CleanupBinaries()
|
||||
os.Exit(v)
|
||||
}
|
||||
|
||||
func TestDownloadImages(t *testing.T) {
|
||||
if !*runVMTests {
|
||||
t.Skip("not running integration tests (need --run-vm-tests)")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// This file exists just so go mod tidy won't remove
|
||||
// staticcheck's module from our go.mod.
|
||||
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package tstest
|
||||
|
||||
@@ -173,7 +173,7 @@ func getVal() []interface{} {
|
||||
&tailcfg.MapResponse{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: &tailcfg.DERPRegion{
|
||||
1: {
|
||||
RegionID: 1,
|
||||
RegionCode: "foo",
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package wf
|
||||
|
||||
@@ -3097,7 +3097,6 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var tailAddr4 string
|
||||
var tailscaleIPs []netaddr.IP
|
||||
if c.netMap != nil {
|
||||
tailscaleIPs = make([]netaddr.IP, 0, len(c.netMap.Addresses))
|
||||
@@ -3106,13 +3105,6 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
continue
|
||||
}
|
||||
sb.AddTailscaleIP(addr.IP())
|
||||
// TailAddr previously only allowed for a
|
||||
// single Tailscale IP. For compatibility for
|
||||
// a couple releases starting with 1.8, keep
|
||||
// that field pulled out separately.
|
||||
if addr.IP().Is4() {
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
@@ -3135,7 +3127,6 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
}
|
||||
ss.TailscaleIPs = tailscaleIPs
|
||||
ss.TailAddrDeprecated = tailAddr4
|
||||
})
|
||||
|
||||
c.peerMap.forEachEndpoint(func(ep *endpoint) {
|
||||
|
||||
@@ -401,14 +401,14 @@ func TestPickDERPFallback(t *testing.T) {
|
||||
c := newConn()
|
||||
dm := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: &tailcfg.DERPRegion{},
|
||||
2: &tailcfg.DERPRegion{},
|
||||
3: &tailcfg.DERPRegion{},
|
||||
4: &tailcfg.DERPRegion{},
|
||||
5: &tailcfg.DERPRegion{},
|
||||
6: &tailcfg.DERPRegion{},
|
||||
7: &tailcfg.DERPRegion{},
|
||||
8: &tailcfg.DERPRegion{},
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
4: {},
|
||||
5: {},
|
||||
6: {},
|
||||
7: {},
|
||||
8: {},
|
||||
},
|
||||
}
|
||||
c.derpMap = dm
|
||||
@@ -1011,7 +1011,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
PrivateKey: m1.privateKey,
|
||||
Addresses: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.0.0.1/32")},
|
||||
Peers: []wgcfg.Peer{
|
||||
wgcfg.Peer{
|
||||
{
|
||||
PublicKey: m2.privateKey.Public(),
|
||||
DiscoKey: m2.conn.DiscoPublicKey(),
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.0.0.2/32")},
|
||||
@@ -1023,7 +1023,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
PrivateKey: m2.privateKey,
|
||||
Addresses: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.0.0.2/32")},
|
||||
Peers: []wgcfg.Peer{
|
||||
wgcfg.Peer{
|
||||
{
|
||||
PublicKey: m1.privateKey.Public(),
|
||||
DiscoKey: m1.conn.DiscoPublicKey(),
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.0.0.1/32")},
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -35,6 +37,7 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -377,11 +380,69 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var userPingSem = syncs.NewSemaphore(20) // 20 child ping processes at once
|
||||
|
||||
// userPing tried to ping dstIP and if it succeeds, injects pingResPkt
|
||||
// into the tundev.
|
||||
//
|
||||
// It's used in userspace/netstack mode when we don't have kernel
|
||||
// support or raw socket access. As such, this does the dumbest thing
|
||||
// that can work: runs the ping command. It's not super efficient, so
|
||||
// it bounds the number of pings going on at once. The idea is that
|
||||
// people only use ping occasionally to see if their internet's working
|
||||
// so this doesn't need to be great.
|
||||
//
|
||||
// TODO(bradfitz): when we're running on Windows as the system user, use
|
||||
// raw socket APIs instead of ping child processes.
|
||||
func (ns *Impl) userPing(dstIP netaddr.IP, pingResPkt []byte) {
|
||||
if !userPingSem.TryAcquire() {
|
||||
return
|
||||
}
|
||||
defer userPingSem.Release()
|
||||
|
||||
t0 := time.Now()
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = exec.Command("ping", "-n", "1", "-w", "3000", dstIP.String()).Run()
|
||||
default:
|
||||
err = exec.Command("ping", "-c", "1", "-W", "3", dstIP.String()).Run()
|
||||
}
|
||||
d := time.Since(t0)
|
||||
if err != nil {
|
||||
ns.logf("exec ping of %v failed in %v", dstIP, d)
|
||||
return
|
||||
}
|
||||
if debugNetstack {
|
||||
ns.logf("exec pinged %v in %v", dstIP, time.Since(t0))
|
||||
}
|
||||
if err := ns.tundev.InjectOutbound(pingResPkt); err != nil {
|
||||
ns.logf("InjectOutbound ping response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
if !ns.shouldProcessInbound(p, t) {
|
||||
// Let the host network stack (if any) deal with it.
|
||||
return filter.Accept
|
||||
}
|
||||
|
||||
destIP := p.Dst.IP()
|
||||
if p.IsEchoRequest() && ns.ProcessSubnets && !tsaddr.IsTailscaleIP(destIP) {
|
||||
var pong []byte // the reply to the ping, if our relayed ping works
|
||||
if destIP.Is4() {
|
||||
h := p.ICMP4Header()
|
||||
h.ToResponse()
|
||||
pong = packet.Generate(&h, p.Payload())
|
||||
} else if destIP.Is6() {
|
||||
h := p.ICMP6Header()
|
||||
h.ToResponse()
|
||||
pong = packet.Generate(&h, p.Payload())
|
||||
}
|
||||
go ns.userPing(destIP, pong)
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
var pn tcpip.NetworkProtocolNumber
|
||||
switch p.IPVersion {
|
||||
case 4:
|
||||
|
||||
@@ -18,6 +18,13 @@ type CallbackRouter struct {
|
||||
SetBoth func(rcfg *Config, dcfg *dns.OSConfig) error
|
||||
SplitDNS bool
|
||||
|
||||
// GetBaseConfigFunc optionally specifies a function to return the current DNS
|
||||
// config in response to GetBaseConfig.
|
||||
//
|
||||
// If nil, reading the current config isn't supported and GetBaseConfig()
|
||||
// will return ErrGetBaseConfigNotSupported.
|
||||
GetBaseConfigFunc func() (dns.OSConfig, error)
|
||||
|
||||
mu sync.Mutex // protects all the following
|
||||
rcfg *Config // last applied router config
|
||||
dcfg *dns.OSConfig // last applied DNS config
|
||||
@@ -50,7 +57,10 @@ func (r *CallbackRouter) SupportsSplitDNS() bool {
|
||||
}
|
||||
|
||||
func (r *CallbackRouter) GetBaseConfig() (dns.OSConfig, error) {
|
||||
return dns.OSConfig{}, dns.ErrGetBaseConfigNotSupported
|
||||
if r.GetBaseConfigFunc == nil {
|
||||
return dns.OSConfig{}, dns.ErrGetBaseConfigNotSupported
|
||||
}
|
||||
return r.GetBaseConfigFunc()
|
||||
}
|
||||
|
||||
func (r *CallbackRouter) Close() error {
|
||||
|
||||
@@ -183,7 +183,10 @@ func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
|
||||
args = append([]string{"advfirewall", "firewall"}, args...)
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
err := cmd.Run()
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %v", err, string(b))
|
||||
}
|
||||
return time.Since(t0).Round(time.Millisecond), err
|
||||
}
|
||||
|
||||
|
||||
@@ -1199,7 +1199,7 @@ func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) {
|
||||
// suspend/resume or whenever NetworkManager is started, it
|
||||
// nukes all systemd-resolved configs. So reapply our DNS
|
||||
// config on major link change.
|
||||
if runtime.GOOS == "linux" && changed {
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "android") && changed {
|
||||
e.wgLock.Lock()
|
||||
dnsCfg := e.lastDNSConfig
|
||||
e.wgLock.Unlock()
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestUserspaceEngineReconfig(t *testing.T) {
|
||||
} {
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
&tailcfg.Node{
|
||||
{
|
||||
Key: nkFromHex(nodeHex),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winnet
|
||||
|
||||
Reference in New Issue
Block a user