diff --git a/cmd/derper/mesh.go b/cmd/derper/mesh.go index 670d3f5e8..b442d1b36 100644 --- a/cmd/derper/mesh.go +++ b/cmd/derper/mesh.go @@ -5,6 +5,7 @@ package main import ( + "context" "errors" "fmt" "log" @@ -40,6 +41,6 @@ func startMeshWithHost(s *derp.Server, host string) error { c.MeshKey = s.MeshKey() add := func(k key.Public) { s.AddPacketForwarder(k, c) } remove := func(k key.Public) { s.RemovePacketForwarder(k, c) } - go c.RunWatchConnectionLoop(s.PublicKey(), add, remove) + go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove) return nil } diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index bd4145144..c31ccb703 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -16,6 +16,8 @@ import ( "net" "net/http" "net/url" + "os" + "strconv" "strings" "tailscale.com/safesocket" @@ -25,10 +27,24 @@ import ( var ( httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none") httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none") + testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server") ) func main() { flag.Parse() + if *testIP != "" { + res, err := whoIs(*testIP) + if err != nil { + log.Fatal(err) + } + e := json.NewEncoder(os.Stdout) + e.SetIndent("", "\t") + e.Encode(res) + return + } + if !devMode() { + tmpl = template.Must(template.New("home").Parse(slurpHTML())) + } http.HandleFunc("/", root) log.Printf("Starting hello server.") @@ -61,13 +77,24 @@ func slurpHTML() string { return string(slurp) } -var tmpl = template.Must(template.New("home").Parse(slurpHTML())) +func devMode() bool { return *httpsAddr == "" && *httpAddr != "" } + +func getTmpl() (*template.Template, error) { + if devMode() { + return template.New("home").Parse(slurpHTML()) + } + return tmpl, nil +} + +var tmpl *template.Template // not used in dev mode, initialized by main after flag parse type tmplData struct { - DisplayName string // "Foo Barberson" - LoginName string // "foo@bar.com" - MachineName string // "imac5k" - IP string // "100.2.3.4" + DisplayName string // "Foo Barberson" + LoginName string // "foo@bar.com" + ProfilePicURL string // "https://..." + MachineName string // "imac5k" + MachineOS string // "Linux" + IP string // "100.2.3.4" } func root(w http.ResponseWriter, r *http.Request) { @@ -88,19 +115,51 @@ func root(w http.ResponseWriter, r *http.Request) { http.Error(w, "no remote addr", 500) return } - who, err := whoIs(ip) + tmpl, err := getTmpl() if err != nil { - log.Printf("whois(%q) error: %v", ip, err) - http.Error(w, "Your Tailscale works, but we failed to look you up.", 500) + w.Header().Set("Content-Type", "text/plain") + http.Error(w, "template error: "+err.Error(), 500) return } + + who, err := whoIs(ip) + var data tmplData + if err != nil { + if devMode() { + log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err) + data = tmplData{ + DisplayName: "Taily Scalerson", + LoginName: "taily@scaler.son", + ProfilePicURL: "https://placekitten.com/200/200", + MachineName: "scaled", + MachineOS: "Linux", + IP: "100.1.2.3", + } + } else { + log.Printf("whois(%q) error: %v", ip, err) + http.Error(w, "Your Tailscale works, but we failed to look you up.", 500) + return + } + } else { + data = tmplData{ + DisplayName: who.UserProfile.DisplayName, + LoginName: who.UserProfile.LoginName, + ProfilePicURL: who.UserProfile.ProfilePicURL, + MachineName: firstLabel(who.Node.ComputedName), + MachineOS: who.Node.Hostinfo.OS, + IP: ip, + } + } w.Header().Set("Content-Type", "text/html; charset=utf-8") - tmpl.Execute(w, tmplData{ - DisplayName: who.UserProfile.DisplayName, - LoginName: who.UserProfile.LoginName, - MachineName: who.Node.ComputedName, - IP: ip, - }) + tmpl.Execute(w, data) +} + +// firstLabel s up until the first period, if any. +func firstLabel(s string) string { + if i := strings.Index(s, "."); i != -1 { + return s[:i] + } + return s } // tsSockClient does HTTP requests to the local Tailscale daemon. @@ -108,13 +167,28 @@ func root(w http.ResponseWriter, r *http.Request) { var tsSockClient = &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // 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. + if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { + var d net.Dialer + return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) + } return safesocket.ConnectDefault() }, }, } func whoIs(ip string) (*tailcfg.WhoIsResponse, error) { - res, err := tsSockClient.Get("http://local-tailscaled.sock/localapi/v0/whois?ip=" + url.QueryEscape(ip)) + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil) + if err != nil { + return nil, err + } + if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { + req.SetBasicAuth("", token) + } + res, err := tsSockClient.Do(req) if err != nil { return nil, err } diff --git a/cmd/hello/hello.tmpl.html b/cmd/hello/hello.tmpl.html index bf43b65ac..ce64a615f 100644 --- a/cmd/hello/hello.tmpl.html +++ b/cmd/hello/hello.tmpl.html @@ -1,17 +1,436 @@ - - - Hello from Tailscale - - -

Hello!

-

- Hello {{.DisplayName}} ({{.LoginName}}) from {{.MachineName}} ({{.IP}}). -

-

- Your Tailscale is working! -

-

- Welcome to Tailscale. -

- + + + + + + Hello from Tailscale + + + + +
+ +
+

You're connected over Tailscale!

+

This device is signed in as…

+
+
+
+ + + +
+
+
+
+ {{ with .DisplayName }} +

{{.}}

+ {{ end }} +
{{.LoginName}}
+
+
+
+
+ + + + + + +

{{.MachineName}}

+
+
{{.IP}}
+
+
+ +
+ diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 623b4a3bd..f8d5e0880 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -68,11 +68,6 @@ change in the future. Exec: func(context.Context, []string) error { return flag.ErrHelp }, } - // Don't advertise the debug command, but it exists. - if strSliceContains(args, "debug") { - rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd) - } - if err := rootCmd.Parse(args); err != nil { return err } @@ -134,12 +129,3 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { bc.GotNotifyMsg(msg) } } - -func strSliceContains(ss []string, s string) bool { - for _, v := range ss { - if v == s { - return true - } - } - return false -} diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index f4f8c78ad..dde138fa2 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -159,13 +159,18 @@ func runStatus(ctx context.Context, args []string) error { relay := ps.Relay anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0 if !active { - if anyTraffic { + if ps.ExitNode { + f("idle; exit node") + } else if anyTraffic { f("idle") } else { f("-") } } else { f("active; ") + if ps.ExitNode { + f("exit node; ") + } if relay != "" && ps.CurAddr == "" { f("relay %q", relay) } else if ps.CurAddr != "" { diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index e60801f9b..f968a1598 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -22,9 +22,9 @@ import ( "inet.af/netaddr" "tailscale.com/ipn" "tailscale.com/tailcfg" + "tailscale.com/types/preftype" "tailscale.com/version" "tailscale.com/version/distro" - "tailscale.com/wgengine/router" ) var upCmd = &ffcli.Command{ @@ -45,6 +45,7 @@ specify any flags, options are reset to their default. upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "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 IP of the exit node for internet traffic") upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections") upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication") upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)") @@ -74,6 +75,7 @@ var upArgs struct { acceptRoutes bool acceptDNS bool singleRoutes bool + exitNodeIP string shieldsUp bool forceReauth bool advertiseRoutes string @@ -138,6 +140,9 @@ func runUp(ctx context.Context, args []string) error { if upArgs.acceptRoutes { return errors.New("--accept-routes is " + notSupported) } + if upArgs.exitNodeIP != "" { + return errors.New("--exit-node is " + notSupported) + } if upArgs.netfilterMode != "off" { return errors.New("--netfilter-mode values besides \"off\" " + notSupported) } @@ -170,6 +175,15 @@ func runUp(ctx context.Context, args []string) error { checkIPForwarding() } + var exitNodeIP netaddr.IP + if upArgs.exitNodeIP != "" { + var err error + exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP) + if err != nil { + fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err) + } + } + var tags []string if upArgs.advertiseTags != "" { tags = strings.Split(upArgs.advertiseTags, ",") @@ -190,6 +204,7 @@ func runUp(ctx context.Context, args []string) error { prefs.ControlURL = upArgs.server prefs.WantRunning = true prefs.RouteAll = upArgs.acceptRoutes + prefs.ExitNodeIP = exitNodeIP prefs.CorpDNS = upArgs.acceptDNS prefs.AllowSingleHosts = upArgs.singleRoutes prefs.ShieldsUp = upArgs.shieldsUp @@ -202,12 +217,12 @@ func runUp(ctx context.Context, args []string) error { if runtime.GOOS == "linux" { switch upArgs.netfilterMode { case "on": - prefs.NetfilterMode = router.NetfilterOn + prefs.NetfilterMode = preftype.NetfilterOn case "nodivert": - prefs.NetfilterMode = router.NetfilterNoDivert + prefs.NetfilterMode = preftype.NetfilterNoDivert warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.") case "off": - prefs.NetfilterMode = router.NetfilterOff + prefs.NetfilterMode = preftype.NetfilterOff warnf("netfilter=off; configure iptables yourself.") default: fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode) diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 559f214db..4fb8d7d6c 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -4,124 +4,83 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole - L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router - LW github.com/go-multierror/multierror from tailscale.com/wgengine/router - 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/wgengine/router/dns - L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor - L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink - L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ - L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ - L github.com/mdlayher/sdnotify from tailscale.com/util/systemd github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli - 💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+ - 💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+ - github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device - 💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device - W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc - github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device - github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device - github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+ - github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+ - 💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+ - W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+ github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli 💣 go4.org/intern from inet.af/netaddr - 💣 go4.org/mem from tailscale.com/control/controlclient+ + 💣 go4.org/mem from tailscale.com/derp+ go4.org/unsafe/assume-no-moving-gc from go4.org/intern W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ inet.af/netaddr from tailscale.com/cmd/tailscale/cli+ rsc.io/goversion/version from tailscale.com/version - tailscale.com/atomicfile from tailscale.com/ipn+ + tailscale.com/atomicfile from tailscale.com/ipn tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale - tailscale.com/control/controlclient from tailscale.com/ipn+ - tailscale.com/derp from tailscale.com/derp/derphttp+ - tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+ + tailscale.com/derp from tailscale.com/derp/derphttp + tailscale.com/derp/derphttp from tailscale.com/net/netcheck tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli - tailscale.com/disco from tailscale.com/derp+ - tailscale.com/internal/deepprint from tailscale.com/ipn+ + tailscale.com/disco from tailscale.com/derp tailscale.com/ipn from tailscale.com/cmd/tailscale/cli tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+ - tailscale.com/ipn/policy from tailscale.com/ipn - tailscale.com/log/logheap from tailscale.com/control/controlclient - tailscale.com/logtail/backoff from tailscale.com/control/controlclient+ tailscale.com/metrics from tailscale.com/derp - tailscale.com/net/dnscache from tailscale.com/control/controlclient+ + tailscale.com/net/dnscache from tailscale.com/derp/derphttp tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+ 💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+ - tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+ - tailscale.com/net/netns from tailscale.com/control/controlclient+ - tailscale.com/net/packet from tailscale.com/wgengine+ - tailscale.com/net/stun from tailscale.com/net/netcheck+ - tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ - tailscale.com/net/tsaddr from tailscale.com/ipn+ - 💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+ - tailscale.com/paths from tailscale.com/cmd/tailscale/cli - tailscale.com/portlist from tailscale.com/ipn + tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli + tailscale.com/net/netns from tailscale.com/derp/derphttp+ + tailscale.com/net/packet from tailscale.com/wgengine/filter + tailscale.com/net/stun from tailscale.com/net/netcheck + tailscale.com/net/tlsdial from tailscale.com/derp/derphttp + tailscale.com/net/tsaddr from tailscale.com/net/interfaces + 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ + tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli 💣 tailscale.com/syncs from tailscale.com/net/interfaces+ tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ W tailscale.com/tsconst from tailscale.com/net/interfaces - tailscale.com/tstime from tailscale.com/wgengine/magicsock - tailscale.com/types/empty from tailscale.com/control/controlclient+ - tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+ + tailscale.com/types/empty from tailscale.com/ipn + tailscale.com/types/key from tailscale.com/derp+ tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+ - tailscale.com/types/nettype from tailscale.com/wgengine/magicsock - tailscale.com/types/opt from tailscale.com/control/controlclient+ + tailscale.com/types/netmap from tailscale.com/ipn + tailscale.com/types/opt from tailscale.com/net/netcheck+ + tailscale.com/types/persist from tailscale.com/ipn + tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+ tailscale.com/types/strbuilder from tailscale.com/net/packet - tailscale.com/types/structs from tailscale.com/control/controlclient+ - tailscale.com/types/wgkey from tailscale.com/control/controlclient+ + tailscale.com/types/structs from tailscale.com/ipn+ + tailscale.com/types/wgkey from tailscale.com/types/netmap+ tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ - LW tailscale.com/util/endian from tailscale.com/net/netns+ - tailscale.com/util/lineread from tailscale.com/control/controlclient+ - tailscale.com/util/systemd from tailscale.com/control/controlclient+ + W tailscale.com/util/endian from tailscale.com/net/netns + tailscale.com/util/lineread from tailscale.com/net/interfaces tailscale.com/version from tailscale.com/cmd/tailscale/cli+ - tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ - tailscale.com/wgengine from tailscale.com/ipn - tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ - tailscale.com/wgengine/magicsock from tailscale.com/wgengine - 💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+ - tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+ - tailscale.com/wgengine/router/dns from tailscale.com/ipn+ - tailscale.com/wgengine/tsdns from tailscale.com/ipn+ - tailscale.com/wgengine/tstun from tailscale.com/wgengine - tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+ - tailscale.com/wgengine/wglog from tailscale.com/wgengine - W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router + tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli + tailscale.com/wgengine/filter from tailscale.com/types/netmap golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box - golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+ golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/chacha20poly1305 from crypto/tls+ golang.org/x/crypto/cryptobyte from crypto/ecdsa+ golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+ golang.org/x/crypto/curve25519 from crypto/tls+ golang.org/x/crypto/hkdf from crypto/tls - golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+ + golang.org/x/crypto/nacl/box from tailscale.com/derp golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box - golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+ + 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/bpf from github.com/mdlayher/netlink+ golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal - 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 golang.org/x/net/idna from golang.org/x/net/http/httpguts+ - golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device - golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+ golang.org/x/net/proxy from tailscale.com/net/netns - D golang.org/x/net/route from net - golang.org/x/oauth2 from tailscale.com/control/controlclient+ + D golang.org/x/net/route from net+ + golang.org/x/oauth2 from tailscale.com/ipn+ golang.org/x/oauth2/internal from golang.org/x/oauth2 golang.org/x/sync/errgroup from tailscale.com/derp golang.org/x/sync/singleflight from tailscale.com/net/dnscache golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ - LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+ + LD golang.org/x/sys/unix from tailscale.com/net/netns+ W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+ - W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+ + W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg golang.org/x/text/secure/bidirule from golang.org/x/net/idna golang.org/x/text/transform from golang.org/x/text/secure/bidirule+ golang.org/x/text/unicode/bidi from golang.org/x/net/idna+ @@ -130,7 +89,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep bufio from compress/flate+ bytes from bufio+ compress/flate from compress/gzip+ - compress/gzip from net/http+ + compress/gzip from net/http compress/zlib from debug/elf+ container/list from crypto/tls+ context from crypto/tls+ @@ -158,7 +117,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep debug/elf from rsc.io/goversion/version debug/macho from rsc.io/goversion/version debug/pe from rsc.io/goversion/version - encoding from encoding/json+ + encoding from encoding/json encoding/asn1 from crypto/x509+ encoding/base64 from encoding/json+ encoding/binary from compress/gzip+ @@ -172,7 +131,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep hash from compress/zlib+ hash/adler32 from compress/zlib hash/crc32 from compress/gzip+ - hash/fnv from tailscale.com/wgengine/magicsock hash/maphash from go4.org/mem html from tailscale.com/ipn/ipnstate io from bufio+ @@ -181,7 +139,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep math from compress/flate+ math/big from crypto/dsa+ math/bits from compress/flate+ - math/rand from github.com/mdlayher/netlink+ + math/rand from math/big+ mime from golang.org/x/oauth2/internal+ mime/multipart from net/http mime/quotedprintable from mime/multipart @@ -192,23 +150,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep net/textproto from golang.org/x/net/http/httpguts+ net/url from crypto/x509+ os from crypto/rand+ - os/exec from github.com/coreos/go-iptables/iptables+ + os/exec from github.com/toqueteos/webbrowser+ os/signal from tailscale.com/cmd/tailscale/cli - L os/user from github.com/godbus/dbus/v5 path from debug/dwarf+ path/filepath from crypto/x509+ reflect from crypto/x509+ - regexp from github.com/coreos/go-iptables/iptables+ + regexp from rsc.io/goversion/version regexp/syntax from regexp runtime/debug from golang.org/x/sync/singleflight - runtime/pprof from tailscale.com/log/logheap+ sort from compress/flate+ strconv from compress/flate+ strings from bufio+ sync from compress/flate+ sync/atomic from context+ syscall from crypto/rand+ - text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+ + text/tabwriter from github.com/peterbourgon/ff/v2/ffcli time from compress/gzip+ unicode from bytes+ unicode/utf16 from encoding/asn1+ diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscaled/debug.go similarity index 86% rename from cmd/tailscale/cli/debug.go rename to cmd/tailscaled/debug.go index 7873a65de..d852ad893 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscaled/debug.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cli +package main import ( "context" @@ -18,7 +18,6 @@ import ( "os" "time" - "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/derp/derphttp" "tailscale.com/derp/derpmap" "tailscale.com/net/interfaces" @@ -28,28 +27,26 @@ import ( "tailscale.com/wgengine/monitor" ) -var debugCmd = &ffcli.Command{ - Name: "debug", - Exec: runDebug, - FlagSet: (func() *flag.FlagSet { - fs := flag.NewFlagSet("debug", flag.ExitOnError) - fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.") - fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.") - fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code") - return fs - })(), -} - var debugArgs struct { monitor bool getURL string derpCheck string } -func runDebug(ctx context.Context, args []string) error { - if len(args) > 0 { - return errors.New("unknown arguments") +var debugModeFunc = debugMode // so it can be addressable + +func debugMode(args []string) error { + fs := flag.NewFlagSet("debug", flag.ExitOnError) + fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.") + fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.") + fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code") + if err := fs.Parse(args); err != nil { + return err } + if len(fs.Args()) > 0 { + return errors.New("unknown non-flag debug subcommand arguments") + } + ctx := context.Background() if debugArgs.derpCheck != "" { return checkDerp(ctx, debugArgs.derpCheck) } diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 77ef9073c..414734d43 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -2,8 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy - github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled - W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router LW github.com/go-multierror/multierror from tailscale.com/wgengine/router W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+ @@ -22,7 +20,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L github.com/mdlayher/sdnotify from tailscale.com/util/systemd 💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+ 💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+ - github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device 💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device @@ -66,17 +63,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+ inet.af/netaddr from tailscale.com/control/controlclient+ + inet.af/peercred from tailscale.com/ipn/ipnserver rsc.io/goversion/version from tailscale.com/version tailscale.com/atomicfile from tailscale.com/ipn+ - tailscale.com/control/controlclient from tailscale.com/ipn+ + tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ tailscale.com/derp from tailscale.com/derp/derphttp+ tailscale.com/derp/derphttp from tailscale.com/net/netcheck+ + tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled tailscale.com/disco from tailscale.com/derp+ - tailscale.com/internal/deepprint from tailscale.com/ipn+ - tailscale.com/ipn from tailscale.com/ipn/ipnserver + tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+ + tailscale.com/ipn from tailscale.com/ipn/ipnserver+ + tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+ tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnstate from tailscale.com/ipn+ - tailscale.com/ipn/policy from tailscale.com/ipn + tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver + tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver tailscale.com/log/logheap from tailscale.com/control/controlclient tailscale.com/logpolicy from tailscale.com/cmd/tailscaled @@ -86,17 +87,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/metrics from tailscale.com/derp tailscale.com/net/dnscache from tailscale.com/control/controlclient+ tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+ - 💣 tailscale.com/net/interfaces from tailscale.com/ipn+ + 💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+ tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock tailscale.com/net/netns from tailscale.com/control/controlclient+ 💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver tailscale.com/net/packet from tailscale.com/wgengine+ tailscale.com/net/stun from tailscale.com/net/netcheck+ tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ - tailscale.com/net/tsaddr from tailscale.com/ipn+ + tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ tailscale.com/paths from tailscale.com/cmd/tailscaled+ - tailscale.com/portlist from tailscale.com/ipn + tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/ipn/ipnserver tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ 💣 tailscale.com/syncs from tailscale.com/net/interfaces+ @@ -107,8 +108,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled tailscale.com/types/key from tailscale.com/derp+ tailscale.com/types/logger from tailscale.com/cmd/tailscaled+ + tailscale.com/types/netmap from tailscale.com/control/controlclient+ tailscale.com/types/nettype from tailscale.com/wgengine/magicsock tailscale.com/types/opt from tailscale.com/control/controlclient+ + tailscale.com/types/persist from tailscale.com/control/controlclient+ + tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/strbuilder from tailscale.com/net/packet tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/wgkey from tailscale.com/control/controlclient+ @@ -123,13 +127,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+ - 💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine + 💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine+ tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+ - tailscale.com/wgengine/router/dns from tailscale.com/ipn+ - tailscale.com/wgengine/tsdns from tailscale.com/ipn+ + tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+ + tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/tstun from tailscale.com/wgengine+ - tailscale.com/wgengine/wgcfg from tailscale.com/control/controlclient+ + tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+ + tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal tailscale.com/wgengine/wglog from tailscale.com/wgengine W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box @@ -154,15 +159,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+ golang.org/x/net/proxy from tailscale.com/net/netns - D golang.org/x/net/route from net + D golang.org/x/net/route from net+ golang.org/x/oauth2 from tailscale.com/control/controlclient+ golang.org/x/oauth2/internal from golang.org/x/oauth2 golang.org/x/sync/errgroup from tailscale.com/derp golang.org/x/sync/singleflight from tailscale.com/net/dnscache golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+ - W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+ + W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+ W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+ + W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled golang.org/x/term from tailscale.com/logpolicy golang.org/x/text/secure/bidirule from golang.org/x/net/idna golang.org/x/text/transform from golang.org/x/text/secure/bidirule+ diff --git a/cmd/tailscaled/install_darwin.go b/cmd/tailscaled/install_darwin.go new file mode 100644 index 000000000..7c86cd985 --- /dev/null +++ b/cmd/tailscaled/install_darwin.go @@ -0,0 +1,142 @@ +// 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 main + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" +) + +func init() { + installSystemDaemon = installSystemDaemonDarwin + uninstallSystemDaemon = uninstallSystemDaemonDarwin +} + +// darwinLaunchdPlist is the launchd.plist that's written to +// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the +// future) a user-specific location. +// +// See man launchd.plist. +const darwinLaunchdPlist = ` + + + + + + Label + com.tailscale.tailscaled + + ProgramArguments + + /usr/local/bin/tailscaled + + + RunAtLoad + + + + +` + +const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist" +const targetBin = "/usr/local/bin/tailscaled" +const service = "com.tailscale.tailscaled" + +func uninstallSystemDaemonDarwin(args []string) (ret error) { + if len(args) > 0 { + return errors.New("uninstall subcommand takes no arguments") + } + + plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output() + _ = plist // parse it? https://github.com/DHowett/go-plist if we need something. + running := err == nil + + if running { + out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput() + if err != nil { + fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out) + ret = err + } + out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput() + if err != nil { + fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out) + if ret == nil { + ret = err + } + } + } + + err = os.Remove(sysPlist) + if os.IsNotExist(err) { + err = nil + if ret == nil { + ret = err + } + } + return ret +} + +func installSystemDaemonDarwin(args []string) (err error) { + if len(args) > 0 { + return errors.New("install subcommand takes no arguments") + } + defer func() { + if err != nil && os.Getuid() != 0 { + err = fmt.Errorf("%w; try running tailscaled with sudo", err) + } + }() + + // Copy ourselves to /usr/local/bin/tailscaled. + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to find our own executable path: %w", err) + } + tmpBin := targetBin + ".tmp" + f, err := os.Create(tmpBin) + if err != nil { + return err + } + self, err := os.Open(exe) + if err != nil { + f.Close() + return err + } + _, err = io.Copy(f, self) + self.Close() + if err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + if err := os.Chmod(tmpBin, 0755); err != nil { + return err + } + if err := os.Rename(tmpBin, targetBin); err != nil { + return err + } + + // Best effort: + uninstallSystemDaemonDarwin(nil) + + if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil { + return err + } + + if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil { + return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out) + } + + if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil { + return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out) + } + + return nil +} diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index a8acf514c..e9691a5dc 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -24,7 +24,6 @@ import ( "syscall" "time" - "github.com/apenwarr/fixconsole" "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" "tailscale.com/paths" @@ -53,6 +52,10 @@ func defaultTunName() string { return "tun" case "windows": return "Tailscale" + case "darwin": + // "utun" is recognized by wireguard-go/tun/tun_darwin.go + // as a magic value that uses/creates any free number. + return "utun" } return "tailscale0" } @@ -68,6 +71,17 @@ var args struct { verbose int } +var ( + installSystemDaemon func([]string) error // non-nil on some platforms + uninstallSystemDaemon func([]string) error // non-nil on some platforms +) + +var subCommands = map[string]*func([]string) error{ + "install-system-daemon": &installSystemDaemon, + "uninstall-system-daemon": &uninstallSystemDaemon, + "debug": &debugModeFunc, +} + func main() { // We aren't very performance sensitive, and the parts that are // performance sensitive (wireguard) try hard not to do any memory @@ -88,9 +102,23 @@ func main() { flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket") flag.BoolVar(&printVersion, "version", false, "print version information and exit") - err := fixconsole.FixConsoleIfNeeded() - if err != nil { - log.Fatalf("fixConsoleOutput: %v", err) + if len(os.Args) > 1 { + sub := os.Args[1] + if fp, ok := subCommands[sub]; ok { + if *fp == nil { + log.SetFlags(0) + log.Fatalf("%s not available on %v", sub, runtime.GOOS) + } + if err := (*fp)(os.Args[2:]); err != nil { + log.SetFlags(0) + log.Fatal(err) + } + return + } + } + + if beWindowsSubprocess() { + return } flag.Parse() @@ -125,6 +153,16 @@ func run() error { pol.Shutdown(ctx) }() + if isWindowsService() { + // Run the IPN server from the Windows service manager. + log.Printf("Running service...") + if err := runWindowsService(pol); err != nil { + log.Printf("runservice: %v", err) + } + log.Printf("Service ended.") + return nil + } + var logf logger.Logf = log.Printf if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v { logf = logger.RusagePrefixLog(logf) diff --git a/cmd/tailscaled/tailscaled_notwindows.go b/cmd/tailscaled/tailscaled_notwindows.go new file mode 100644 index 000000000..58221a2ea --- /dev/null +++ b/cmd/tailscaled/tailscaled_notwindows.go @@ -0,0 +1,15 @@ +// 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. + +// +build !windows + +package main // import "tailscale.com/cmd/tailscaled" + +import "tailscale.com/logpolicy" + +func isWindowsService() bool { return false } + +func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") } + +func beWindowsSubprocess() bool { return false } diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go new file mode 100644 index 000000000..4de9a50e6 --- /dev/null +++ b/cmd/tailscaled/tailscaled_windows.go @@ -0,0 +1,180 @@ +// 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 main // import "tailscale.com/cmd/tailscaled" + +// TODO: check if administrator, like tswin does. +// +// TODO: try to load wintun.dll early at startup, before wireguard/tun +// does (which panics) and if we'd fail (e.g. due to access +// denied, even if administrator), use 'tasklist /m wintun.dll' +// to see if something else is currently using it and tell user. +// +// TODO: check if Tailscale service is already running, and fail early +// like tswin does. +// +// TODO: on failure, check if on a UNC drive and recommend copying it +// to C:\ to run it, like tswin does. + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "tailscale.com/ipn/ipnserver" + "tailscale.com/logpolicy" + "tailscale.com/types/logger" + "tailscale.com/version" + "tailscale.com/wgengine" +) + +const serviceName = "Tailscale" + +func isWindowsService() bool { + v, err := svc.IsWindowsService() + if err != nil { + log.Fatalf("svc.IsWindowsService failed: %v", err) + } + return v +} + +func runWindowsService(pol *logpolicy.Policy) error { + return svc.Run(serviceName, &ipnService{Policy: pol}) +} + +type ipnService struct { + Policy *logpolicy.Policy +} + +// Called by Windows to execute the windows service. +func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + changes <- svc.Status{State: svc.StartPending} + + ctx, cancel := context.WithCancel(context.Background()) + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + args := []string{"/subproc", service.Policy.PublicID.String()} + ipnserver.BabysitProc(ctx, args, log.Printf) + }() + + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop} + + for ctx.Err() == nil { + select { + case <-doneCh: + case cmd := <-r: + switch cmd.Cmd { + case svc.Stop: + cancel() + case svc.Interrogate: + changes <- cmd.CurrentStatus + } + } + } + + changes <- svc.Status{State: svc.StopPending} + return false, windows.NO_ERROR +} + +func beWindowsSubprocess() bool { + if len(os.Args) != 3 || os.Args[1] != "/subproc" { + return false + } + logid := os.Args[2] + + log.Printf("Program starting: v%v: %#v", version.Long, os.Args) + log.Printf("subproc mode: logid=%v", logid) + + go func() { + b := make([]byte, 16) + for { + _, err := os.Stdin.Read(b) + if err != nil { + log.Fatalf("stdin err (parent process died): %v", err) + } + } + }() + + err := startIPNServer(context.Background(), logid) + if err != nil { + log.Fatalf("ipnserver: %v", err) + } + return true +} + +func startIPNServer(ctx context.Context, logid string) error { + var logf logger.Logf = log.Printf + var eng wgengine.Engine + var err error + + getEngine := func() (wgengine.Engine, error) { + eng, err := wgengine.NewUserspaceEngine(logf, "Tailscale", 41641) + if err != nil { + return nil, err + } + return wgengine.NewWatchdog(eng), nil + } + + if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" { + err = fmt.Errorf("pretending to be a service failure: %v", msg) + } else { + // We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors, + // all intermittently. A few times I (Brad) have also seen sporadic failures that simply + // restarting fixed. So try a few times. + for try := 1; try <= 5; try++ { + if try > 1 { + // Only sleep a bit. Don't do some massive backoff because + // the frontend GUI has a 30 second timeout on connecting to us, + // but even 5 seconds is too long for them to get any results. + // 5 tries * 1 second each seems fine. + time.Sleep(time.Second) + } + eng, err = getEngine() + if err != nil { + logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err) + continue + } + if try > 1 { + logf("wgengine.NewUserspaceEngine: ended up working on try %v", try) + } + break + } + } + if err != nil { + // Log the error, but don't fatalf. We want to + // propagate the error message to the UI frontend. So + // we continue and tell the ipnserver to return that + // in a Notify message. + logf("wgengine.NewUserspaceEngine: %v", err) + } + opts := ipnserver.Options{ + Port: 41112, + SurviveDisconnects: false, + StatePath: args.statepath, + } + if err != nil { + // Return nicer errors to users, annotated with logids, which helps + // when they file bugs. + rawGetEngine := getEngine // raw == without verbose logid-containing error + getEngine = func() (wgengine.Engine, error) { + eng, err := rawGetEngine() + if err != nil { + return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid) + } + return eng, nil + } + } else { + getEngine = ipnserver.FixedEngine(eng) + } + err = ipnserver.Run(ctx, logf, logid, getEngine, opts) + if err != nil { + logf("ipnserver.Run: %v", err) + } + return err +} diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 8bbae5c20..128ead9a7 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -22,6 +22,8 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/empty" "tailscale.com/types/logger" + "tailscale.com/types/netmap" + "tailscale.com/types/persist" "tailscale.com/types/structs" "tailscale.com/types/wgkey" ) @@ -68,9 +70,9 @@ type Status struct { LoginFinished *empty.Message Err string URL string - Persist *Persist // locally persisted configuration - NetMap *NetworkMap // server-pushed configuration - Hostinfo *tailcfg.Hostinfo // current Hostinfo data + Persist *persist.Persist // locally persisted configuration + NetMap *netmap.NetworkMap // server-pushed configuration + Hostinfo *tailcfg.Hostinfo // current Hostinfo data State State } @@ -213,7 +215,7 @@ func (c *Client) sendNewMapRequest() { // If we're not already streaming a netmap, or if we're already stuck // in a lite update, then tear down everything and start a new stream // (which starts by sending a new map request) - if !c.inPollNetMap || c.inLiteMapUpdate { + if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn { c.mu.Unlock() c.cancelMapSafely() return @@ -509,7 +511,7 @@ func (c *Client) mapRoutine() { c.inPollNetMap = false c.mu.Unlock() - err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) { + err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) { c.mu.Lock() select { @@ -606,7 +608,7 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) { c.sendNewMapRequest() } -func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) { +func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) { c.mu.Lock() state := c.state loggedIn := c.loggedIn @@ -618,7 +620,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) { c.logf("[v1] sendStatus: %s: %v", who, state) - var p *Persist + var p *persist.Persist var fin *empty.Message if state == StateAuthenticated { fin = new(empty.Message) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index e1aa7958b..1328af09e 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -4,8 +4,6 @@ package controlclient -//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go - import ( "bytes" "context" @@ -22,6 +20,7 @@ import ( "net/url" "os" "os/exec" + "path/filepath" "reflect" "runtime" "sort" @@ -41,70 +40,15 @@ import ( "tailscale.com/net/tshttpproxy" "tailscale.com/tailcfg" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/types/opt" - "tailscale.com/types/structs" + "tailscale.com/types/persist" "tailscale.com/types/wgkey" "tailscale.com/util/systemd" "tailscale.com/version" "tailscale.com/wgengine/filter" ) -type Persist struct { - _ structs.Incomparable - - // LegacyFrontendPrivateMachineKey is here temporarily - // (starting 2020-09-28) during migration of Windows users' - // machine keys from frontend storage to the backend. On the - // first LocalBackend.Start call, the backend will initialize - // the real (backend-owned) machine key from the frontend's - // provided value (if non-zero), picking a new random one if - // needed. This field should be considered read-only from GUI - // frontends. The real value should not be written back in - // this field, lest the frontend persist it to disk. - LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"` - - PrivateNodeKey wgkey.Private - OldPrivateNodeKey wgkey.Private // needed to request key rotation - Provider string - LoginName string -} - -func (p *Persist) Equals(p2 *Persist) bool { - if p == nil && p2 == nil { - return true - } - if p == nil || p2 == nil { - return false - } - - return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) && - p.PrivateNodeKey.Equal(p2.PrivateNodeKey) && - p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) && - p.Provider == p2.Provider && - p.LoginName == p2.LoginName -} - -func (p *Persist) Pretty() string { - var mk, ok, nk wgkey.Key - if !p.LegacyFrontendPrivateMachineKey.IsZero() { - mk = p.LegacyFrontendPrivateMachineKey.Public() - } - if !p.OldPrivateNodeKey.IsZero() { - ok = p.OldPrivateNodeKey.Public() - } - if !p.PrivateNodeKey.IsZero() { - nk = p.PrivateNodeKey.Public() - } - ss := func(k wgkey.Key) string { - if k.IsZero() { - return "" - } - return k.ShortString() - } - return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}", - ss(mk), ss(ok), ss(nk), p.LoginName) -} - // Direct is the client that connects to a tailcontrol server for a node. type Direct struct { httpc *http.Client // HTTP client used to talk to tailcontrol @@ -121,7 +65,7 @@ type Direct struct { mu sync.Mutex // mutex guards the following fields serverKey wgkey.Key - persist Persist + persist persist.Persist authKey string tryingNewKey wgkey.Private expiry *time.Time @@ -133,7 +77,7 @@ type Direct struct { } type Options struct { - Persist Persist // initial persistent data + Persist persist.Persist // initial persistent data MachinePrivateKey wgkey.Private // the machine key to use ServerURL string // URL of the tailcontrol server AuthKey string // optional node auth key for auto registration @@ -229,10 +173,25 @@ func NewHostinfo() *tailcfg.Hostinfo { Hostname: hostname, OS: version.OS(), OSVersion: osv, + Package: packageType(), GoArch: runtime.GOARCH, } } +func packageType() string { + switch runtime.GOOS { + case "windows": + if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil { + return "choco" + } + case "darwin": + // Using tailscaled or IPNExtension? + exe, _ := os.Executable() + return filepath.Base(exe) + } + return "" +} + // SetHostinfo clones the provided Hostinfo and remembers it for the // next update. It reports whether the Hostinfo has changed. func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool { @@ -271,7 +230,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool { return true } -func (c *Direct) GetPersist() Persist { +func (c *Direct) GetPersist() persist.Persist { c.mu.Lock() defer c.mu.Unlock() return c.persist @@ -294,7 +253,7 @@ func (c *Direct) TryLogout(ctx context.Context) error { // immediately invalidated. //if !c.persist.PrivateNodeKey.IsZero() { //} - c.persist = Persist{} + c.persist = persist.Persist{} return nil } @@ -526,7 +485,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil } // // maxPolls is how many network maps to download; common values are 1 // or -1 (to keep a long-poll query open to the server). -func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error { +func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error { return c.sendMapRequest(ctx, maxPolls, cb) } @@ -538,7 +497,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error { } // cb nil means to omit peers. -func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error { +func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error { c.mu.Lock() persist := c.persist serverURL := c.serverURL @@ -550,6 +509,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw everEndpoints := c.everEndpoints c.mu.Unlock() + if persist.PrivateNodeKey.IsZero() { + return errors.New("privateNodeKey is zero") + } if backendLogID == "" { return errors.New("hostinfo: BackendLogID missing") } @@ -769,7 +731,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw localPort = c.localPort c.mu.Unlock() - nm := &NetworkMap{ + nm := &netmap.NetworkMap{ SelfNode: resp.Node, NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()), PrivateKey: persist.PrivateNodeKey, diff --git a/control/controlclient/direct_clone.go b/control/controlclient/direct_clone.go deleted file mode 100644 index 9254d82ea..000000000 --- a/control/controlclient/direct_clone.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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. - -// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT. - -package controlclient - -import () - -// Clone makes a deep copy of Persist. -// The result aliases no memory with the original. -func (src *Persist) Clone() *Persist { - if src == nil { - return nil - } - dst := new(Persist) - *dst = *src - return dst -} diff --git a/control/controlclient/direct_test.go b/control/controlclient/direct_test.go index bb1637845..3dab4d9ec 100644 --- a/control/controlclient/direct_test.go +++ b/control/controlclient/direct_test.go @@ -5,6 +5,7 @@ package controlclient import ( + "encoding/json" "fmt" "reflect" "strings" @@ -156,3 +157,15 @@ func TestNewDirect(t *testing.T) { t.Errorf("c.newEndpoints(13) want true got %v", changed) } } + +func TestNewHostinfo(t *testing.T) { + hi := NewHostinfo() + if hi == nil { + t.Fatal("no Hostinfo") + } + j, err := json.MarshalIndent(hi, " ", "") + if err != nil { + t.Fatal(err) + } + t.Logf("Got: %s", j) +} diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index 87f11eca6..0c523f07e 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -709,10 +709,19 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) { m, err = client.Recv() if err != nil { c.closeForReconnect(client) + if c.isClosed() { + err = ErrClientClosed + } } return m, connGen, err } +func (c *Client) isClosed() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.closed +} + // Close closes the client. It will not automatically reconnect after // being closed. func (c *Client) Close() error { diff --git a/derp/derphttp/mesh_client.go b/derp/derphttp/mesh_client.go index 28f54653e..e53e9ec5d 100644 --- a/derp/derphttp/mesh_client.go +++ b/derp/derphttp/mesh_client.go @@ -5,20 +5,32 @@ package derphttp import ( + "context" "sync" "time" "tailscale.com/derp" "tailscale.com/types/key" + "tailscale.com/types/logger" ) -// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to +// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to // connection changes. // // If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns. // // Otherwise, the add and remove funcs are called as clients come & go. -func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) { +// +// infoLogf, if non-nil, is the logger to write periodic status +// updates about how many peers are on the server. Error log output is +// set to the c's logger, regardless of infoLogf's value. +// +// To force RunWatchConnectionLoop to return quickly, its ctx needs to +// be closed, and c itself needs to be closed. +func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) { + if infoLogf == nil { + infoLogf = logger.Discard + } logf := c.logf const retryInterval = 5 * time.Second const statusInterval = 10 * time.Second @@ -45,7 +57,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove if loggedConnected { return } - logf("connected; %d peers", len(present)) + infoLogf("connected; %d peers", len(present)) loggedConnected = true } @@ -79,12 +91,21 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove } } - for { + sleep := func(d time.Duration) { + t := time.NewTimer(d) + select { + case <-ctx.Done(): + t.Stop() + case <-t.C: + } + } + + for ctx.Err() == nil { err := c.WatchConnectionChanges() if err != nil { clear() logf("WatchConnectionChanges: %v", err) - time.Sleep(retryInterval) + sleep(retryInterval) continue } @@ -97,7 +118,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove if err != nil { clear() logf("Recv: %v", err) - time.Sleep(retryInterval) + sleep(retryInterval) break } if connGen != lastConnGen { @@ -114,9 +135,8 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove } if now := time.Now(); now.Sub(lastStatus) > statusInterval { lastStatus = now - logf("%d peers", len(present)) + infoLogf("%d peers", len(present)) } } } - } diff --git a/go.mod b/go.mod index 4f976363f..a7f3bcbf3 100644 --- a/go.mod +++ b/go.mod @@ -24,15 +24,15 @@ require ( github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 github.com/peterbourgon/ff/v2 v2.0.0 github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 - github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365 + github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 github.com/tcnksm/go-httpstat v0.2.0 github.com/toqueteos/webbrowser v1.2.0 go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 - golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 - golang.org/x/net v0.0.0-20201216054612-986b41b23924 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + golang.org/x/net v0.0.0-20201224014010-6772e930b67b golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 + golang.org/x/sys v0.0.0-20210216224549-f992740a1bac golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 @@ -40,5 +40,6 @@ require ( gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6 honnef.co/go/tools v0.1.0 inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d + inet.af/peercred v0.0.0-20210216231719-993aa01eacaa rsc.io/goversion v1.2.0 ) diff --git a/go.sum b/go.sum index d5becc8b9..2663ff004 100644 --- a/go.sum +++ b/go.sum @@ -300,6 +300,14 @@ github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 h1:7OWHhbjW github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8= github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365 h1:0OC8+fnUCx5ww7uRSlzbcVC6Q/FK0PmVclmimbpWbyk= github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8= +github.com/tailscale/wireguard-go v0.0.0-20210204220812-81c7f3687020 h1:DbQtiKont9TyOBIuTHhj1UUpWE75QcsyBiJPxTbqRGQ= +github.com/tailscale/wireguard-go v0.0.0-20210204220812-81c7f3687020/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924= +github.com/tailscale/wireguard-go v0.0.0-20210209210853-838c6fc0dc12 h1:kk8nOHkXmG/yD1a4FQvH7+VOdNEP7GKkQimXFR2iwv8= +github.com/tailscale/wireguard-go v0.0.0-20210209210853-838c6fc0dc12/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924= +github.com/tailscale/wireguard-go v0.0.0-20210210160038-385d6fdeb57a h1:zgMmUGUb2U3E9VerpED4MlIceYjTT0QgpGr3qJKHyBE= +github.com/tailscale/wireguard-go v0.0.0-20210210160038-385d6fdeb57a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924= +github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM= +github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= @@ -342,6 +350,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -392,6 +402,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2l golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -447,6 +459,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210105210732-16f7687f5001 h1:/dSxr6gT0FNI1MO5WLJo8mTmItROeOKTkDn+7OwWBos= +golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac h1:9glrpwtNjBYgRpb67AZJKHfzj1stG/8BL5H7In2oTC4= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q= @@ -562,6 +578,8 @@ inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/R inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o= inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8= inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg= +inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM= +inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4= k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs= k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ= diff --git a/ipn/backend.go b/ipn/backend.go index 8042b6625..9352853b1 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -9,12 +9,11 @@ import ( "time" "golang.org/x/oauth2" - "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "tailscale.com/types/empty" + "tailscale.com/types/netmap" "tailscale.com/types/structs" - "tailscale.com/wgengine" ) type State int @@ -46,10 +45,10 @@ func (s State) String() string { // EngineStatus contains WireGuard engine stats. type EngineStatus struct { - RBytes, WBytes wgengine.ByteCount + RBytes, WBytes int64 NumLive int LiveDERPs int // number of active DERP connections - LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus + LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite } // Notify is a communication from a backend (e.g. tailscaled) to a frontend @@ -59,16 +58,16 @@ type EngineStatus struct { // They are JSON-encoded on the wire, despite the lack of struct tags. type Notify struct { _ structs.Incomparable - Version string // version number of IPN backend - ErrMessage *string // critical error message, if any; for InUseOtherUser, the details - LoginFinished *empty.Message // event: non-nil when login process succeeded - State *State // current IPN state has changed - Prefs *Prefs // preferences were changed - NetMap *controlclient.NetworkMap // new netmap received - Engine *EngineStatus // wireguard engine stats - Status *ipnstate.Status // full status - BrowseToURL *string // UI should open a browser right now - BackendLogID *string // public logtail id used by backend + Version string // version number of IPN backend + ErrMessage *string // critical error message, if any; for InUseOtherUser, the details + LoginFinished *empty.Message // event: non-nil when login process succeeded + State *State // current IPN state has changed + Prefs *Prefs // preferences were changed + NetMap *netmap.NetworkMap // new netmap received + Engine *EngineStatus // wireguard engine stats + Status *ipnstate.Status // full status + BrowseToURL *string // UI should open a browser right now + BackendLogID *string // public logtail id used by backend PingResult *ipnstate.PingResult // LocalTCPPort, if non-nil, informs the UI frontend which diff --git a/ipn/fake_test.go b/ipn/fake_test.go index 9b16cceaa..e918f77f0 100644 --- a/ipn/fake_test.go +++ b/ipn/fake_test.go @@ -9,8 +9,8 @@ import ( "time" "golang.org/x/oauth2" - "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" + "tailscale.com/types/netmap" ) type FakeBackend struct { @@ -54,7 +54,7 @@ func (b *FakeBackend) login() { b.newState(NeedsMachineAuth) b.newState(Stopped) // TODO(apenwarr): Fill in a more interesting netmap here. - b.notify(Notify{NetMap: &controlclient.NetworkMap{}}) + b.notify(Notify{NetMap: &netmap.NetworkMap{}}) b.newState(Starting) // TODO(apenwarr): Fill in a more interesting status. b.notify(Notify{Engine: &EngineStatus{}}) @@ -92,7 +92,7 @@ func (b *FakeBackend) RequestStatus() { } func (b *FakeBackend) FakeExpireAfter(x time.Duration) { - b.notify(Notify{NetMap: &controlclient.NetworkMap{}}) + b.notify(Notify{NetMap: &netmap.NetworkMap{}}) } func (b *FakeBackend) Ping(ip string) { diff --git a/ipn/handle.go b/ipn/handle.go index b79eea8e2..91b757f56 100644 --- a/ipn/handle.go +++ b/ipn/handle.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/types/logger" + "tailscale.com/types/netmap" ) type Handle struct { @@ -22,7 +22,7 @@ type Handle struct { // Mutex protects everything below mu sync.Mutex - netmapCache *controlclient.NetworkMap + netmapCache *netmap.NetworkMap engineStatusCache EngineStatus stateCache State prefsCache *Prefs @@ -129,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix { return []netaddr.IPPrefix{} } -func (h *Handle) NetMap() *controlclient.NetworkMap { +func (h *Handle) NetMap() *netmap.NetworkMap { h.mu.Lock() defer h.mu.Unlock() diff --git a/ipn/local.go b/ipn/ipnlocal/local.go similarity index 86% rename from ipn/local.go rename to ipn/ipnlocal/local.go index 0877ad9f7..071761434 100644 --- a/ipn/local.go +++ b/ipn/ipnlocal/local.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package ipn +package ipnlocal import ( "bytes" @@ -19,6 +19,7 @@ import ( "inet.af/netaddr" "tailscale.com/control/controlclient" "tailscale.com/internal/deepprint" + "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/ipn/policy" "tailscale.com/net/interfaces" @@ -28,6 +29,8 @@ import ( "tailscale.com/types/empty" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/types/netmap" + "tailscale.com/types/persist" "tailscale.com/types/wgkey" "tailscale.com/util/systemd" "tailscale.com/version" @@ -37,6 +40,7 @@ import ( "tailscale.com/wgengine/router/dns" "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" + "tailscale.com/wgengine/wgcfg/nmcfg" ) var controlDebugFlags = getControlDebugFlags() @@ -66,7 +70,7 @@ type LocalBackend struct { keyLogf logger.Logf // for printing list of peers on change statsLogf logger.Logf // for printing peers stats on change e wgengine.Engine - store StateStore + store ipn.StateStore backendLogID string portpoll *portlist.Poller // may be nil portpollOnce sync.Once // guards starting readPoller @@ -78,21 +82,21 @@ type LocalBackend struct { // The mutex protects the following elements. mu sync.Mutex - notify func(Notify) + notify func(ipn.Notify) c *controlclient.Client - stateKey StateKey // computed in part from user-provided value - userID string // current controlling user ID (for Windows, primarily) - prefs *Prefs + stateKey ipn.StateKey // computed in part from user-provided value + userID string // current controlling user ID (for Windows, primarily) + prefs *ipn.Prefs inServerMode bool machinePrivKey wgkey.Private - state State + state ipn.State // hostinfo is mutated in-place while mu is held. hostinfo *tailcfg.Hostinfo // netMap is not mutated in-place once set. - netMap *controlclient.NetworkMap + netMap *netmap.NetworkMap nodeByAddr map[netaddr.IP]*tailcfg.Node activeLogin string // last logged LoginName from netMap - engineStatus EngineStatus + engineStatus ipn.EngineStatus endpoints []string blocked bool authURL string @@ -107,7 +111,7 @@ type LocalBackend struct { // NewLocalBackend returns a new LocalBackend that is ready to run, // but is not actually running. -func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengine.Engine) (*LocalBackend, error) { +func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) { if e == nil { panic("ipn.NewLocalBackend: wgengine must not be nil") } @@ -130,7 +134,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin e: e, store: store, backendLogID: logid, - state: NoState, + state: ipn.NoState, portpoll: portpoll, gotPortPollRes: make(chan struct{}), } @@ -151,7 +155,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { networkUp := ifst.AnyInterfaceUp() if b.c != nil { - go b.c.SetPaused(b.state == Stopped || !networkUp) + go b.c.SetPaused(b.state == ipn.Stopped || !networkUp) } // If the PAC-ness of the network changed, reconfig wireguard+route to @@ -159,7 +163,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { if hadPAC != ifst.HasPAC() { b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC()) switch b.state { - case NoState, Stopped: + case ipn.NoState, ipn.Stopped: // Do nothing. default: go b.authReconfig() @@ -232,6 +236,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { Created: p.Created, LastSeen: lastSeen, ShareeNode: p.Hostinfo.ShareeNode, + ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID, }) } } @@ -280,7 +285,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { // Auth completed, unblock the engine b.blockEngineUpdates(false) b.authReconfig() - b.send(Notify{LoginFinished: &empty.Message{}}) + b.send(ipn.Notify{LoginFinished: &empty.Message{}}) } prefsChanged := false @@ -305,13 +310,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { prefsChanged = true } if st.NetMap != nil { + if b.keepOneExitNodeLocked(st.NetMap) { + prefsChanged = true + } b.setNetMapLocked(st.NetMap) - } if st.URL != "" { b.authURL = st.URL } - if b.state == NeedsLogin { + if b.state == ipn.NeedsLogin { if !b.prefs.WantRunning { prefsChanged = true } @@ -331,7 +338,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { b.logf("Failed to save new controlclient state: %v", err) } } - b.send(Notify{Prefs: prefs}) + b.send(ipn.Notify{Prefs: prefs}) } if st.NetMap != nil { if netMap != nil { @@ -350,7 +357,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { } b.e.SetDERPMap(st.NetMap.DERPMap) - b.send(Notify{NetMap: st.NetMap}) + b.send(ipn.Notify{NetMap: st.NetMap}) } if st.URL != "" { b.logf("Received auth URL: %.20v...", st.URL) @@ -364,6 +371,53 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { b.authReconfig() } +// keepOneExitNodeLocked edits nm to retain only the default +// routes provided by the exit node specified in b.prefs. It returns +// whether prefs was mutated as part of the process, due to an exit +// node IP being converted into a node ID. +func (b *LocalBackend) keepOneExitNodeLocked(nm *netmap.NetworkMap) (prefsChanged bool) { + // If we have a desired IP on file, try to find the corresponding + // node. + if !b.prefs.ExitNodeIP.IsZero() { + // IP takes precedence over ID, so if both are set, clear ID. + if b.prefs.ExitNodeID != "" { + b.prefs.ExitNodeID = "" + prefsChanged = true + } + + peerLoop: + for _, peer := range nm.Peers { + for _, addr := range peer.Addresses { + if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP { + continue + } + // Found the node being referenced, upgrade prefs to + // reference it directly for next time. + b.prefs.ExitNodeID = peer.StableID + b.prefs.ExitNodeIP = netaddr.IP{} + prefsChanged = true + break peerLoop + } + } + } + + // At this point, we have a node ID if the requested node is in + // the netmap. If not, the ID will be empty, and we'll strip out + // all default routes. + for _, peer := range nm.Peers { + out := peer.AllowedIPs[:0] + for _, allowedIP := range peer.AllowedIPs { + if allowedIP.Bits == 0 && peer.StableID != b.prefs.ExitNodeID { + continue + } + out = append(out, allowedIP) + } + peer.AllowedIPs = out + } + + return prefsChanged +} + // setWgengineStatus is the callback by the wireguard engine whenever it posts a new status. // This updates the endpoints both in the backend and in the control client. func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) { @@ -392,7 +446,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) { b.statusChanged.Broadcast() b.statusLock.Unlock() - b.send(Notify{Engine: &es}) + b.send(ipn.Notify{Engine: &es}) } // Start applies the configuration specified in opts, and starts the @@ -405,7 +459,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) { // guarantee that switching from one user's state to another is // actually a supported operation (it should be, but it's very unclear // from the following whether or not that is a safe transition). -func (b *LocalBackend) Start(opts Options) error { +func (b *LocalBackend) Start(opts ipn.Options) error { if opts.Prefs == nil && opts.StateKey == "" { return errors.New("no state key or prefs provided") } @@ -438,7 +492,7 @@ func (b *LocalBackend) Start(opts Options) error { hostinfo.NetInfo = b.hostinfo.NetInfo } b.hostinfo = hostinfo - b.state = NoState + b.state = ipn.NoState if err := b.loadStateLocked(opts.StateKey, opts.Prefs, opts.LegacyConfigPath); err != nil { b.mu.Unlock() @@ -456,7 +510,7 @@ func (b *LocalBackend) Start(opts Options) error { b.notify = opts.Notify b.setNetMapLocked(nil) - persist := b.prefs.Persist + persistv := b.prefs.Persist machinePrivKey := b.machinePrivKey b.mu.Unlock() @@ -489,14 +543,14 @@ func (b *LocalBackend) Start(opts Options) error { } var err error - if persist == nil { + if persistv == nil { // let controlclient initialize it - persist = &controlclient.Persist{} + persistv = &persist.Persist{} } cli, err := controlclient.New(controlclient.Options{ MachinePrivateKey: machinePrivKey, Logf: logger.WithPrefix(b.logf, "control: "), - Persist: *persist, + Persist: *persistv, ServerURL: b.serverURL, AuthKey: opts.AuthKey, Hostinfo: hostinfo, @@ -535,8 +589,8 @@ func (b *LocalBackend) Start(opts Options) error { blid := b.backendLogID b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID) - b.send(Notify{BackendLogID: &blid}) - b.send(Notify{Prefs: prefs}) + b.send(ipn.Notify{BackendLogID: &blid}) + b.send(ipn.Notify{Prefs: prefs}) cli.Login(nil, controlclient.LoginDefault) return nil @@ -544,7 +598,7 @@ func (b *LocalBackend) Start(opts Options) error { // updateFilter updates the packet filter in wgengine based on the // given netMap and user preferences. -func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) { +func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) { // NOTE(danderson): keep change detection as the first thing in // this function. Don't try to optimize by returning early, more // likely than not you'll just end up breaking the change @@ -603,7 +657,7 @@ func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool { // dnsMapsEqual determines whether the new and the old network map // induce the same DNS map. It does so without allocating memory, // at the expense of giving false negatives if peers are reordered. -func dnsMapsEqual(new, old *controlclient.NetworkMap) bool { +func dnsMapsEqual(new, old *netmap.NetworkMap) bool { if (old == nil) != (new == nil) { return false } @@ -637,7 +691,7 @@ func dnsMapsEqual(new, old *controlclient.NetworkMap) bool { // updateDNSMap updates the domain map in the DNS resolver in wgengine // based on the given netMap and user preferences. -func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) { +func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) { if netMap == nil { b.logf("dns map: (not ready)") return @@ -701,7 +755,7 @@ func (b *LocalBackend) readPoller() { // send delivers n to the connected frontend. If no frontend is // connected, the notification is dropped without being delivered. -func (b *LocalBackend) send(n Notify) { +func (b *LocalBackend) send(n ipn.Notify) { b.mu.Lock() notify := b.notify b.mu.Unlock() @@ -727,9 +781,9 @@ func (b *LocalBackend) popBrowserAuthNow() { b.blockEngineUpdates(true) b.stopEngineAndWait() - b.send(Notify{BrowseToURL: &url}) - if b.State() == Running { - b.enterState(Starting) + b.send(ipn.Notify{BrowseToURL: &url}) + if b.State() == ipn.Running { + b.enterState(ipn.Starting) } } @@ -760,21 +814,21 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) { legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey } - keyText, err := b.store.ReadState(MachineKeyStateKey) + keyText, err := b.store.ReadState(ipn.MachineKeyStateKey) if err == nil { if err := b.machinePrivKey.UnmarshalText(keyText); err != nil { - return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err) + return fmt.Errorf("invalid key in %s key of %v: %w", ipn.MachineKeyStateKey, b.store, err) } if b.machinePrivKey.IsZero() { - return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store) + return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.MachineKeyStateKey, b.store) } if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) { b.logf("frontend-provided legacy machine key ignored; used value from server state") } return nil } - if err != ErrStateNotExist { - return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err) + if err != ipn.ErrStateNotExist { + return fmt.Errorf("error reading %v key of %v: %w", ipn.MachineKeyStateKey, b.store, err) } // If we didn't find one already on disk and the prefs already @@ -797,7 +851,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) { } keyText, _ = b.machinePrivKey.MarshalText() - if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil { + if err := b.store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil { b.logf("error writing machine key to store: %v", err) return err } @@ -810,14 +864,14 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) { // user and prefs. If userID is blank or prefs is blank, no work is done. // // b.mu may either be held or not. -func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) { +func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs) { if userID == "" || prefs == nil { return } if prefs.ForceDaemon { - stateKey := StateKey("user-" + userID) - if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil { + stateKey := ipn.StateKey("user-" + userID) + if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil { b.logf("WriteState error: %v", err) } // It's important we do this here too, even if it looks @@ -829,7 +883,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) { b.logf("WriteState error: %v", err) } } else { - if err := b.store.WriteState(ServerModeStartKey, nil); err != nil { + if err := b.store.WriteState(ipn.ServerModeStartKey, nil); err != nil { b.logf("WriteState error: %v", err) } } @@ -838,7 +892,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) { // loadStateLocked sets b.prefs and b.stateKey based on a complex // combination of key, prefs, and legacyPath. b.mu must be held when // calling. -func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) { +func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legacyPath string) (err error) { if prefs == nil && key == "" { panic("state key and prefs are both unset") } @@ -880,19 +934,19 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st b.logf("using backend prefs") bs, err := b.store.ReadState(key) if err != nil { - if errors.Is(err, ErrStateNotExist) { + if errors.Is(err, ipn.ErrStateNotExist) { if legacyPath != "" { - b.prefs, err = LoadPrefs(legacyPath) + b.prefs, err = ipn.LoadPrefs(legacyPath) if err != nil { if !errors.Is(err, os.ErrNotExist) { b.logf("failed to load legacy prefs: %v", err) } - b.prefs = NewPrefs() + b.prefs = ipn.NewPrefs() } else { b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty()) } } else { - b.prefs = NewPrefs() + b.prefs = ipn.NewPrefs() b.logf("created empty state for %q: %s", key, b.prefs.Pretty()) } if err := b.initMachineKeyLocked(); err != nil { @@ -902,7 +956,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st } return fmt.Errorf("store.ReadState(%q): %v", key, err) } - b.prefs, err = PrefsFromBytes(bs, false) + b.prefs, err = ipn.PrefsFromBytes(bs, false) if err != nil { return fmt.Errorf("PrefsFromBytes: %v", err) } @@ -914,7 +968,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st } // State returns the backend state machine's current state. -func (b *LocalBackend) State() State { +func (b *LocalBackend) State() ipn.State { b.mu.Lock() defer b.mu.Unlock() @@ -930,7 +984,7 @@ func (b *LocalBackend) InServerMode() bool { // getEngineStatus returns a copy of b.engineStatus. // // TODO(bradfitz): remove this and use Status() throughout. -func (b *LocalBackend) getEngineStatus() EngineStatus { +func (b *LocalBackend) getEngineStatus() ipn.EngineStatus { b.mu.Lock() defer b.mu.Unlock() @@ -986,7 +1040,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) { mapCopy.Expiry = time.Now().Add(x) } b.setNetMapLocked(&mapCopy) - b.send(Notify{NetMap: b.netMap}) + b.send(ipn.Notify{NetMap: b.netMap}) } func (b *LocalBackend) Ping(ipStr string) { @@ -996,7 +1050,7 @@ func (b *LocalBackend) Ping(ipStr string) { return } b.e.Ping(ip, func(pr *ipnstate.PingResult) { - b.send(Notify{PingResult: pr}) + b.send(ipn.Notify{PingResult: pr}) }) } @@ -1005,11 +1059,11 @@ func (b *LocalBackend) Ping(ipStr string) { // b.mu must be held; mostly because the caller is about to anyway, and doing so // gives us slightly better guarantees about the two peers stats lines not // being intermixed if there are concurrent calls to our caller. -func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) { +func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineStatus) { var peerStats, peerKeys strings.Builder ret.LiveDERPs = s.DERPs - ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{} + ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{} for _, p := range s.Peers { if !p.LastHandshake.IsZero() { fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes) @@ -1065,7 +1119,7 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) { // SetPrefs saves new user preferences and propagates them throughout // the system. Implements Backend. -func (b *LocalBackend) SetPrefs(newp *Prefs) { +func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) { if newp == nil { panic("SetPrefs got nil prefs") } @@ -1132,7 +1186,7 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) { b.authReconfig() } - b.send(Notify{Prefs: newp}) + b.send(ipn.Notify{Prefs: newp}) } // doSetHostinfoFilterServices calls SetHostinfo on the controlclient, @@ -1158,7 +1212,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) { // NetMap returns the latest cached network map received from // controlclient, or nil if no network map was received yet. -func (b *LocalBackend) NetMap() *controlclient.NetworkMap { +func (b *LocalBackend) NetMap() *netmap.NetworkMap { b.mu.Lock() defer b.mu.Unlock() return b.netMap @@ -1200,23 +1254,21 @@ func (b *LocalBackend) authReconfig() { return } - var flags controlclient.WGConfigFlags + var flags netmap.WGConfigFlags if uc.RouteAll { - flags |= controlclient.AllowDefaultRoute - // TODO(apenwarr): Make subnet routes a different pref? - flags |= controlclient.AllowSubnetRoutes + flags |= netmap.AllowSubnetRoutes } if uc.AllowSingleHosts { - flags |= controlclient.AllowSingleHosts + flags |= netmap.AllowSingleHosts } if hasPAC && disableSubnetsIfPAC { - if flags&controlclient.AllowSubnetRoutes != 0 { + if flags&netmap.AllowSubnetRoutes != 0 { b.logf("authReconfig: have PAC; disabling subnet routes") - flags &^= controlclient.AllowSubnetRoutes + flags &^= netmap.AllowSubnetRoutes } } - cfg, err := nm.WGCfg(b.logf, flags) + cfg, err := nmcfg.WGCfg(nm, b.logf, flags) if err != nil { b.logf("wgcfg: %v", err) return @@ -1248,15 +1300,20 @@ func (b *LocalBackend) authReconfig() { // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. // Each entry has a trailing period. -func magicDNSRootDomains(nm *controlclient.NetworkMap) []string { +func magicDNSRootDomains(nm *netmap.NetworkMap) []string { if v := nm.MagicDNSSuffix(); v != "" { return []string{strings.Trim(v, ".") + "."} } return nil } +var ( + ipv4Default = netaddr.MustParseIPPrefix("0.0.0.0/0") + ipv6Default = netaddr.MustParseIPPrefix("::/0") +) + // routerConfig produces a router.Config from a wireguard config and IPN prefs. -func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config { +func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config { rs := &router.Config{ LocalAddrs: unmapIPPrefixes(cfg.Addresses), SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes), @@ -1268,6 +1325,32 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config { rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...) } + // Sanity check: we expect the control server to program both a v4 + // and a v6 default route, if default routing is on. Fill in + // blackhole routes appropriately if we're missing some. This is + // likely to break some functionality, but if the user expressed a + // preference for routing remotely, we want to avoid leaking + // traffic at the expense of functionality. + if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() { + var default4, default6 bool + for _, route := range rs.Routes { + if route == ipv4Default { + default4 = true + } else if route == ipv6Default { + default6 = true + } + if default4 && default6 { + break + } + } + if !default4 { + rs.Routes = append(rs.Routes, ipv4Default) + } + if !default6 { + rs.Routes = append(rs.Routes, ipv6Default) + } + } + rs.Routes = append(rs.Routes, netaddr.IPPrefix{ IP: tsaddr.TailscaleServiceIP(), Bits: 32, @@ -1285,7 +1368,7 @@ func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) { return ret } -func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) { +func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) { if h := prefs.Hostname; h != "" { hi.Hostname = h } @@ -1305,7 +1388,7 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) { // places twiddle IPN internal state without going through here, so // really this is more "one of several places in which random things // happen". -func (b *LocalBackend) enterState(newState State) { +func (b *LocalBackend) enterState(newState ipn.State) { b.mu.Lock() state := b.state b.state = newState @@ -1323,19 +1406,19 @@ func (b *LocalBackend) enterState(newState State) { b.logf("Switching ipn state %v -> %v (WantRunning=%v)", state, newState, prefs.WantRunning) if notify != nil { - b.send(Notify{State: &newState}) + b.send(ipn.Notify{State: &newState}) } if bc != nil { - bc.SetPaused(newState == Stopped || !networkUp) + bc.SetPaused(newState == ipn.Stopped || !networkUp) } switch newState { - case NeedsLogin: + case ipn.NeedsLogin: systemd.Status("Needs login: %s", authURL) b.blockEngineUpdates(true) fallthrough - case Stopped: + case ipn.Stopped: err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{}) if err != nil { b.logf("Reconfig(down): %v", err) @@ -1344,11 +1427,11 @@ func (b *LocalBackend) enterState(newState State) { if authURL == "" { systemd.Status("Stopped; run 'tailscale up' to log in") } - case Starting, NeedsMachineAuth: + case ipn.Starting, ipn.NeedsMachineAuth: b.authReconfig() // Needed so that UpdateEndpoints can run b.e.RequestStatus() - case Running: + case ipn.Running: var addrs []string for _, addr := range b.netMap.Addresses { addrs = append(addrs, addr.IP.String()) @@ -1362,7 +1445,7 @@ func (b *LocalBackend) enterState(newState State) { // nextState returns the state the backend seems to be in, based on // its internal state. -func (b *LocalBackend) nextState() State { +func (b *LocalBackend) nextState() ipn.State { b.mu.Lock() b.assertClientLocked() var ( @@ -1378,31 +1461,31 @@ func (b *LocalBackend) nextState() State { if c.AuthCantContinue() { // Auth was interrupted or waiting for URL visit, // so it won't proceed without human help. - return NeedsLogin + return ipn.NeedsLogin } else { // Auth or map request needs to finish return state } case !wantRunning: - return Stopped + return ipn.Stopped case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0: - return NeedsLogin + return ipn.NeedsLogin case netMap.MachineStatus != tailcfg.MachineAuthorized: // TODO(crawshaw): handle tailcfg.MachineInvalid - return NeedsMachineAuth - case state == NeedsMachineAuth: + return ipn.NeedsMachineAuth + case state == ipn.NeedsMachineAuth: // (if we get here, we know MachineAuthorized == true) - return Starting - case state == Starting: + return ipn.Starting + case state == ipn.Starting: if st := b.getEngineStatus(); st.NumLive > 0 || st.LiveDERPs > 0 { - return Running + return ipn.Running } else { return state } - case state == Running: - return Running + case state == ipn.Running: + return ipn.Running default: - return Starting + return ipn.Starting } } @@ -1414,7 +1497,7 @@ func (b *LocalBackend) RequestEngineStatus() { // RequestStatus implements Backend. func (b *LocalBackend) RequestStatus() { st := b.Status() - b.send(Notify{Status: st}) + b.send(ipn.Notify{Status: st}) } // stateMachine updates the state machine state based on other things @@ -1510,7 +1593,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) { c.SetNetInfo(ni) } -func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) { +func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { var login string if nm != nil { login = nm.UserProfiles[nm.User].LoginName diff --git a/ipn/local_test.go b/ipn/ipnlocal/local_test.go similarity index 50% rename from ipn/local_test.go rename to ipn/ipnlocal/local_test.go index 547262f61..9f19a08f1 100644 --- a/ipn/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package ipn +package ipnlocal import ( - "inet.af/netaddr" - "tailscale.com/control/controlclient" - "tailscale.com/tailcfg" "testing" + + "inet.af/netaddr" + "tailscale.com/tailcfg" + "tailscale.com/types/netmap" ) func TestNetworkMapCompare(t *testing.T) { @@ -26,7 +27,7 @@ func TestNetworkMapCompare(t *testing.T) { tests := []struct { name string - a, b *controlclient.NetworkMap + a, b *netmap.NetworkMap want bool }{ { @@ -37,76 +38,76 @@ func TestNetworkMapCompare(t *testing.T) { }, { "b nil", - &controlclient.NetworkMap{}, + &netmap.NetworkMap{}, nil, false, }, { "a nil", nil, - &controlclient.NetworkMap{}, + &netmap.NetworkMap{}, false, }, { "both default", - &controlclient.NetworkMap{}, - &controlclient.NetworkMap{}, + &netmap.NetworkMap{}, + &netmap.NetworkMap{}, true, }, { "names identical", - &controlclient.NetworkMap{Name: "map1"}, - &controlclient.NetworkMap{Name: "map1"}, + &netmap.NetworkMap{Name: "map1"}, + &netmap.NetworkMap{Name: "map1"}, true, }, { "names differ", - &controlclient.NetworkMap{Name: "map1"}, - &controlclient.NetworkMap{Name: "map2"}, + &netmap.NetworkMap{Name: "map1"}, + &netmap.NetworkMap{Name: "map2"}, false, }, { "Peers identical", - &controlclient.NetworkMap{Peers: []*tailcfg.Node{}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, true, }, { "Peer list length", // length of Peers list differs - &controlclient.NetworkMap{Peers: []*tailcfg.Node{{}}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{{}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, false, }, { "Node names identical", - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, true, }, { "Node names differ", - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}}, false, }, { "Node lists identical", - &controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, true, }, { "Node lists differ", - &controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node2}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}}, false, }, { "Node Users differ", // User field is not checked. - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}}, - &controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}}, + &netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}}, true, }, } diff --git a/ipn/loglines_test.go b/ipn/ipnlocal/loglines_test.go similarity index 89% rename from ipn/loglines_test.go rename to ipn/ipnlocal/loglines_test.go index fb056e8be..83ae8a309 100644 --- a/ipn/loglines_test.go +++ b/ipn/ipnlocal/loglines_test.go @@ -2,18 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package ipn +package ipnlocal import ( "reflect" "testing" "time" - "tailscale.com/control/controlclient" + "tailscale.com/ipn" + "tailscale.com/ipn/ipnstate" "tailscale.com/logtail" "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/types/key" + "tailscale.com/types/persist" "tailscale.com/wgengine" ) @@ -38,9 +40,7 @@ func TestLocalLogLines(t *testing.T) { idA := logid(0xaa) // set up a LocalBackend, super bare bones. No functional data. - store := &MemoryStore{ - cache: make(map[StateKey][]byte), - } + store := &ipn.MemoryStore{} e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil) if err != nil { t.Fatal(err) @@ -53,7 +53,7 @@ func TestLocalLogLines(t *testing.T) { defer lb.Shutdown() // custom adjustments for required non-nil fields - lb.prefs = NewPrefs() + lb.prefs = ipn.NewPrefs() lb.hostinfo = &tailcfg.Hostinfo{} // hacky manual override of the usual log-on-change behaviour of keylogf lb.keyLogf = logListen.Logf @@ -67,8 +67,8 @@ func TestLocalLogLines(t *testing.T) { } // log prefs line - persist := &controlclient.Persist{} - prefs := NewPrefs() + persist := &persist.Persist{} + prefs := ipn.NewPrefs() prefs.Persist = persist lb.SetPrefs(prefs) @@ -76,7 +76,7 @@ func TestLocalLogLines(t *testing.T) { // log peers, peer keys status := &wgengine.Status{ - Peers: []wgengine.PeerStatus{wgengine.PeerStatus{ + Peers: []ipnstate.PeerStatusLite{{ TxBytes: 10, RxBytes: 10, LastHandshake: time.Now(), diff --git a/ipn/ipnserver/conn_linux.go b/ipn/ipnserver/conn_linux.go deleted file mode 100644 index 1aca57e26..000000000 --- a/ipn/ipnserver/conn_linux.go +++ /dev/null @@ -1,49 +0,0 @@ -// 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. - -// +build linux - -package ipnserver - -import ( - "net" - - "golang.org/x/sys/unix" - "tailscale.com/types/logger" -) - -func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) { - ro = true // conservative default for naked returns below - uc, ok := c.(*net.UnixConn) - if !ok { - logf("unexpected connection type %T", c) - return - } - raw, err := uc.SyscallConn() - if err != nil { - logf("SyscallConn: %v", err) - return - } - - var cred *unix.Ucred - cerr := raw.Control(func(fd uintptr) { - cred, err = unix.GetsockoptUcred(int(fd), - unix.SOL_SOCKET, - unix.SO_PEERCRED) - }) - if cerr != nil { - logf("raw.Control: %v", err) - return - } - if err != nil { - logf("raw.Control: %v", err) - return - } - if cred.Uid == 0 { - // root is not read-only. - return false - } - logf("non-root connection from %v (read-only)", cred.Uid) - return true -} diff --git a/ipn/ipnserver/conn_no_ucred.go b/ipn/ipnserver/conn_no_ucred.go deleted file mode 100644 index c50e4778d..000000000 --- a/ipn/ipnserver/conn_no_ucred.go +++ /dev/null @@ -1,27 +0,0 @@ -// 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. - -// +build !linux - -package ipnserver - -import ( - "net" - - "tailscale.com/types/logger" -) - -func isReadonlyConn(c net.Conn, logf logger.Logf) bool { - // Windows doesn't need/use this mechanism, at least yet. It - // has a different last-user-wins auth model. - - // And on Darwin, we're not using it yet, as the Darwin - // tailscaled port isn't yet done, and unix.Ucred and - // unix.GetsockoptUcred aren't in x/sys/unix. - - // TODO(bradfitz): OpenBSD and FreeBSD should implement this too. - // But their x/sys/unix package is different than Linux, so - // I didn't include it for now. - return false -} diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index dd8a39ab6..f7937d39a 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -7,7 +7,6 @@ package ipnserver import ( "bufio" "context" - "encoding/json" "errors" "fmt" "io" @@ -22,18 +21,22 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "syscall" "time" + "go4.org/mem" "inet.af/netaddr" + "inet.af/peercred" "tailscale.com/control/controlclient" "tailscale.com/ipn" + "tailscale.com/ipn/ipnlocal" + "tailscale.com/ipn/localapi" "tailscale.com/log/filelogger" "tailscale.com/logtail/backoff" "tailscale.com/net/netstat" "tailscale.com/safesocket" "tailscale.com/smallzstd" - "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/util/pidowner" "tailscale.com/util/systemd" @@ -93,7 +96,7 @@ type Options struct { // server is an IPN backend and its set of 0 or more active connections // talking to an IPN backend. type server struct { - b *ipn.LocalBackend + b *ipnlocal.LocalBackend logf logger.Logf // resetOnZero is whether to call bs.Reset on transition from // 1->0 connections. That is, this is whether the backend is @@ -221,13 +224,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) { } } +// bufferHasHTTPRequest reports whether br looks like it has an HTTP +// request in it, without reading any bytes from it. +func bufferHasHTTPRequest(br *bufio.Reader) bool { + peek, _ := br.Peek(br.Buffered()) + return mem.HasPrefix(mem.B(peek), mem.S("GET ")) || + mem.HasPrefix(mem.B(peek), mem.S("POST ")) || + mem.Contains(mem.B(peek), mem.S(" HTTP/")) +} + func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { // First see if it's an HTTP request. br := bufio.NewReader(c) c.SetReadDeadline(time.Now().Add(time.Second)) - peek, _ := br.Peek(4) + br.Peek(4) c.SetReadDeadline(time.Time{}) - isHTTPReq := string(peek) == "GET " + isHTTPReq := bufferHasHTTPRequest(br) ci, err := s.addConn(c, isHTTPReq) if err != nil { @@ -254,7 +266,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { s.b.SetCurrentUserID(ci.UserID) if isHTTPReq { - httpServer := http.Server{ + httpServer := &http.Server{ // Localhost connections are cheap; so only do // keep-alives for a short period of time, as these // active connections lock the server into only serving @@ -299,6 +311,70 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { } } +func isReadonlyConn(c net.Conn, logf logger.Logf) bool { + const ro = true + const rw = false + creds, err := peercred.Get(c) + if err != nil { + logf("connection from unknown peer; read-only") + return ro + } + uid, ok := creds.UserID() + if !ok { + logf("connection from peer with unknown userid; read-only") + return ro + } + if uid == "0" { + logf("connection from userid %v; root has access", uid) + return rw + } + var adminGroupID string + switch runtime.GOOS { + case "darwin": + adminGroupID = darwinAdminGroupID() + default: + logf("connection from userid %v; read-only", uid) + return ro + } + if adminGroupID == "" { + logf("connection from userid %v; no system admin group found, read-only", uid) + return ro + } + u, err := user.LookupId(uid) + if err != nil { + logf("connection from userid %v; failed to look up user; read-only", uid) + return ro + } + gids, err := u.GroupIds() + if err != nil { + logf("connection from userid %v; failed to look up groups; read-only", uid) + return ro + } + for _, gid := range gids { + if gid == adminGroupID { + logf("connection from userid %v; is local admin, has access", uid) + return rw + } + } + logf("connection from userid %v; read-only", uid) + return ro +} + +var darwinAdminGroupIDCache atomic.Value // of string + +func darwinAdminGroupID() string { + s, _ := darwinAdminGroupIDCache.Load().(string) + if s != "" { + return s + } + g, err := user.LookupGroup("admin") + if err != nil { + return "" + } + darwinAdminGroupIDCache.Store(g.Gid) + return g.Gid +} + // inUseOtherUserError is the error type for when the server is in use // by a different local user. type inUseOtherUserError struct{ error } @@ -612,7 +688,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() ( } } - b, err := ipn.NewLocalBackend(logf, logid, store, eng) + b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng) if err != nil { return fmt.Errorf("NewLocalBackend: %v", err) } @@ -625,7 +701,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() ( opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) { serveHTMLStatus(w, b) }) - opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b}) + h := localapi.NewHandler(b) + h.PermitRead = true + opts.DebugMux.Handle("/localapi/", h) } server.b = b @@ -866,8 +944,11 @@ func (psc *protoSwitchConn) Close() error { func (s *server) localhostHandler(ci connIdentity) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ci.IsUnixSock && r.URL.Path == "/localapi/v0/whois" { - whoIsHandler{s.b}.ServeHTTP(w, r) + if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") { + h := localapi.NewHandler(s.b) + h.PermitRead = true + h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this + h.ServeHTTP(w, r) return } if ci.Unknown { @@ -878,7 +959,7 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler { }) } -func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) { +func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) { w.Header().Set("Content-Type", "text/html; charset=utf-8") st := b.Status() // TODO(bradfitz): add LogID and opts to st? @@ -893,40 +974,3 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int { } return 0 } - -// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler. -type whoIsHandler struct { - b *ipn.LocalBackend -} - -func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - b := h.b - var ip netaddr.IP - if v := r.FormValue("ip"); v != "" { - var err error - ip, err = netaddr.ParseIP(r.FormValue("ip")) - if err != nil { - http.Error(w, "invalid 'ip' parameter", 400) - return - } - } else { - http.Error(w, "missing 'ip' parameter", 400) - return - } - n, u, ok := b.WhoIs(ip) - if !ok { - http.Error(w, "no match for IP", 404) - return - } - res := &tailcfg.WhoIsResponse{ - Node: n, - UserProfile: &u, - } - j, err := json.MarshalIndent(res, "", "\t") - if err != nil { - http.Error(w, "JSON encoding error", 500) - return - } - w.Header().Set("Content-Type", "application/json") - w.Write(j) -} diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index fbddbc46b..6d713bc78 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -50,6 +50,12 @@ func (s *Status) Peers() []key.Public { return kk } +type PeerStatusLite struct { + TxBytes, RxBytes int64 + LastHandshake time.Time + NodeKey tailcfg.NodeKey +} + type PeerStatus struct { PublicKey key.Public HostName string // HostInfo's Hostname (not a DNS name or necessarily unique) @@ -71,6 +77,7 @@ type PeerStatus struct { LastSeen time.Time // last seen to tailcontrol LastHandshake time.Time // with local wireguard KeepAlive bool + ExitNode bool // true if this is the currently selected exit node. // ShareeNode indicates this node exists in the netmap because // it's owned by a shared-to user and that node might connect @@ -238,6 +245,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) { if st.KeepAlive { e.KeepAlive = true } + if st.ExitNode { + e.ExitNode = true + } if st.ShareeNode { e.ShareeNode = true } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go new file mode 100644 index 000000000..7b537d404 --- /dev/null +++ b/ipn/localapi/localapi.go @@ -0,0 +1,95 @@ +// 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 localapi contains the HTTP server handlers for tailscaled's API server. +package localapi + +import ( + "encoding/json" + "io" + "net/http" + + "inet.af/netaddr" + "tailscale.com/ipn/ipnlocal" + "tailscale.com/tailcfg" +) + +func NewHandler(b *ipnlocal.LocalBackend) *Handler { + return &Handler{b: b} +} + +type Handler struct { + // RequiredPassword, if non-empty, forces all HTTP + // requests to have HTTP basic auth with this password. + // It's used by the sandboxed macOS sameuserproof GUI auth mechanism. + RequiredPassword string + + // PermitRead is whether read-only HTTP handlers are allowed. + PermitRead bool + + // PermitWrite is whether mutating HTTP handlers are allowed. + PermitWrite bool + + b *ipnlocal.LocalBackend +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.b == nil { + http.Error(w, "server has no local backend", http.StatusInternalServerError) + return + } + if h.RequiredPassword != "" { + _, pass, ok := r.BasicAuth() + if !ok { + http.Error(w, "auth required", http.StatusUnauthorized) + return + } + if pass != h.RequiredPassword { + http.Error(w, "bad password", http.StatusForbidden) + return + } + } + switch r.URL.Path { + case "/localapi/v0/whois": + h.serveWhoIs(w, r) + default: + io.WriteString(w, "tailscaled\n") + } +} + +func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { + if !h.PermitRead { + http.Error(w, "whois access denied", http.StatusForbidden) + return + } + b := h.b + var ip netaddr.IP + if v := r.FormValue("ip"); v != "" { + var err error + ip, err = netaddr.ParseIP(r.FormValue("ip")) + if err != nil { + http.Error(w, "invalid 'ip' parameter", 400) + return + } + } else { + http.Error(w, "missing 'ip' parameter", 400) + return + } + n, u, ok := b.WhoIs(ip) + if !ok { + http.Error(w, "no match for IP", 404) + return + } + res := &tailcfg.WhoIsResponse{ + Node: n, + UserProfile: &u, + } + j, err := json.MarshalIndent(res, "", "\t") + if err != nil { + http.Error(w, "JSON encoding error", 500) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(j) +} diff --git a/ipn/prefs.go b/ipn/prefs.go index f8256454b..7b4a5562c 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -17,8 +17,9 @@ import ( "inet.af/netaddr" "tailscale.com/atomicfile" - "tailscale.com/control/controlclient" - "tailscale.com/wgengine/router" + "tailscale.com/tailcfg" + "tailscale.com/types/persist" + "tailscale.com/types/preftype" ) //go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go @@ -28,8 +29,10 @@ type Prefs struct { // ControlURL is the URL of the control server to use. ControlURL string - // RouteAll specifies whether to accept subnet and default routes - // advertised by other nodes on the Tailscale network. + // RouteAll specifies whether to accept subnets advertised by + // other nodes on the Tailscale network. Note that this does not + // include default routes (0.0.0.0/0 and ::/0), those are + // controlled by ExitNodeID/IP below. RouteAll bool // AllowSingleHosts specifies whether to install routes for each @@ -44,6 +47,24 @@ type Prefs struct { // packets stop flowing. What's up with that? AllowSingleHosts bool + // ExitNodeID and ExitNodeIP specify the node that should be used + // as an exit node for internet traffic. At most one of these + // should be non-zero. + // + // The preferred way to express the chosen node is ExitNodeID, but + // in some cases it's not possible to use that ID (e.g. in the + // linux CLI, before tailscaled has a netmap). For those + // situations, we allow specifying the exit node by IP, and + // ipnlocal.LocalBackend will translate the IP into an ID when the + // node is found in the netmap. + // + // If the selected exit node doesn't exist (e.g. it's not part of + // the current tailnet), or it doesn't offer exit node services, a + // blackhole route will be installed on the local system to + // prevent any traffic escaping to the local network. + ExitNodeID tailcfg.StableNodeID + ExitNodeIP netaddr.IP + // CorpDNS specifies whether to install the Tailscale network's // DNS configuration, if it exists. CorpDNS bool @@ -116,14 +137,14 @@ type Prefs struct { // NetfilterMode specifies how much to manage netfilter rules for // Tailscale, if at all. - NetfilterMode router.NetfilterMode + NetfilterMode preftype.NetfilterMode // The Persist field is named 'Config' in the file for backward // compatibility with earlier versions. // TODO(apenwarr): We should move this out of here, it's not a pref. // We can maybe do that once we're sure which module should persist // it (backend or frontend?) - Persist *controlclient.Persist `json:"Config"` + Persist *persist.Persist `json:"Config"` } // IsEmpty reports whether p is nil or pointing to a Prefs zero value. @@ -191,6 +212,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool { p.ControlURL == p2.ControlURL && p.RouteAll == p2.RouteAll && p.AllowSingleHosts == p2.AllowSingleHosts && + p.ExitNodeID == p2.ExitNodeID && + p.ExitNodeIP == p2.ExitNodeIP && p.CorpDNS == p2.CorpDNS && p.WantRunning == p2.WantRunning && p.NotepadURLs == p2.NotepadURLs && @@ -240,7 +263,7 @@ func NewPrefs() *Prefs { AllowSingleHosts: true, CorpDNS: true, WantRunning: true, - NetfilterMode: router.NetfilterOn, + NetfilterMode: preftype.NetfilterOn, } } @@ -252,7 +275,7 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) { if len(b) == 0 { return p, nil } - persist := &controlclient.Persist{} + persist := &persist.Persist{} err := json.Unmarshal(b, persist) if err == nil && (persist.Provider != "" || persist.LoginName != "") { // old-style relaynode config; import it diff --git a/ipn/prefs_clone.go b/ipn/prefs_clone.go index c0e11f8f8..9e426a4fa 100644 --- a/ipn/prefs_clone.go +++ b/ipn/prefs_clone.go @@ -8,8 +8,9 @@ package ipn import ( "inet.af/netaddr" - "tailscale.com/control/controlclient" - "tailscale.com/wgengine/router" + "tailscale.com/tailcfg" + "tailscale.com/types/persist" + "tailscale.com/types/preftype" ) // Clone makes a deep copy of Prefs. @@ -23,7 +24,7 @@ func (src *Prefs) Clone() *Prefs { dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...) dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...) if dst.Persist != nil { - dst.Persist = new(controlclient.Persist) + dst.Persist = new(persist.Persist) *dst.Persist = *src.Persist } return dst @@ -35,6 +36,8 @@ var _PrefsNeedsRegeneration = Prefs(struct { ControlURL string RouteAll bool AllowSingleHosts bool + ExitNodeID tailcfg.StableNodeID + ExitNodeIP netaddr.IP CorpDNS bool WantRunning bool ShieldsUp bool @@ -46,6 +49,6 @@ var _PrefsNeedsRegeneration = Prefs(struct { ForceDaemon bool AdvertiseRoutes []netaddr.IPPrefix NoSNAT bool - NetfilterMode router.NetfilterMode - Persist *controlclient.Persist + NetfilterMode preftype.NetfilterMode + Persist *persist.Persist }{}) diff --git a/ipn/prefs_test.go b/ipn/prefs_test.go index 9715d7c89..ad8905b03 100644 --- a/ipn/prefs_test.go +++ b/ipn/prefs_test.go @@ -14,10 +14,10 @@ import ( "time" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/tstest" + "tailscale.com/types/persist" + "tailscale.com/types/preftype" "tailscale.com/types/wgkey" - "tailscale.com/wgengine/router" ) func fieldsOf(t reflect.Type) (fields []string) { @@ -30,7 +30,7 @@ func fieldsOf(t reflect.Type) (fields []string) { func TestPrefsEqual(t *testing.T) { tstest.PanicOnLog() - prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} + prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"} if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) { t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n", have, prefsHandles) @@ -99,6 +99,28 @@ func TestPrefsEqual(t *testing.T) { true, }, + { + &Prefs{ExitNodeID: "n1234"}, + &Prefs{}, + false, + }, + { + &Prefs{ExitNodeID: "n1234"}, + &Prefs{ExitNodeID: "n1234"}, + true, + }, + + { + &Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")}, + &Prefs{}, + false, + }, + { + &Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")}, + &Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")}, + true, + }, + { &Prefs{CorpDNS: true}, &Prefs{CorpDNS: false}, @@ -192,24 +214,24 @@ func TestPrefsEqual(t *testing.T) { }, { - &Prefs{NetfilterMode: router.NetfilterOff}, - &Prefs{NetfilterMode: router.NetfilterOn}, + &Prefs{NetfilterMode: preftype.NetfilterOff}, + &Prefs{NetfilterMode: preftype.NetfilterOn}, false, }, { - &Prefs{NetfilterMode: router.NetfilterOn}, - &Prefs{NetfilterMode: router.NetfilterOn}, + &Prefs{NetfilterMode: preftype.NetfilterOn}, + &Prefs{NetfilterMode: preftype.NetfilterOn}, true, }, { - &Prefs{Persist: &controlclient.Persist{}}, - &Prefs{Persist: &controlclient.Persist{LoginName: "dave"}}, + &Prefs{Persist: &persist.Persist{}}, + &Prefs{Persist: &persist.Persist{LoginName: "dave"}}, false, }, { - &Prefs{Persist: &controlclient.Persist{LoginName: "dave"}}, - &Prefs{Persist: &controlclient.Persist{LoginName: "dave"}}, + &Prefs{Persist: &persist.Persist{LoginName: "dave"}}, + &Prefs{Persist: &persist.Persist{LoginName: "dave"}}, true, }, } @@ -274,7 +296,7 @@ func TestBasicPrefs(t *testing.T) { func TestPrefsPersist(t *testing.T) { tstest.PanicOnLog() - c := controlclient.Persist{ + c := persist.Persist{ LoginName: "test@example.com", } p := Prefs{ @@ -340,14 +362,14 @@ func TestPrefsPretty(t *testing.T) { }, { Prefs{ - Persist: &controlclient.Persist{}, + Persist: &persist.Persist{}, }, "linux", `Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`, }, { Prefs{ - Persist: &controlclient.Persist{ + Persist: &persist.Persist{ PrivateNodeKey: wgkey.Private{1: 1}, }, }, diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index 3a07479a4..add1874a6 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -17,6 +17,7 @@ import ( "log" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -338,6 +339,18 @@ func New(collection string) *Policy { tryFixLogStateLocation(dir, cmdName) cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName)) + + // The Windows service previously ran as tailscale-ipn.exe, so + // let's keep using that log base name if it exists. + if runtime.GOOS == "windows" && cmdName == "tailscaled" { + const oldCmdName = "tailscale-ipn" + oldPath := filepath.Join(dir, oldCmdName+".log.conf") + if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() { + cfgPath = oldPath + cmdName = oldCmdName + } + } + var oldc *Config data, err := ioutil.ReadFile(cfgPath) if err != nil { @@ -387,6 +400,13 @@ func New(collection string) *Policy { HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)}, } + if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok { + log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.") + c.BaseURL = val + u, _ := url.Parse(val) + c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)} + } + filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{}) if filchBuf != nil { c.Buffer = filchBuf diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 725515887..e0d249393 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -197,10 +197,9 @@ func (s *State) String() string { fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface) ifs := make([]string, 0, len(s.InterfaceUp)) for k := range s.InterfaceUp { - if allLoopbackIPs(s.InterfaceIPs[k]) { - continue + if anyInterestingIP(s.InterfaceIPs[k]) { + ifs = append(ifs, k) } - ifs = append(ifs, k) } sort.Slice(ifs, func(i, j int) bool { upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]] @@ -218,7 +217,7 @@ func (s *State) String() string { fmt.Fprintf(&sb, "%s:[", ifName) needSpace := false for _, ip := range s.InterfaceIPs[ifName] { - if ip.IsLinkLocalUnicast() { + if !isInterestingIP(ip) { continue } if needSpace { @@ -403,14 +402,23 @@ var ( v6Global1 = mustCIDR("2000::/3") ) -func allLoopbackIPs(ips []netaddr.IP) bool { - if len(ips) == 0 { - return false - } +// anyInterestingIP reports ips contains any IP that matches +// isInterestingIP. +func anyInterestingIP(ips []netaddr.IP) bool { for _, ip := range ips { - if !ip.IsLoopback() { - return false + if isInterestingIP(ip) { + return true } } + return false +} + +// isInterestingIP reports whether ip is an interesting IP that we +// should log in interfaces.State logging. We don't need to show +// localhost or link-local addresses. +func isInterestingIP(ip netaddr.IP) bool { + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return false + } return true } diff --git a/net/interfaces/interfaces_darwin_tailscaled.go b/net/interfaces/interfaces_darwin_tailscaled.go new file mode 100644 index 000000000..1dd598619 --- /dev/null +++ b/net/interfaces/interfaces_darwin_tailscaled.go @@ -0,0 +1,81 @@ +// 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. + +// +build darwin,!redo,!ios +// (Exclude redo, because we don't want this code in the App Store +// version's sandbox, where it won't work, and also don't want it on +// iOS. This is just for utun-using non-sandboxed cmd/tailscaled on macOS. + +package interfaces + +import ( + "errors" + "fmt" + "net" + "syscall" + + "golang.org/x/net/route" +) + +func DefaultRouteInterface() (string, error) { + idx, err := DefaultRouteInterfaceIndex() + if err != nil { + return "", err + } + iface, err := net.InterfaceByIndex(idx) + if err != nil { + return "", err + } + return iface.Name, nil +} + +func DefaultRouteInterfaceIndex() (int, error) { + // $ netstat -nr + // Routing tables + // Internet: + // Destination Gateway Flags Netif Expire + // default 10.0.0.1 UGSc en0 <-- want this one + // default 10.0.0.1 UGScI en1 + + // From man netstat: + // U RTF_UP Route usable + // G RTF_GATEWAY Destination requires forwarding by intermediary + // S RTF_STATIC Manually added + // c RTF_PRCLONING Protocol-specified generate new routes on use + // I RTF_IFSCOPE Route is associated with an interface scope + + rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0) + if err != nil { + return 0, fmt.Errorf("route.FetchRIB: %w", err) + } + msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib) + if err != nil { + return 0, fmt.Errorf("route.ParseRIB: %w", err) + } + indexSeen := map[int]int{} // index => count + for _, m := range msgs { + rm, ok := m.(*route.RouteMessage) + if !ok { + continue + } + const RTF_GATEWAY = 0x2 + const RTF_IFSCOPE = 0x1000000 + if rm.Flags&RTF_GATEWAY == 0 { + continue + } + if rm.Flags&RTF_IFSCOPE != 0 { + continue + } + indexSeen[rm.Index]++ + } + if len(indexSeen) == 0 { + return 0, errors.New("no gateway index found") + } + if len(indexSeen) == 1 { + for idx := range indexSeen { + return idx, nil + } + } + return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen) +} diff --git a/net/interfaces/interfaces_default_route_test.go b/net/interfaces/interfaces_default_route_test.go new file mode 100644 index 000000000..d88bdf685 --- /dev/null +++ b/net/interfaces/interfaces_default_route_test.go @@ -0,0 +1,17 @@ +// 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. + +// +build linux darwin,!redo + +package interfaces + +import "testing" + +func TestDefaultRouteInterface(t *testing.T) { + v, err := DefaultRouteInterface() + if err != nil { + t.Fatal(err) + } + t.Logf("got %q", v) +} diff --git a/net/interfaces/interfaces_defaultrouteif_todo.go b/net/interfaces/interfaces_defaultrouteif_todo.go index a5067151a..255543336 100644 --- a/net/interfaces/interfaces_defaultrouteif_todo.go +++ b/net/interfaces/interfaces_defaultrouteif_todo.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !linux,!windows +// +build !linux,!windows,!darwin darwin,redo package interfaces diff --git a/net/netns/netns_darwin_tailscaled.go b/net/netns/netns_darwin_tailscaled.go new file mode 100644 index 000000000..a5a323fd2 --- /dev/null +++ b/net/netns/netns_darwin_tailscaled.go @@ -0,0 +1,52 @@ +// 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. + +// +build darwin,!redo + +package netns + +import ( + "fmt" + "log" + "strings" + "syscall" + + "golang.org/x/sys/unix" + "tailscale.com/net/interfaces" +) + +// control marks c as necessary to dial in a separate network namespace. +// +// It's intentionally the same signature as net.Dialer.Control +// and net.ListenConfig.Control. +func control(network, address string, c syscall.RawConn) error { + if strings.HasPrefix(address, "127.") || address == "::1" { + // Don't bind to an interface for localhost connections. + return nil + } + idx, err := interfaces.DefaultRouteInterfaceIndex() + if err != nil { + log.Printf("netns: DefaultRouteInterfaceIndex: %v", err) + return nil + } + v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 + proto := unix.IPPROTO_IP + opt := unix.IP_BOUND_IF + if v6 { + proto = unix.IPPROTO_IPV6 + opt = unix.IPV6_BOUND_IF + } + + var sockErr error + err = c.Control(func(fd uintptr) { + sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx) + }) + if err != nil { + return fmt.Errorf("RawConn.Control on %T: %w", c, err) + } + if sockErr != nil { + log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr) + } + return sockErr +} diff --git a/net/netns/netns_default.go b/net/netns/netns_default.go index e794fccb7..0a0e0179b 100644 --- a/net/netns/netns_default.go +++ b/net/netns/netns_default.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !linux,!windows +// +build !linux,!windows,!darwin darwin,redo package netns diff --git a/net/netns/netns_test.go b/net/netns/netns_test.go new file mode 100644 index 000000000..0e3eb963f --- /dev/null +++ b/net/netns/netns_test.go @@ -0,0 +1,42 @@ +// 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 netns contains the common code for using the Go net package +// in a logical "network namespace" to avoid routing loops where +// Tailscale-created packets would otherwise loop back through +// Tailscale routes. +// +// Despite the name netns, the exact mechanism used differs by +// operating system, and perhaps even by version of the OS. +// +// The netns package also handles connecting via SOCKS proxies when +// configured by the environment. +package netns + +import ( + "flag" + "testing" +) + +var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests") + +func TestDial(t *testing.T) { + if !*extNetwork { + t.Skip("skipping test without --use-external-network") + } + d := NewDialer() + c, err := d.Dial("tcp", "google.com:80") + if err != nil { + t.Fatal(err) + } + defer c.Close() + t.Logf("got addr %v", c.RemoteAddr()) + + c, err = d.Dial("tcp4", "google.com:80") + if err != nil { + t.Fatal(err) + } + defer c.Close() + t.Logf("got addr %v", c.RemoteAddr()) +} diff --git a/net/packet/tsmp.go b/net/packet/tsmp.go index 8b46a6c98..2346c9419 100644 --- a/net/packet/tsmp.go +++ b/net/packet/tsmp.go @@ -23,12 +23,14 @@ import ( // Tailscale node has rejected the connection from another. Unlike a // TCP RST, this includes a reason. // -// On the wire, after the IP header, it's currently 7 bytes: +// On the wire, after the IP header, it's currently 7 or 8 bytes: // * '!' // * IPProto byte (IANA protocol number: TCP or UDP) // * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp) // * srcPort big endian uint16 // * dstPort big endian uint16 +// * [optional] byte of flag bits: +// lowest bit (0x1): MaybeBroken // // In the future it might also accept 16 byte IP flow src/dst IPs // after the header, if they're different than the IP-level ones. @@ -39,8 +41,21 @@ type TailscaleRejectedHeader struct { Dst netaddr.IPPort // rejected flow's dst Proto IPProto // proto that was rejected (TCP or UDP) Reason TailscaleRejectReason // why the connection was rejected + + // MaybeBroken is whether the rejection is non-terminal (the + // client should not fail immediately). This is sent by a + // target when it's not sure whether it's totally broken, but + // it might be. For example, the target tailscaled might think + // its host firewall or IP forwarding aren't configured + // properly, but tailscaled might be wrong (not having enough + // visibility into what the OS is doing). When true, the + // message is simply an FYI as a potential reason to use for + // later when the pendopen connection tracking timer expires. + MaybeBroken bool } +const rejectFlagBitMaybeBroken = 0x1 + func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple { return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst} } @@ -52,14 +67,32 @@ func (rh TailscaleRejectedHeader) String() string { type TSMPType uint8 const ( + // TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader. TSMPTypeRejectedConn TSMPType = '!' ) type TailscaleRejectReason byte +// IsZero reports whether r is the zero value, representing no rejection. +func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone } + const ( - RejectedDueToACLs TailscaleRejectReason = 'A' + // TailscaleRejectReasonNone is the TailscaleRejectReason zero value. + TailscaleRejectReasonNone TailscaleRejectReason = 0 + + // RejectedDueToACLs means that the host rejected the connection due to ACLs. + RejectedDueToACLs TailscaleRejectReason = 'A' + + // RejectedDueToShieldsUp means that the host rejected the connection due to shields being up. RejectedDueToShieldsUp TailscaleRejectReason = 'S' + + // RejectedDueToIPForwarding means that the relay node's IP + // forwarding is disabled. + RejectedDueToIPForwarding TailscaleRejectReason = 'F' + + // RejectedDueToHostFirewall means that the target host's + // firewall is blocking the traffic. + RejectedDueToHostFirewall TailscaleRejectReason = 'W' ) func (r TailscaleRejectReason) String() string { @@ -68,22 +101,32 @@ func (r TailscaleRejectReason) String() string { return "acl" case RejectedDueToShieldsUp: return "shields" + case RejectedDueToIPForwarding: + return "host-ip-forwarding-unavailable" + case RejectedDueToHostFirewall: + return "host-firewall" } return fmt.Sprintf("0x%02x", byte(r)) } +func (h TailscaleRejectedHeader) hasFlags() bool { + return h.MaybeBroken // the only one currently +} + func (h TailscaleRejectedHeader) Len() int { - var ipHeaderLen int - if h.IPSrc.Is4() { - ipHeaderLen = ip4HeaderLength - } else if h.IPSrc.Is6() { - ipHeaderLen = ip6HeaderLength - } - return ipHeaderLen + - 1 + // TSMPType byte + v := 1 + // TSMPType byte 1 + // IPProto byte 1 + // TailscaleRejectReason byte 2*2 // 2 uint16 ports + if h.IPSrc.Is4() { + v += ip4HeaderLength + } else if h.IPSrc.Is6() { + v += ip6HeaderLength + } + if h.hasFlags() { + v++ + } + return v } func (h TailscaleRejectedHeader) Marshal(buf []byte) error { @@ -117,6 +160,14 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error { buf[2] = byte(h.Reason) binary.BigEndian.PutUint16(buf[3:5], h.Src.Port) binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port) + + if h.hasFlags() { + var flags byte + if h.MaybeBroken { + flags |= rejectFlagBitMaybeBroken + } + buf[7] = flags + } return nil } @@ -129,12 +180,17 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) { return } - return TailscaleRejectedHeader{ + h = TailscaleRejectedHeader{ Proto: IPProto(p[1]), Reason: TailscaleRejectReason(p[2]), IPSrc: pp.Src.IP, IPDst: pp.Dst.IP, Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])}, Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])}, - }, true + } + if len(p) > 7 { + flags := p[7] + h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0 + } + return h, true } diff --git a/net/packet/tsmp_test.go b/net/packet/tsmp_test.go index 71e4f9439..d4a0cf1a0 100644 --- a/net/packet/tsmp_test.go +++ b/net/packet/tsmp_test.go @@ -37,6 +37,18 @@ func TestTailscaleRejectedHeader(t *testing.T) { }, wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields", }, + { + h: TailscaleRejectedHeader{ + IPSrc: netaddr.MustParseIP("2::2"), + IPDst: netaddr.MustParseIP("1::1"), + Src: netaddr.MustParseIPPort("[1::1]:567"), + Dst: netaddr.MustParseIPPort("[2::2]:443"), + Proto: UDP, + Reason: RejectedDueToIPForwarding, + MaybeBroken: true, + }, + wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: host-ip-forwarding-unavailable", + }, } for i, tt := range tests { gotStr := tt.h.String() diff --git a/net/stun/stuntest/stuntest.go b/net/stun/stuntest/stuntest.go index e97accbb8..6015b9066 100644 --- a/net/stun/stuntest/stuntest.go +++ b/net/stun/stuntest/stuntest.go @@ -59,6 +59,7 @@ func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- stru for { n, addr, err := pc.ReadFrom(buf[:]) if err != nil { + // TODO: when we switch to Go 1.16, replace this with errors.Is(err, net.ErrClosed) if strings.Contains(err.Error(), "closed network connection") { t.Logf("STUN server shutdown") return diff --git a/paths/paths.go b/paths/paths.go index 9e583713a..b42b2864f 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -8,6 +8,7 @@ package paths import ( "os" + "path/filepath" "runtime" ) @@ -27,6 +28,9 @@ func DefaultTailscaledSocket() string { if runtime.GOOS == "windows" { return "" } + if runtime.GOOS == "darwin" { + return "/var/run/tailscaled.socket" + } if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() { return "/var/run/tailscale/tailscaled.sock" } @@ -42,5 +46,8 @@ func DefaultTailscaledStateFile() string { if f := stateFileFunc; f != nil { return f() } + if runtime.GOOS == "windows" { + return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf") + } return "" } diff --git a/paths/paths_unix.go b/paths/paths_unix.go index bde26948e..1633fc32e 100644 --- a/paths/paths_unix.go +++ b/paths/paths_unix.go @@ -23,6 +23,8 @@ func statePath() string { return "/var/lib/tailscale/tailscaled.state" case "freebsd", "openbsd": return "/var/db/tailscale/tailscaled.state" + case "darwin": + return "/Library/Tailscale/tailscaled.state" default: return "" } diff --git a/portlist/netstat.go b/portlist/netstat.go index e0f9345be..02b1a5957 100644 --- a/portlist/netstat.go +++ b/portlist/netstat.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !darwin !arm64 +// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64 package portlist diff --git a/portlist/netstat_exec.go b/portlist/netstat_exec.go index 2ca3fd574..dd78215ec 100644 --- a/portlist/netstat_exec.go +++ b/portlist/netstat_exec.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build windows freebsd openbsd darwin,amd64 +// +build windows freebsd openbsd darwin,amd64 go1.16,darwin,arm64 package portlist diff --git a/portlist/portlist_ios.go b/portlist/portlist_ios.go index a7a85a54c..19bc2db39 100644 --- a/portlist/portlist_ios.go +++ b/portlist/portlist_ios.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin,!amd64 +// +build go1.16,ios !go1.16,darwin,!amd64 package portlist diff --git a/portlist/portlist_macos.go b/portlist/portlist_macos.go index 66bd558b8..86dd3058e 100644 --- a/portlist/portlist_macos.go +++ b/portlist/portlist_macos.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin,amd64 +// +build darwin,amd64 go1.16,darwin,arm64 package portlist diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go index 19e183463..5cbf73770 100644 --- a/safesocket/safesocket.go +++ b/safesocket/safesocket.go @@ -10,6 +10,8 @@ import ( "errors" "net" "runtime" + + "tailscale.com/paths" ) type closeable interface { @@ -31,7 +33,7 @@ func ConnCloseWrite(c net.Conn) error { // ConnectDefault connects to the local Tailscale daemon. func ConnectDefault() (net.Conn, error) { - return Connect("/var/run/tailscale/tailscaled.sock", 41112) + return Connect(paths.DefaultTailscaledSocket(), 41112) } // Connect connects to either path (on Unix) or the provided localhost port (on Windows). diff --git a/safesocket/unixsocket.go b/safesocket/unixsocket.go index fd2a58852..f86d55367 100644 --- a/safesocket/unixsocket.go +++ b/safesocket/unixsocket.go @@ -13,6 +13,7 @@ import ( "log" "net" "os" + "os/exec" "path/filepath" "runtime" "strconv" @@ -54,6 +55,9 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) { c, err := net.Dial("unix", path) if err == nil { c.Close() + if tailscaledRunningUnderLaunchd() { + return nil, 0, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path) + } return nil, 0, fmt.Errorf("%v: address already in use", path) } _ = os.Remove(path) @@ -86,11 +90,22 @@ func listen(path string, port uint16) (ln net.Listener, _ uint16, err error) { return pipe, 0, err } +func tailscaledRunningUnderLaunchd() bool { + if runtime.GOOS != "darwin" { + return false + } + plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output() + _ = plist // parse it? https://github.com/DHowett/go-plist if we need something. + running := err == nil + return running +} + // socketPermissionsForOS returns the permissions to use for the // tailscaled.sock. func socketPermissionsForOS() os.FileMode { - if runtime.GOOS == "linux" { - // On Linux, the ipn/ipnserver package looks at the Unix peer creds + switch runtime.GOOS { + case "linux", "darwin": + // On Linux and Darwin, the ipn/ipnserver package looks at the Unix peer creds // and only permits read-only actions from non-root users, so we want // this opened up wider. // diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index bcb7ec205..f687c9b04 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -406,6 +406,7 @@ type Hostinfo struct { BackendLogID string `json:",omitempty"` // logtail ID of backend instance OS string // operating system the client runs on (a version.OS value) OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041") + Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown) DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro") Hostname string // name of the host the client runs on ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index 87bbb2484..ee57f8113 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -107,6 +107,7 @@ var _HostinfoNeedsRegeneration = Hostinfo(struct { BackendLogID string OS string OSVersion string + Package string DeviceModel string Hostname string ShieldsUp bool diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index a6c843db5..5fa579b0d 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -25,7 +25,7 @@ func fieldsOf(t reflect.Type) (fields []string) { func TestHostinfoEqual(t *testing.T) { hiHandles := []string{ "IPNVersion", "FrontendLogID", "BackendLogID", - "OS", "OSVersion", "DeviceModel", "Hostname", + "OS", "OSVersion", "Package", "DeviceModel", "Hostname", "ShieldsUp", "ShareeNode", "GoArch", "RoutableIPs", "RequestTags", diff --git a/tstest/natlab/natlab.go b/tstest/natlab/natlab.go index 7d1508afe..df2611be4 100644 --- a/tstest/natlab/natlab.go +++ b/tstest/natlab/natlab.go @@ -26,6 +26,7 @@ import ( "sync" "time" + wgconn "github.com/tailscale/wireguard-go/conn" "inet.af/netaddr" ) @@ -758,7 +759,8 @@ func (c *conn) canRead() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { - return errors.New("closed network connection") // sadface: magic string used by other; don't change + // TODO: when we switch to Go 1.16, replace this with net.ErrClosed + return wgconn.NetErrClosed } if !c.readDeadline.IsZero() && c.readDeadline.Before(time.Now()) { return errors.New("read deadline exceeded") diff --git a/control/controlclient/netmap.go b/types/netmap/netmap.go similarity index 67% rename from control/controlclient/netmap.go rename to types/netmap/netmap.go index 40041a491..558c74637 100644 --- a/control/controlclient/netmap.go +++ b/types/netmap/netmap.go @@ -2,23 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package controlclient +// Package netmap contains the netmap.NetworkMap type. +package netmap import ( "encoding/json" "fmt" - "net" "reflect" - "strconv" "strings" "time" "inet.af/netaddr" "tailscale.com/tailcfg" - "tailscale.com/types/logger" "tailscale.com/types/wgkey" "tailscale.com/wgengine/filter" - "tailscale.com/wgengine/wgcfg" ) type NetworkMap struct { @@ -249,126 +246,8 @@ type WGConfigFlags int const ( AllowSingleHosts WGConfigFlags = 1 << iota AllowSubnetRoutes - AllowDefaultRoute ) -// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key -// and is then the sole wireguard endpoint for peers with a non-zero discovery key. -// This form is then recognize by magicsock's CreateEndpoint. -const EndpointDiscoSuffix = ".disco.tailscale:12345" - -// WGCfg returns the NetworkMaps's Wireguard configuration. -func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) { - cfg := &wgcfg.Config{ - Name: "tailscale", - PrivateKey: wgcfg.PrivateKey(nm.PrivateKey), - Addresses: nm.Addresses, - ListenPort: nm.LocalPort, - Peers: make([]wgcfg.Peer, 0, len(nm.Peers)), - } - - for _, peer := range nm.Peers { - if Debug.OnlyDisco && peer.DiscoKey.IsZero() { - continue - } - if (flags&AllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 { - logf("wgcfg: %v skipping a single-host peer.", peer.Key.ShortString()) - continue - } - cfg.Peers = append(cfg.Peers, wgcfg.Peer{ - PublicKey: wgcfg.Key(peer.Key), - }) - cpeer := &cfg.Peers[len(cfg.Peers)-1] - if peer.KeepAlive { - cpeer.PersistentKeepalive = 25 // seconds - } - - if !peer.DiscoKey.IsZero() { - if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], EndpointDiscoSuffix)); err != nil { - return nil, err - } - cpeer.Endpoints = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:]) - } else { - if err := appendEndpoint(cpeer, peer.DERP); err != nil { - return nil, err - } - for _, ep := range peer.Endpoints { - if err := appendEndpoint(cpeer, ep); err != nil { - return nil, err - } - } - } - for _, allowedIP := range peer.AllowedIPs { - if allowedIP.Bits == 0 { - if (flags & AllowDefaultRoute) == 0 { - logf("[v1] wgcfg: not accepting default route from %q (%v)", - nodeDebugName(peer), peer.Key.ShortString()) - continue - } - } else if cidrIsSubnet(peer, allowedIP) { - if (flags & AllowSubnetRoutes) == 0 { - logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)", - allowedIP, nodeDebugName(peer), peer.Key.ShortString()) - continue - } - } - cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP) - } - } - - return cfg, nil -} - -func nodeDebugName(n *tailcfg.Node) string { - name := n.Name - if name == "" { - name = n.Hostinfo.Hostname - } - if i := strings.Index(name, "."); i != -1 { - name = name[:i] - } - if name == "" && len(n.Addresses) != 0 { - return n.Addresses[0].String() - } - return name -} - -// cidrIsSubnet reports whether cidr is a non-default-route subnet -// exported by node that is not one of its own self addresses. -func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool { - if cidr.Bits == 0 { - return false - } - if !cidr.IsSingleIP() { - return true - } - for _, selfCIDR := range node.Addresses { - if cidr == selfCIDR { - return false - } - } - return true -} - -func appendEndpoint(peer *wgcfg.Peer, epStr string) error { - if epStr == "" { - return nil - } - _, port, err := net.SplitHostPort(epStr) - if err != nil { - return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString()) - } - _, err = strconv.ParseUint(port, 10, 16) - if err != nil { - return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString()) - } - if peer.Endpoints != "" { - peer.Endpoints += "," - } - peer.Endpoints += epStr - return nil -} - // eqStringsIgnoreNil reports whether a and b have the same length and // contents, but ignore whether a or b are nil. func eqStringsIgnoreNil(a, b []string) bool { diff --git a/control/controlclient/netmap_test.go b/types/netmap/netmap_test.go similarity index 96% rename from control/controlclient/netmap_test.go rename to types/netmap/netmap_test.go index 5bb529ab0..977a64cf0 100644 --- a/control/controlclient/netmap_test.go +++ b/types/netmap/netmap_test.go @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package controlclient +package netmap import ( "encoding/hex" - "encoding/json" "testing" "inet.af/netaddr" @@ -283,15 +282,3 @@ func TestConciseDiffFrom(t *testing.T) { }) } } - -func TestNewHostinfo(t *testing.T) { - hi := NewHostinfo() - if hi == nil { - t.Fatal("no Hostinfo") - } - j, err := json.MarshalIndent(hi, " ", "") - if err != nil { - t.Fatal(err) - } - t.Logf("Got: %s", j) -} diff --git a/types/persist/persist.go b/types/persist/persist.go new file mode 100644 index 000000000..169288280 --- /dev/null +++ b/types/persist/persist.go @@ -0,0 +1,73 @@ +// 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 persist contains the Persist type. +package persist + +import ( + "fmt" + + "tailscale.com/types/structs" + "tailscale.com/types/wgkey" +) + +//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=persist_clone.go + +// Persist is the JSON type stored on disk on nodes to remember their +// settings between runs. +type Persist struct { + _ structs.Incomparable + + // LegacyFrontendPrivateMachineKey is here temporarily + // (starting 2020-09-28) during migration of Windows users' + // machine keys from frontend storage to the backend. On the + // first LocalBackend.Start call, the backend will initialize + // the real (backend-owned) machine key from the frontend's + // provided value (if non-zero), picking a new random one if + // needed. This field should be considered read-only from GUI + // frontends. The real value should not be written back in + // this field, lest the frontend persist it to disk. + LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"` + + PrivateNodeKey wgkey.Private + OldPrivateNodeKey wgkey.Private // needed to request key rotation + Provider string + LoginName string +} + +func (p *Persist) Equals(p2 *Persist) bool { + if p == nil && p2 == nil { + return true + } + if p == nil || p2 == nil { + return false + } + + return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) && + p.PrivateNodeKey.Equal(p2.PrivateNodeKey) && + p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) && + p.Provider == p2.Provider && + p.LoginName == p2.LoginName +} + +func (p *Persist) Pretty() string { + var mk, ok, nk wgkey.Key + if !p.LegacyFrontendPrivateMachineKey.IsZero() { + mk = p.LegacyFrontendPrivateMachineKey.Public() + } + if !p.OldPrivateNodeKey.IsZero() { + ok = p.OldPrivateNodeKey.Public() + } + if !p.PrivateNodeKey.IsZero() { + nk = p.PrivateNodeKey.Public() + } + ss := func(k wgkey.Key) string { + if k.IsZero() { + return "" + } + return k.ShortString() + } + return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}", + ss(mk), ss(ok), ss(nk), p.LoginName) +} diff --git a/types/persist/persist_clone.go b/types/persist/persist_clone.go new file mode 100644 index 000000000..533e9294d --- /dev/null +++ b/types/persist/persist_clone.go @@ -0,0 +1,34 @@ +// 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. + +// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT. + +package persist + +import ( + "tailscale.com/types/structs" + "tailscale.com/types/wgkey" +) + +// Clone makes a deep copy of Persist. +// The result aliases no memory with the original. +func (src *Persist) Clone() *Persist { + if src == nil { + return nil + } + dst := new(Persist) + *dst = *src + return dst +} + +// A compilation failure here means this code must be regenerated, with command: +// tailscale.com/cmd/cloner -type Persist +var _PersistNeedsRegeneration = Persist(struct { + _ structs.Incomparable + LegacyFrontendPrivateMachineKey wgkey.Private + PrivateNodeKey wgkey.Private + OldPrivateNodeKey wgkey.Private + Provider string + LoginName string +}{}) diff --git a/control/controlclient/persist_test.go b/types/persist/persist_test.go similarity index 91% rename from control/controlclient/persist_test.go rename to types/persist/persist_test.go index efee06273..04fdb8bc3 100644 --- a/control/controlclient/persist_test.go +++ b/types/persist/persist_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package controlclient +package persist import ( "reflect" @@ -11,6 +11,15 @@ import ( "tailscale.com/types/wgkey" ) +func fieldsOf(t reflect.Type) (fields []string) { + for i := 0; i < t.NumField(); i++ { + if name := t.Field(i).Name; name != "_" { + fields = append(fields, name) + } + } + return +} + func TestPersistEqual(t *testing.T) { persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"} if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) { diff --git a/types/preftype/netfiltermode.go b/types/preftype/netfiltermode.go new file mode 100644 index 000000000..7e8dec9dd --- /dev/null +++ b/types/preftype/netfiltermode.go @@ -0,0 +1,30 @@ +// 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 preftype is a leaf package containing types for various +// preferences. +package preftype + +// NetfilterMode is the firewall management mode to use when +// programming the Linux network stack. +type NetfilterMode int + +const ( + NetfilterOff NetfilterMode = iota // remove all tailscale netfilter state + NetfilterNoDivert // manage tailscale chains, but don't call them + NetfilterOn // manage tailscale chains and call them from main chains +) + +func (m NetfilterMode) String() string { + switch m { + case NetfilterOff: + return "off" + case NetfilterNoDivert: + return "nodivert" + case NetfilterOn: + return "on" + default: + return "???" + } +} diff --git a/util/cibuild/cibuild.go b/util/cibuild/cibuild.go new file mode 100644 index 000000000..b2d4af20c --- /dev/null +++ b/util/cibuild/cibuild.go @@ -0,0 +1,13 @@ +// 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 cibuild reports runtime CI information. +package cibuild + +import "os" + +// On reports whether the current binary is executing on a CI system. +func On() bool { + return os.Getenv("GITHUB_ACTIONS") != "" +} diff --git a/version/cmdname.go b/version/cmdname.go index a7899ed9f..832563532 100644 --- a/version/cmdname.go +++ b/version/cmdname.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !darwin !arm64 +// +build go1.16,!ios !go1.16,!darwin !go1.16,!arm64 package version diff --git a/version/cmdname_ios.go b/version/cmdname_ios.go index 69d71f7db..514da5da0 100644 --- a/version/cmdname_ios.go +++ b/version/cmdname_ios.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build darwin,arm64 +// +build go1.16,ios !go1.16,darwin,arm64 package version diff --git a/version/version.go b/version/version.go index 5ddd6ff91..9811be774 100644 --- a/version/version.go +++ b/version/version.go @@ -10,7 +10,7 @@ package version // Long is a full version number for this build, of the form // "x.y.z-commithash", or "date.yyyymmdd" if no actual version was // provided. -const Long = "date.20210104" +const Long = "date.20210211" // Short is a short version number for this build, of the form // "x.y.z", or "date.yyyymmdd" if no actual version was provided. diff --git a/wgengine/magicsock/legacy.go b/wgengine/magicsock/legacy.go index 7620cc1ce..eb4ce9da6 100644 --- a/wgengine/magicsock/legacy.go +++ b/wgengine/magicsock/legacy.go @@ -53,7 +53,6 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End return nil, fmt.Errorf("bogus address %q", ep) } a.ipPorts = append(a.ipPorts, ipp) - a.addrs = append(a.addrs, *ipp.UDPAddr()) } } @@ -84,14 +83,14 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End return a, nil } -func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, packet []byte) conn.Endpoint { +func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, packet []byte) conn.Endpoint { if c.disableLegacy { return nil } // Pre-disco: look up their addrSet. if as, ok := c.addrsByUDP[ipp]; ok { - as.updateDst(addr) + as.updateDst(ipp) return as } @@ -100,7 +99,7 @@ func (c *Conn) findLegacyEndpointLocked(ipp netaddr.IPPort, addr *net.UDPAddr, p // know. If this is a handshake packet, we can try to identify the // peer in question. if as := c.peerFromPacketLocked(packet); as != nil { - as.updateDst(addr) + as.updateDst(ipp) return as } @@ -268,14 +267,6 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP as.lastSend = now - // Some internal invariant checks. - if len(as.addrs) != len(as.ipPorts) { - panic(fmt.Sprintf("lena %d != leni %d", len(as.addrs), len(as.ipPorts))) - } - if n1, n2 := as.roamAddr != nil, as.roamAddrStd != nil; n1 != n2 { - panic(fmt.Sprintf("roamnil %v != roamstdnil %v", n1, n2)) - } - // Spray logic. // // After exchanging a handshake with a peer, we send some outbound @@ -320,8 +311,8 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP // roamAddr should be special like this. dsts = append(dsts, *as.roamAddr) case as.curAddr != -1: - if as.curAddr >= len(as.addrs) { - as.Logf("[unexpected] magicsock bug: as.curAddr >= len(as.addrs): %d >= %d", as.curAddr, len(as.addrs)) + if as.curAddr >= len(as.ipPorts) { + as.Logf("[unexpected] magicsock bug: as.curAddr >= len(as.ipPorts): %d >= %d", as.curAddr, len(as.ipPorts)) break } // No roaming addr, but we've seen packets from a known peer @@ -352,15 +343,14 @@ func (as *addrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP type addrSet struct { publicKey key.Public // peer public key used for DERP communication - // addrs is an ordered priority list provided by wgengine, + // ipPorts is an ordered priority list provided by wgengine, // sorted from expensive+slow+reliable at the begnining to // fast+cheap at the end. More concretely, it's typically: // // [DERP fakeip:node, Global IP:port, LAN ip:port] // // But there could be multiple or none of each. - addrs []net.UDPAddr - ipPorts []netaddr.IPPort // same as addrs, in different form + ipPorts []netaddr.IPPort // clock, if non-nil, is used in tests instead of time.Now. clock func() time.Time @@ -376,8 +366,7 @@ type addrSet struct { // this should hopefully never be used (or at least used // rarely) in the case that all the components of Tailscale // are correctly learning/sharing the network map details. - roamAddr *netaddr.IPPort - roamAddrStd *net.UDPAddr + roamAddr *netaddr.IPPort // curAddr is an index into addrs of the highest-priority // address a valid packet has been received from so far. @@ -400,9 +389,9 @@ type addrSet struct { // derpID returns this addrSet's home DERP node, or 0 if none is found. func (as *addrSet) derpID() int { - for _, ua := range as.addrs { - if ua.IP.Equal(derpMagicIP) { - return ua.Port + for _, ua := range as.ipPorts { + if ua.IP == derpMagicIPAddr { + return int(ua.Port) } } return 0 @@ -424,7 +413,7 @@ func (a *addrSet) dst() netaddr.IPPort { if a.roamAddr != nil { return *a.roamAddr } - if len(a.addrs) == 0 { + if len(a.ipPorts) == 0 { return noAddr } i := a.curAddr @@ -439,7 +428,7 @@ func (a *addrSet) DstToBytes() []byte { } func (a *addrSet) DstToString() string { var addrs []string - for _, addr := range a.addrs { + for _, addr := range a.ipPorts { addrs = append(addrs, addr.String()) } @@ -459,8 +448,8 @@ func (a *addrSet) ClearSrc() {} // updateDst records receipt of a packet from new. This is used to // potentially update the transmit address used for this addrSet. -func (a *addrSet) updateDst(new *net.UDPAddr) error { - if new.IP.Equal(derpMagicIP) { +func (a *addrSet) updateDst(new netaddr.IPPort) error { + if new.IP == derpMagicIPAddr { // Never consider DERP addresses as a viable candidate for // either curAddr or roamAddr. It's only ever a last resort // choice, never a preferred choice. @@ -471,25 +460,20 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error { a.mu.Lock() defer a.mu.Unlock() - if a.roamAddrStd != nil && equalUDPAddr(new, a.roamAddrStd) { + if a.roamAddr != nil && new == *a.roamAddr { // Packet from the current roaming address, no logging. // This is a hot path for established connections. return nil } - if a.roamAddr == nil && a.curAddr >= 0 && equalUDPAddr(new, &a.addrs[a.curAddr]) { + if a.roamAddr == nil && a.curAddr >= 0 && new == a.ipPorts[a.curAddr] { // Packet from current-priority address, no logging. // This is a hot path for established connections. return nil } - newa, ok := netaddr.FromStdAddr(new.IP, new.Port, new.Zone) - if !ok { - return nil - } - index := -1 - for i := range a.addrs { - if equalUDPAddr(new, &a.addrs[i]) { + for i := range a.ipPorts { + if new == a.ipPorts[i] { index = i break } @@ -499,7 +483,7 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error { pk := publicKey.ShortString() old := "" if a.curAddr >= 0 { - old = a.addrs[a.curAddr].String() + old = a.ipPorts[a.curAddr].String() } switch { @@ -509,18 +493,16 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error { } else { a.Logf("magicsock: rx %s from roaming address %s, replaces roaming address %s", pk, new, a.roamAddr) } - a.roamAddr = &newa - a.roamAddrStd = new + a.roamAddr = &new case a.roamAddr != nil: a.Logf("magicsock: rx %s from known %s (%d), replaces roaming address %s", pk, new, index, a.roamAddr) a.roamAddr = nil - a.roamAddrStd = nil a.curAddr = index a.loggedLogPriMask = 0 case a.curAddr == -1: - a.Logf("magicsock: rx %s from %s (%d/%d), set as new priority", pk, new, index, len(a.addrs)) + a.Logf("magicsock: rx %s from %s (%d/%d), set as new priority", pk, new, index, len(a.ipPorts)) a.curAddr = index a.loggedLogPriMask = 0 @@ -531,7 +513,7 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error { } default: // index > a.curAddr - a.Logf("magicsock: rx %s from %s (%d/%d), replaces old priority %s", pk, new, index, len(a.addrs), old) + a.Logf("magicsock: rx %s from %s (%d/%d), replaces old priority %s", pk, new, index, len(a.ipPorts), old) a.curAddr = index a.loggedLogPriMask = 0 } @@ -539,10 +521,6 @@ func (a *addrSet) updateDst(new *net.UDPAddr) error { return nil } -func equalUDPAddr(x, y *net.UDPAddr) bool { - return x.Port == y.Port && x.IP.Equal(y.IP) -} - func (a *addrSet) String() string { a.mu.Lock() defer a.mu.Unlock() @@ -551,9 +529,9 @@ func (a *addrSet) String() string { buf.WriteByte('[') if a.roamAddr != nil { buf.WriteString("roam:") - sbPrintAddr(buf, *a.roamAddrStd) + sbPrintAddr(buf, *a.roamAddr) } - for i, addr := range a.addrs { + for i, addr := range a.ipPorts { if i > 0 || a.roamAddr != nil { buf.WriteString(", ") } @@ -572,8 +550,8 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) { defer as.mu.Unlock() ps.LastWrite = as.lastSend - for i, ua := range as.addrs { - if ua.IP.Equal(derpMagicIP) { + for i, ua := range as.ipPorts { + if ua.IP == derpMagicIPAddr { continue } uaStr := ua.String() @@ -583,7 +561,7 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) { } } if as.roamAddr != nil { - ps.CurAddr = udpAddrDebugString(*as.roamAddrStd) + ps.CurAddr = ippDebugString(*as.roamAddr) } } diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index d28a07db3..407ca1720 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -12,6 +12,7 @@ import ( crand "crypto/rand" "encoding/binary" "errors" + "expvar" "fmt" "hash/fnv" "math" @@ -48,9 +49,11 @@ import ( "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/types/nettype" "tailscale.com/types/wgkey" "tailscale.com/version" + "tailscale.com/wgengine/wgcfg" ) // Various debugging and experimental tweakables, set by environment @@ -153,13 +156,10 @@ type Conn struct { // derpRecvCh is used by ReceiveIPv4 to read DERP messages. derpRecvCh chan derpReadResult - // derpRecvCountAtomic is atomically incremented by runDerpReader whenever - // a DERP message arrives. It's incremented before runDerpReader is interrupted. + // derpRecvCountAtomic is how many derpRecvCh sends are pending. + // It's incremented by runDerpReader whenever a DERP message + // arrives and decremented when they're read. derpRecvCountAtomic int64 - // derpRecvCountLast is used by ReceiveIPv4 to compare against - // its last read value of derpRecvCountAtomic to determine - // whether a DERP channel read should be done. - derpRecvCountLast int64 // owned by ReceiveIPv4 // ippEndpoint4 and ippEndpoint6 are owned by ReceiveIPv4 and // ReceiveIPv6, respectively, to cache an IPPort->endpoint for @@ -272,7 +272,7 @@ type Conn struct { netInfoLast *tailcfg.NetInfo derpMap *tailcfg.DERPMap // nil (or zero regions/nodes) means DERP is disabled - netMap *controlclient.NetworkMap + netMap *netmap.NetworkMap privateKey key.Private // WireGuard private key for this node everHadKey bool // whether we ever had a non-zero private key myDerp int // nearest DERP region ID; 0 means none/unknown @@ -304,6 +304,9 @@ type Conn struct { // with IPv4 or IPv6). It's used to suppress log spam and prevent // new connection that'll fail. networkUp syncs.AtomicBool + + // havePrivateKey is whether privateKey is non-zero. + havePrivateKey syncs.AtomicBool } // derpRoute is a route entry for a public key, saying that a certain @@ -345,8 +348,7 @@ func (c *Conn) addDerpPeerRoute(peer key.Public, derpID int, dc *derphttp.Client // Mnemonic: 3.3.40 are numbers above the keys D, E, R, P. const DerpMagicIP = "127.3.3.40" -var derpMagicIP = net.ParseIP(DerpMagicIP).To4() -var derpMagicIPAddr = netaddr.IPv4(127, 3, 3, 40) +var derpMagicIPAddr = netaddr.MustParseIP(DerpMagicIP) // activeDerp contains fields for an active DERP connection. type activeDerp struct { @@ -355,7 +357,7 @@ type activeDerp struct { writeCh chan<- derpWriteRequest // lastWrite is the time of the last request for its write // channel (currently even if there was no write). - // It is always non-nil and initialized to a non-zero Time[ + // It is always non-nil and initialized to a non-zero Time. lastWrite *time.Time createTime time.Time } @@ -773,7 +775,7 @@ func (c *Conn) SetNetInfoCallback(fn func(*tailcfg.NetInfo)) { // peerForIP returns the Node in nm that's responsible for // handling the given IP address. -func peerForIP(nm *controlclient.NetworkMap, ip netaddr.IP) (n *tailcfg.Node, ok bool) { +func peerForIP(nm *netmap.NetworkMap, ip netaddr.IP) (n *tailcfg.Node, ok bool) { if nm == nil { return nil, false } @@ -960,6 +962,13 @@ func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) { return true } +// startDerpHomeConnectLocked starts connecting to our DERP home, if any. +// +// c.mu must be held. +func (c *Conn) startDerpHomeConnectLocked() { + c.goDerpConnect(c.myDerp) +} + // goDerpConnect starts a goroutine to start connecting to the given // DERP node. // @@ -1353,6 +1362,8 @@ type derpReadResult struct { // copyBuf is called to copy the data to dst. It returns how // much data was copied, which will be n if dst is large // enough. copyBuf can only be called once. + // If copyBuf is nil, that's a signal from the sender to ignore + // this message. copyBuf func(dst []byte) int } @@ -1440,28 +1451,62 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d continue } - // Before we wake up ReceiveIPv4 with SetReadDeadline, - // note that a DERP packet has arrived. ReceiveIPv4 - // will read this field to note that its UDP read - // error is due to us. - atomic.AddInt64(&c.derpRecvCountAtomic, 1) - // Cancel the pconn read goroutine. - c.pconn4.SetReadDeadline(aLongTimeAgo) + if !c.sendDerpReadResult(ctx, res) { + return + } select { case <-ctx.Done(): return - case c.derpRecvCh <- res: - select { - case <-ctx.Done(): - return - case <-didCopy: - continue - } + case <-didCopy: + continue } } } +var ( + testCounterZeroDerpReadResultSend expvar.Int + testCounterZeroDerpReadResultRecv expvar.Int +) + +// sendDerpReadResult sends res to c.derpRecvCh and reports whether it +// was sent. (It reports false if ctx was done first.) +// +// This includes doing the whole wake-up dance to interrupt +// ReceiveIPv4's blocking UDP read. +func (c *Conn) sendDerpReadResult(ctx context.Context, res derpReadResult) (sent bool) { + // Before we wake up ReceiveIPv4 with SetReadDeadline, + // note that a DERP packet has arrived. ReceiveIPv4 + // will read this field to note that its UDP read + // error is due to us. + atomic.AddInt64(&c.derpRecvCountAtomic, 1) + // Cancel the pconn read goroutine. + c.pconn4.SetReadDeadline(aLongTimeAgo) + select { + case <-ctx.Done(): + select { + case <-c.donec: + // The whole Conn shut down. The reader of + // c.derpRecvCh also selects on c.donec, so it's + // safe to abort now. + case c.derpRecvCh <- (derpReadResult{}): + // Just this DERP reader is closing (perhaps + // the user is logging out, or the DERP + // connection is too idle for sends). Since we + // already incremented c.derpRecvCountAtomic, + // we need to send on the channel (unless the + // conn is going down). + // The receiver treats a derpReadResult zero value + // message as a skip. + testCounterZeroDerpReadResultSend.Add(1) + + } + return false + case c.derpRecvCh <- res: + return true + } +} + type derpWriteRequest struct { addr netaddr.IPPort pubKey key.Public @@ -1493,7 +1538,6 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan // findEndpoint maps from a UDP address to a WireGuard endpoint, for // ReceiveIPv4/ReceiveIPv6. -// The provided addr and ipp must match. // // TODO(bradfitz): add a fast path that returns nil here for normal // wireguard-go transport packets; wireguard-go only uses this @@ -1501,7 +1545,7 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan // Endpoint to find the UDPAddr to return to wireguard anyway, so no // benefit unless we can, say, always return the same fake UDPAddr for // all packets. -func (c *Conn) findEndpoint(ipp netaddr.IPPort, addr *net.UDPAddr, packet []byte) conn.Endpoint { +func (c *Conn) findEndpoint(ipp netaddr.IPPort, packet []byte) conn.Endpoint { c.mu.Lock() defer c.mu.Unlock() @@ -1513,10 +1557,7 @@ func (c *Conn) findEndpoint(ipp netaddr.IPPort, addr *net.UDPAddr, packet []byte } } - if addr == nil { - addr = ipp.UDPAddr() - } - return c.findLegacyEndpointLocked(ipp, addr, packet) + return c.findLegacyEndpointLocked(ipp, packet) } // aLongTimeAgo is a non-zero time, far in the past, used for @@ -1540,31 +1581,31 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, error) { return 0, nil, syscall.EAFNOSUPPORT } for { - n, pAddr, err := c.pconn6.ReadFrom(b) + n, ipp, err := c.pconn6.ReadFromNetaddr(b) if err != nil { return 0, nil, err } - if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint6); ok { + if ep, ok := c.receiveIP(b[:n], ipp, &c.ippEndpoint6); ok { return n, ep, nil } } } func (c *Conn) derpPacketArrived() bool { - rc := atomic.LoadInt64(&c.derpRecvCountAtomic) - if rc != c.derpRecvCountLast { - c.derpRecvCountLast = rc - return true - } - return false + return atomic.LoadInt64(&c.derpRecvCountAtomic) > 0 } // ReceiveIPv4 is called by wireguard-go to receive an IPv4 packet. // In Tailscale's case, that packet might also arrive via DERP. A DERP packet arrival // aborts the pconn4 read deadline to make it fail. func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, err error) { + var ipp netaddr.IPPort for { - n, pAddr, err := c.pconn4.ReadFrom(b) + // Drain DERP queues before reading new UDP packets. + if c.derpPacketArrived() { + goto ReadDERP + } + n, ipp, err = c.pconn4.ReadFromNetaddr(b) if err != nil { // If the pconn4 read failed, the likely reason is a DERP reader received // a packet and interrupted us. @@ -1572,27 +1613,29 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, err error) { // and for there to have also had a DERP packet arrive, but that's fine: // we'll get the same error from ReadFrom later. if c.derpPacketArrived() { - c.pconn4.SetReadDeadline(time.Time{}) // restore - n, ep, err = c.receiveIPv4DERP(b) - if err == errLoopAgain { - continue - } - return n, ep, err + goto ReadDERP } return 0, nil, err } - if ep, ok := c.receiveIP(b[:n], pAddr.(*net.UDPAddr), &c.ippEndpoint4); ok { + if ep, ok := c.receiveIP(b[:n], ipp, &c.ippEndpoint4); ok { return n, ep, nil + } else { + continue } + ReadDERP: + n, ep, err = c.receiveIPv4DERP(b) + if err == errLoopAgain { + continue + } + return n, ep, err } } // receiveIP is the shared bits of ReceiveIPv4 and ReceiveIPv6. -func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr, cache *ippEndpointCache) (ep conn.Endpoint, ok bool) { - ipp, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone) - if !ok { - return nil, false - } +// +// ok is whether this read should be reported up to wireguard-go (our +// caller). +func (c *Conn) receiveIP(b []byte, ipp netaddr.IPPort, cache *ippEndpointCache) (ep conn.Endpoint, ok bool) { if stun.Is(b) { c.stunReceiveFunc.Load().(func([]byte, netaddr.IPPort))(b, ipp) return nil, false @@ -1600,10 +1643,17 @@ func (c *Conn) receiveIP(b []byte, ua *net.UDPAddr, cache *ippEndpointCache) (ep if c.handleDiscoMessage(b, ipp) { return nil, false } + if !c.havePrivateKey.Get() { + // If we have no private key, we're logged out or + // stopped. Don't try to pass these wireguard packets + // up to wireguard-go; it'll just complain (Issue + // 1167). + return nil, false + } if cache.ipp == ipp && cache.de != nil && cache.gen == cache.de.numStopAndReset() { ep = cache.de } else { - ep = c.findEndpoint(ipp, ua, b) + ep = c.findEndpoint(ipp, b) if ep == nil { return nil, false } @@ -1641,6 +1691,13 @@ func (c *Conn) receiveIPv4DERP(b []byte) (n int, ep conn.Endpoint, err error) { case dm = <-c.derpRecvCh: // Below. } + if atomic.AddInt64(&c.derpRecvCountAtomic, -1) == 0 { + c.pconn4.SetReadDeadline(time.Time{}) + } + if dm.copyBuf == nil { + testCounterZeroDerpReadResultRecv.Add(1) + return 0, nil, errLoopAgain + } var regionID int n, regionID = dm.n, dm.regionID @@ -1693,7 +1750,7 @@ func (c *Conn) receiveIPv4DERP(b []byte) (n int, ep conn.Endpoint, err error) { } else { key := wgkey.Key(dm.src) c.logf("magicsock: DERP packet from unknown key: %s", key.ShortString()) - ep = c.findEndpoint(ipp, nil, b[:n]) + ep = c.findEndpoint(ipp, b[:n]) if ep == nil { return 0, nil, errLoopAgain } @@ -1750,8 +1807,8 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD return sent, err } -// handleDiscoMessage reports whether msg was a Tailscale inter-node discovery message -// that was handled. +// handleDiscoMessage handles a discovery message and reports whether +// msg was a Tailscale inter-node discovery message. // // A discovery message has the form: // @@ -1762,11 +1819,18 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD // // For messages received over DERP, the addr will be derpMagicIP (with // port being the region) -func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { +func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) (isDiscoMsg bool) { const headerLen = len(disco.Magic) + len(tailcfg.DiscoKey{}) + disco.NonceLen if len(msg) < headerLen || string(msg[:len(disco.Magic)]) != disco.Magic { return false } + + // If the first four parts are the prefix of disco.Magic + // (0x5453f09f) then it's definitely not a valid Wireguard + // packet (which starts with little-endian uint32 1, 2, 3, 4). + // Use naked returns for all following paths. + isDiscoMsg = true + var sender tailcfg.DiscoKey copy(sender[:], msg[len(disco.Magic):]) @@ -1774,20 +1838,21 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { defer c.mu.Unlock() if c.closed { - return true + return } if debugDisco { c.logf("magicsock: disco: got disco-looking frame from %v", sender.ShortString()) } if c.privateKey.IsZero() { // Ignore disco messages when we're stopped. - return false + // Still return true, to not pass it down to wireguard. + return } if c.discoPrivate.IsZero() { if debugDisco { c.logf("magicsock: disco: ignoring disco-looking frame, no local key") } - return false + return } peerNode, ok := c.nodeOfDisco[sender] @@ -1795,9 +1860,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { if debugDisco { c.logf("magicsock: disco: ignoring disco-looking frame, don't know node for %v", sender.ShortString()) } - // Returning false keeps passing it down, to WireGuard. - // WireGuard will almost surely reject it, but give it a chance. - return false + return } needsRecvActivityCall := false @@ -1810,7 +1873,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { c.logf("magicsock: got disco message from idle peer, starting lazy conf for %v, %v", peerNode.Key.ShortString(), sender.ShortString()) if c.noteRecvActivity == nil { c.logf("magicsock: [unexpected] have node without endpoint, without c.noteRecvActivity hook") - return false + return } needsRecvActivityCall = true } else { @@ -1829,7 +1892,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { // Now, recheck invariants that might've changed while we'd // released the lock, which isn't much: if c.closed || c.privateKey.IsZero() { - return true + return } de, ok = c.endpointOfDisco[sender] if !ok { @@ -1838,7 +1901,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { return false } c.logf("magicsock: [unexpected] lazy endpoint not created for %v, %v", peerNode.Key.ShortString(), sender.ShortString()) - return false + return } if !endpointFound0 { c.logf("magicsock: lazy endpoint created via disco message for %v, %v", peerNode.Key.ShortString(), sender.ShortString()) @@ -1865,7 +1928,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { c.logf("magicsock: disco: failed to open naclbox from %v (wrong rcpt?)", sender) } // TODO(bradfitz): add some counter for this that logs rarely - return false + return } dm, err := disco.Parse(payload) @@ -1879,7 +1942,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { // understand. Not even worth logging about, lest it // be too spammy for old clients. // TODO(bradfitz): add some counter for this that logs rarely - return true + return } switch dm := dm.(type) { @@ -1887,14 +1950,14 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { c.handlePingLocked(dm, de, src, sender, peerNode) case *disco.Pong: if de == nil { - return true + return } de.handlePongConnLocked(dm, src) case *disco.CallMeMaybe: if src.IP != derpMagicIPAddr { // CallMeMaybe messages should only come via DERP. c.logf("[unexpected] CallMeMaybe packets should only come via DERP") - return true + return } if de != nil { c.logf("magicsock: disco: %v<-%v (%v, %v) got call-me-maybe, %d endpoints", @@ -1904,8 +1967,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool { go de.handleCallMeMaybe(dm) } } - - return true + return } func (c *Conn) handlePingLocked(dm *disco.Ping, de *discoEndpoint, src netaddr.IPPort, sender tailcfg.DiscoKey, peerNode *tailcfg.Node) { @@ -2061,7 +2123,9 @@ func (c *Conn) SetNetworkUp(up bool) { c.logf("magicsock: SetNetworkUp(%v)", up) c.networkUp.Set(up) - if !up { + if up { + c.startDerpHomeConnectLocked() + } else { c.closeAllDerpLocked("network-down") } } @@ -2082,6 +2146,7 @@ func (c *Conn) SetPrivateKey(privateKey wgkey.Private) error { return nil } c.privateKey = newKey + c.havePrivateKey.Set(!newKey.IsZero()) if oldKey.IsZero() { c.everHadKey = true @@ -2102,7 +2167,7 @@ func (c *Conn) SetPrivateKey(privateKey wgkey.Private) error { // Key changed. Close existing DERP connections and reconnect to home. if c.myDerp != 0 && !newKey.IsZero() { c.logf("magicsock: private key changed, reconnecting to home derp-%d", c.myDerp) - c.goDerpConnect(c.myDerp) + c.startDerpHomeConnectLocked() } if newKey.IsZero() { @@ -2178,7 +2243,7 @@ func nodesEqual(x, y []*tailcfg.Node) bool { // // It should not use the DERPMap field of NetworkMap; that's // conditionally sent to SetDERPMap instead. -func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) { +func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { c.mu.Lock() defer c.mu.Unlock() @@ -2565,12 +2630,11 @@ func (c *Conn) Rebind() { c.mu.Lock() c.closeAllDerpLocked("rebind") - haveKey := !c.privateKey.IsZero() + if !c.privateKey.IsZero() { + c.startDerpHomeConnectLocked() + } c.mu.Unlock() - if haveKey { - c.goDerpConnect(c.myDerp) - } c.resetEndpointStates() } @@ -2624,11 +2688,11 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err pk := key.Public(pubKey) c.logf("magicsock: CreateEndpoint: key=%s: %s", pk.ShortString(), derpStr(addrs)) - if !strings.HasSuffix(addrs, controlclient.EndpointDiscoSuffix) { + if !strings.HasSuffix(addrs, wgcfg.EndpointDiscoSuffix) { return c.createLegacyEndpointLocked(pk, addrs) } - discoHex := strings.TrimSuffix(addrs, controlclient.EndpointDiscoSuffix) + discoHex := strings.TrimSuffix(addrs, wgcfg.EndpointDiscoSuffix) discoKey, err := key.NewPublicFromHexMem(mem.S(discoHex)) if err != nil { return nil, fmt.Errorf("magicsock: invalid discokey endpoint %q for %v: %w", addrs, pk.ShortString(), err) @@ -2666,6 +2730,8 @@ func (c *RebindingUDPConn) Reset(pconn net.PacketConn) { } } +// ReadFromNetaddr reads a packet from c into b. +// It returns the number of bytes copied and the source address. func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { for { c.mu.Lock() @@ -2686,6 +2752,58 @@ func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { } } +// ReadFromNetaddr reads a packet from c into b. +// It returns the number of bytes copied and the return address. +// It is identical to c.ReadFrom, except that it returns a netaddr.IPPort instead of a net.Addr. +// ReadFromNetaddr is designed to work with specific underlying connection types. +// If c's underlying connection returns a non-*net.UPDAddr return address, ReadFromNetaddr will return an error. +// ReadFromNetaddr exists because it removes an allocation per read, +// when c's underlying connection is a net.UDPConn. +func (c *RebindingUDPConn) ReadFromNetaddr(b []byte) (n int, ipp netaddr.IPPort, err error) { + for { + c.mu.Lock() + pconn := c.pconn + c.mu.Unlock() + + // Optimization: Treat *net.UDPConn specially. + // ReadFromUDP gets partially inlined, avoiding allocating a *net.UDPAddr, + // as long as pAddr itself doesn't escape. + // The non-*net.UDPConn case works, but it allocates. + var pAddr *net.UDPAddr + if udpConn, ok := pconn.(*net.UDPConn); ok { + n, pAddr, err = udpConn.ReadFromUDP(b) + } else { + var addr net.Addr + n, addr, err = pconn.ReadFrom(b) + if addr != nil { + pAddr, ok = addr.(*net.UDPAddr) + if !ok { + return 0, netaddr.IPPort{}, fmt.Errorf("RebindingUDPConn.ReadFromNetaddr: underlying connection returned address of type %T, want *netaddr.UDPAddr", addr) + } + } + } + + if err != nil { + c.mu.Lock() + pconn2 := c.pconn + c.mu.Unlock() + + if pconn != pconn2 { + continue + } + } else { + // Convert pAddr to a netaddr.IPPort. + // This prevents pAddr from escaping. + var ok bool + ipp, ok = netaddr.FromStdAddr(pAddr.IP, pAddr.Port, pAddr.Zone) + if !ok { + return 0, netaddr.IPPort{}, errors.New("netaddr.FromStdAddr failed") + } + } + return n, ipp, err + } +} + func (c *RebindingUDPConn) LocalAddr() *net.UDPAddr { c.mu.Lock() defer c.mu.Unlock() @@ -2760,8 +2878,8 @@ func peerShort(k key.Public) string { return k2.ShortString() } -func sbPrintAddr(sb *strings.Builder, a net.UDPAddr) { - is6 := a.IP.To4() == nil +func sbPrintAddr(sb *strings.Builder, a netaddr.IPPort) { + is6 := a.IP.Is6() if is6 { sb.WriteByte('[') } @@ -2858,8 +2976,8 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { }) } -func udpAddrDebugString(ua net.UDPAddr) string { - if ua.IP.Equal(derpMagicIP) { +func ippDebugString(ua netaddr.IPPort) string { + if ua.IP == derpMagicIPAddr { return fmt.Sprintf("derp-%d", ua.Port) } return ua.String() diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 998b9bb27..26c157d16 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -11,12 +11,14 @@ import ( "crypto/tls" "encoding/binary" "encoding/json" + "errors" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "os" + "runtime" "strconv" "strings" "sync" @@ -30,7 +32,6 @@ import ( "github.com/tailscale/wireguard-go/tun/tuntest" "golang.org/x/crypto/nacl/box" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/derp" "tailscale.com/derp/derphttp" "tailscale.com/derp/derpmap" @@ -41,11 +42,14 @@ import ( "tailscale.com/tstest/natlab" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/types/nettype" "tailscale.com/types/wgkey" + "tailscale.com/util/cibuild" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/tstun" "tailscale.com/wgengine/wgcfg" + "tailscale.com/wgengine/wgcfg/nmcfg" "tailscale.com/wgengine/wglog" ) @@ -251,9 +255,9 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) { eps = make([][]string, len(ms)) ) - buildNetmapLocked := func(myIdx int) *controlclient.NetworkMap { + buildNetmapLocked := func(myIdx int) *netmap.NetworkMap { me := ms[myIdx] - nm := &controlclient.NetworkMap{ + nm := &netmap.NetworkMap{ PrivateKey: me.privateKey, NodeKey: tailcfg.NodeKey(me.privateKey.Public()), Addresses: []netaddr.IPPrefix{{IP: netaddr.IPv4(1, 0, 0, byte(myIdx+1)), Bits: 32}}, @@ -286,14 +290,14 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) { eps[idx] = newEps for i, m := range ms { - netmap := buildNetmapLocked(i) - m.conn.SetNetworkMap(netmap) - peerSet := make(map[key.Public]struct{}, len(netmap.Peers)) - for _, peer := range netmap.Peers { + nm := buildNetmapLocked(i) + m.conn.SetNetworkMap(nm) + peerSet := make(map[key.Public]struct{}, len(nm.Peers)) + for _, peer := range nm.Peers { peerSet[key.Public(peer.Key)] = struct{}{} } m.conn.UpdatePeers(peerSet) - wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts) + wg, err := nmcfg.WGCfg(nm, logf, netmap.AllowSingleHosts) if err != nil { // We're too far from the *testing.T to be graceful, // blow up. Shouldn't happen anyway. @@ -395,18 +399,6 @@ func pickPort(t testing.TB) uint16 { return uint16(conn.LocalAddr().(*net.UDPAddr).Port) } -func TestDerpIPConstant(t *testing.T) { - tstest.PanicOnLog() - tstest.ResourceCheck(t) - - if DerpMagicIP != derpMagicIP.String() { - t.Errorf("str %q != IP %v", DerpMagicIP, derpMagicIP) - } - if len(derpMagicIP) != 4 { - t.Errorf("derpMagicIP is len %d; want 4", len(derpMagicIP)) - } -} - func TestPickDERPFallback(t *testing.T) { tstest.PanicOnLog() tstest.ResourceCheck(t) @@ -449,7 +441,7 @@ func TestPickDERPFallback(t *testing.T) { // But move if peers are elsewhere. const otherNode = 789 c.addrsByKey = map[key.Public]*addrSet{ - key.Public{1}: &addrSet{addrs: []net.UDPAddr{{IP: derpMagicIP, Port: otherNode}}}, + key.Public{1}: &addrSet{ipPorts: []netaddr.IPPort{{IP: derpMagicIPAddr, Port: otherNode}}}, } if got := c.pickDERPFallback(); got != otherNode { t.Errorf("didn't join peers: got %v; want %v", got, someNode) @@ -925,30 +917,56 @@ func testTwoDevicePing(t *testing.T, d *devices) { t.Fatal(err) } + // In the normal case, pings succeed immediately. + // However, in the case of a handshake race, we need to retry. + // With very bad luck, we can need to retry multiple times. + allowedRetries := 3 + if cibuild.On() { + // Allow extra retries on small/flaky/loaded CI machines. + allowedRetries *= 2 + } + // Retries take 5s each. Add 1s for some processing time. + pingTimeout := 5*time.Second*time.Duration(allowedRetries) + time.Second + + // sendWithTimeout sends msg using send, checking that it is received unchanged from in. + // It resends once per second until the send succeeds, or pingTimeout time has elapsed. + sendWithTimeout := func(msg []byte, in chan []byte, send func()) error { + start := time.Now() + for time.Since(start) < pingTimeout { + send() + select { + case recv := <-in: + if !bytes.Equal(msg, recv) { + return errors.New("ping did not transit correctly") + } + return nil + case <-time.After(time.Second): + // try again + } + } + return errors.New("ping timed out") + } + ping1 := func(t *testing.T) { msg2to1 := tuntest.Ping(net.ParseIP("1.0.0.1"), net.ParseIP("1.0.0.2")) - m2.tun.Outbound <- msg2to1 - t.Log("ping1 sent") - select { - case msgRecv := <-m1.tun.Inbound: - if !bytes.Equal(msg2to1, msgRecv) { - t.Error("ping did not transit correctly") - } - case <-time.After(3 * time.Second): - t.Error("ping did not transit") + send := func() { + m2.tun.Outbound <- msg2to1 + t.Log("ping1 sent") + } + in := m1.tun.Inbound + if err := sendWithTimeout(msg2to1, in, send); err != nil { + t.Error(err) } } ping2 := func(t *testing.T) { msg1to2 := tuntest.Ping(net.ParseIP("1.0.0.2"), net.ParseIP("1.0.0.1")) - m1.tun.Outbound <- msg1to2 - t.Log("ping2 sent") - select { - case msgRecv := <-m2.tun.Inbound: - if !bytes.Equal(msg1to2, msgRecv) { - t.Error("return ping did not transit correctly") - } - case <-time.After(3 * time.Second): - t.Error("return ping did not transit") + send := func() { + m1.tun.Outbound <- msg1to2 + t.Log("ping2 sent") + } + in := m2.tun.Inbound + if err := sendWithTimeout(msg1to2, in, send); err != nil { + t.Error(err) } } @@ -969,17 +987,15 @@ func testTwoDevicePing(t *testing.T, d *devices) { setT(t) defer setT(outerT) msg1to2 := tuntest.Ping(net.ParseIP("1.0.0.2"), net.ParseIP("1.0.0.1")) - if err := m1.tsTun.InjectOutbound(msg1to2); err != nil { - t.Fatal(err) - } - t.Log("SendPacket sent") - select { - case msgRecv := <-m2.tun.Inbound: - if !bytes.Equal(msg1to2, msgRecv) { - t.Error("return ping did not transit correctly") + send := func() { + if err := m1.tsTun.InjectOutbound(msg1to2); err != nil { + t.Fatal(err) } - case <-time.After(3 * time.Second): - t.Error("return ping did not transit") + t.Log("SendPacket sent") + } + in := m2.tun.Inbound + if err := sendWithTimeout(msg1to2, in, send); err != nil { + t.Error(err) } }) @@ -1041,7 +1057,7 @@ func testTwoDevicePing(t *testing.T, d *devices) { t.Errorf("return ping %d did not transit correctly: %s", i, cmp.Diff(b, msgRecv)) } } - case <-time.After(3 * time.Second): + case <-time.After(pingTimeout): if strict { t.Errorf("return ping %d did not transit", i) } @@ -1142,20 +1158,13 @@ func TestAddrSet(t *testing.T) { tstest.ResourceCheck(t) mustIPPortPtr := func(s string) *netaddr.IPPort { - t.Helper() - ipp, err := netaddr.ParseIPPort(s) - if err != nil { - t.Fatal(err) - } + ipp := netaddr.MustParseIPPort(s) return &ipp } - mustUDPAddr := func(s string) *net.UDPAddr { - return mustIPPortPtr(s).UDPAddr() - } - udpAddrs := func(ss ...string) (ret []net.UDPAddr) { + ipps := func(ss ...string) (ret []netaddr.IPPort) { t.Helper() for _, s := range ss { - ret = append(ret, *mustUDPAddr(s)) + ret = append(ret, netaddr.MustParseIPPort(s)) } return ret } @@ -1187,7 +1196,7 @@ func TestAddrSet(t *testing.T) { // updateDst, if set, does an UpdateDst call and // b+want are ignored. - updateDst *net.UDPAddr + updateDst *netaddr.IPPort b []byte want string // comma-separated @@ -1201,7 +1210,7 @@ func TestAddrSet(t *testing.T) { { name: "reg_packet_no_curaddr", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: -1, // unknown roamAddr: nil, }, @@ -1212,7 +1221,7 @@ func TestAddrSet(t *testing.T) { { name: "reg_packet_have_curaddr", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: 1, // global IP roamAddr: nil, }, @@ -1223,36 +1232,36 @@ func TestAddrSet(t *testing.T) { { name: "reg_packet_have_roamaddr", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: 2, // should be ignored roamAddr: mustIPPortPtr("5.6.7.8:123"), }, steps: []step{ {b: regPacket, want: "5.6.7.8:123"}, - {updateDst: mustUDPAddr("10.0.0.1:123")}, // no more roaming + {updateDst: mustIPPortPtr("10.0.0.1:123")}, // no more roaming {b: regPacket, want: "10.0.0.1:123"}, }, }, { name: "start_roaming", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: 2, }, steps: []step{ {b: regPacket, want: "10.0.0.1:123"}, - {updateDst: mustUDPAddr("4.5.6.7:123")}, + {updateDst: mustIPPortPtr("4.5.6.7:123")}, {b: regPacket, want: "4.5.6.7:123"}, - {updateDst: mustUDPAddr("5.6.7.8:123")}, + {updateDst: mustIPPortPtr("5.6.7.8:123")}, {b: regPacket, want: "5.6.7.8:123"}, - {updateDst: mustUDPAddr("123.45.67.89:123")}, // end roaming + {updateDst: mustIPPortPtr("123.45.67.89:123")}, // end roaming {b: regPacket, want: "123.45.67.89:123"}, }, }, { name: "spray_packet", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: 2, // should be ignored roamAddr: mustIPPortPtr("5.6.7.8:123"), }, @@ -1261,19 +1270,19 @@ func TestAddrSet(t *testing.T) { {advance: 300 * time.Millisecond, b: regPacket, want: "127.3.3.40:1,123.45.67.89:123,10.0.0.1:123,5.6.7.8:123"}, {advance: 300 * time.Millisecond, b: regPacket, want: "127.3.3.40:1,123.45.67.89:123,10.0.0.1:123,5.6.7.8:123"}, {advance: 3, b: regPacket, want: "5.6.7.8:123"}, - {advance: 2 * time.Millisecond, updateDst: mustUDPAddr("10.0.0.1:123")}, + {advance: 2 * time.Millisecond, updateDst: mustIPPortPtr("10.0.0.1:123")}, {advance: 3, b: regPacket, want: "10.0.0.1:123"}, }, }, { name: "low_pri", as: &addrSet{ - addrs: udpAddrs("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), + ipPorts: ipps("127.3.3.40:1", "123.45.67.89:123", "10.0.0.1:123"), curAddr: 2, }, steps: []step{ - {updateDst: mustUDPAddr("123.45.67.89:123")}, - {updateDst: mustUDPAddr("123.45.67.89:123")}, + {updateDst: mustIPPortPtr("123.45.67.89:123")}, + {updateDst: mustIPPortPtr("123.45.67.89:123")}, }, logCheck: func(t *testing.T, logged []byte) { if n := bytes.Count(logged, []byte(", keeping current ")); n != 1 { @@ -1292,12 +1301,11 @@ func TestAddrSet(t *testing.T) { t.Logf(format, args...) } tt.as.clock = func() time.Time { return faket } - initAddrSet(tt.as) for i, st := range tt.steps { faket = faket.Add(st.advance) if st.updateDst != nil { - if err := tt.as.updateDst(st.updateDst); err != nil { + if err := tt.as.updateDst(*st.updateDst); err != nil { t.Fatal(err) } continue @@ -1314,23 +1322,6 @@ func TestAddrSet(t *testing.T) { } } -// initAddrSet initializes fields in the provided incomplete addrSet -// to satisfying invariants within magicsock. -func initAddrSet(as *addrSet) { - if as.roamAddr != nil && as.roamAddrStd == nil { - as.roamAddrStd = as.roamAddr.UDPAddr() - } - if len(as.ipPorts) == 0 { - for _, ua := range as.addrs { - ipp, ok := netaddr.FromStdAddr(ua.IP, ua.Port, ua.Zone) - if !ok { - panic(fmt.Sprintf("bogus UDPAddr %+v", ua)) - } - as.ipPorts = append(as.ipPorts, ipp) - } - } -} - func TestDiscoMessage(t *testing.T) { c := newConn() c.logf = t.Logf @@ -1407,62 +1398,235 @@ func Test32bitAlignment(t *testing.T) { atomic.AddInt64(&c.derpRecvCountAtomic, 1) } -func BenchmarkReceiveFrom(b *testing.B) { - port := pickPort(b) +// newNonLegacyTestConn returns a new Conn with DisableLegacyNetworking set true. +func newNonLegacyTestConn(t testing.TB) *Conn { + t.Helper() + port := pickPort(t) conn, err := NewConn(Options{ - Logf: b.Logf, + Logf: t.Logf, Port: port, EndpointsFunc: func(eps []string) { - b.Logf("endpoints: %q", eps) + t.Logf("endpoints: %q", eps) }, DisableLegacyNetworking: true, }) if err != nil { - b.Fatal(err) + t.Fatal(err) } + return conn +} + +// Tests concurrent DERP readers pushing DERP data into ReceiveIPv4 +// (which should blend all DERP reads into UDP reads). +func TestDerpReceiveFromIPv4(t *testing.T) { + conn := newNonLegacyTestConn(t) defer conn.Close() sendConn, err := net.ListenPacket("udp4", "127.0.0.1:0") if err != nil { - b.Fatal(err) + t.Fatal(err) } defer sendConn.Close() + nodeKey, _ := addTestEndpoint(conn, sendConn) + var sends int = 250e3 // takes about a second + if testing.Short() { + sends /= 10 + } + senders := runtime.NumCPU() + sends -= (sends % senders) + var wg sync.WaitGroup + defer wg.Wait() + t.Logf("doing %v sends over %d senders", sends, senders) + + ctx, cancel := context.WithCancel(context.Background()) + defer conn.Close() + defer cancel() + + doneCtx, cancelDoneCtx := context.WithCancel(context.Background()) + cancelDoneCtx() + + for i := 0; i < senders; i++ { + wg.Add(1) + regionID := i + 1 + go func() { + defer wg.Done() + for i := 0; i < sends/senders; i++ { + res := derpReadResult{ + regionID: regionID, + n: 123, + src: key.Public(nodeKey), + copyBuf: func(dst []byte) int { return 123 }, + } + // First send with the closed context. ~50% of + // these should end up going through the + // send-a-zero-derpReadResult path, returning + // true, in which case we don't want to send again. + // We test later that we hit the other path. + if conn.sendDerpReadResult(doneCtx, res) { + continue + } + + if !conn.sendDerpReadResult(ctx, res) { + t.Error("unexpected false") + return + } + } + }() + } + + zeroSendsStart := testCounterZeroDerpReadResultSend.Value() + + buf := make([]byte, 1500) + for i := 0; i < sends; i++ { + n, ep, err := conn.ReceiveIPv4(buf) + if err != nil { + t.Fatal(err) + } + _ = n + _ = ep + } + + t.Logf("did %d ReceiveIPv4 calls", sends) + + zeroSends, zeroRecv := testCounterZeroDerpReadResultSend.Value(), testCounterZeroDerpReadResultRecv.Value() + if zeroSends != zeroRecv { + t.Errorf("did %d zero sends != %d corresponding receives", zeroSends, zeroRecv) + } + zeroSendDelta := zeroSends - zeroSendsStart + if zeroSendDelta == 0 { + t.Errorf("didn't see any sends of derpReadResult zero value") + } + if zeroSendDelta == int64(sends) { + t.Errorf("saw %v sends of the derpReadResult zero value which was unexpectedly high (100%% of our %v sends)", zeroSendDelta, sends) + } +} + +// addTestEndpoint sets conn's network map to a single peer expected +// to receive packets from sendConn (or DERP), and returns that peer's +// nodekey and discokey. +func addTestEndpoint(conn *Conn, sendConn net.PacketConn) (tailcfg.NodeKey, tailcfg.DiscoKey) { // Give conn just enough state that it'll recognize sendConn as a // valid peer and not fall through to the legacy magicsock // codepath. discoKey := tailcfg.DiscoKey{31: 1} - conn.SetNetworkMap(&controlclient.NetworkMap{ + nodeKey := tailcfg.NodeKey{0: 'N', 1: 'K'} + conn.SetNetworkMap(&netmap.NetworkMap{ Peers: []*tailcfg.Node{ { + Key: nodeKey, DiscoKey: discoKey, Endpoints: []string{sendConn.LocalAddr().String()}, }, }, }) - conn.CreateEndpoint([32]byte{1: 1}, "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345") + conn.SetPrivateKey(wgkey.Private{0: 1}) + conn.CreateEndpoint([32]byte(nodeKey), "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345") conn.addValidDiscoPathForTest(discoKey, netaddr.MustParseIPPort(sendConn.LocalAddr().String())) + return nodeKey, discoKey +} + +func setUpReceiveFrom(tb testing.TB) (roundTrip func()) { + conn := newNonLegacyTestConn(tb) + tb.Cleanup(func() { conn.Close() }) + conn.logf = logger.Discard + + sendConn, err := net.ListenPacket("udp4", "127.0.0.1:0") + if err != nil { + tb.Fatal(err) + } + tb.Cleanup(func() { sendConn.Close() }) + + addTestEndpoint(conn, sendConn) var dstAddr net.Addr = conn.pconn4.LocalAddr() sendBuf := make([]byte, 1<<10) for i := range sendBuf { sendBuf[i] = 'x' } - buf := make([]byte, 2<<10) - for i := 0; i < b.N; i++ { + return func() { if _, err := sendConn.WriteTo(sendBuf, dstAddr); err != nil { - b.Fatalf("WriteTo: %v", err) + tb.Fatalf("WriteTo: %v", err) } n, ep, err := conn.ReceiveIPv4(buf) if err != nil { - b.Fatal(err) + tb.Fatal(err) } _ = n _ = ep } } +// goMajorVersion reports the major Go version and whether it is a Tailscale fork. +// If parsing fails, goMajorVersion returns 0, false. +func goMajorVersion(s string) (version int, isTS bool) { + if !strings.HasPrefix(s, "go1.") { + return 0, false + } + mm := s[len("go1."):] + var major, rest string + for _, sep := range []string{".", "rc", "beta"} { + i := strings.Index(mm, sep) + if i > 0 { + major, rest = mm[:i], mm[i:] + break + } + } + if major == "" { + major = mm + } + n, err := strconv.Atoi(major) + if err != nil { + return 0, false + } + return n, strings.Contains(rest, "ts") +} + +func TestGoMajorVersion(t *testing.T) { + tests := []struct { + version string + wantN int + wantTS bool + }{ + {"go1.15.8", 15, false}, + {"go1.16rc1", 16, false}, + {"go1.16rc1", 16, false}, + {"go1.15.5-ts3bd89195a3", 15, true}, + {"go1.15", 15, false}, + } + + for _, tt := range tests { + n, ts := goMajorVersion(tt.version) + if tt.wantN != n || tt.wantTS != ts { + t.Errorf("goMajorVersion(%s) = %v, %v, want %v, %v", tt.version, n, ts, tt.wantN, tt.wantTS) + } + } +} + +func TestReceiveFromAllocs(t *testing.T) { + // Go 1.16 and before: allow 3 allocs. + // Go Tailscale fork, Go 1.17+: only allow 2 allocs. + major, ts := goMajorVersion(runtime.Version()) + maxAllocs := 3 + if major >= 17 || ts { + maxAllocs = 2 + } + t.Logf("allowing %d allocs for Go version %q", maxAllocs, runtime.Version()) + roundTrip := setUpReceiveFrom(t) + avg := int(testing.AllocsPerRun(100, roundTrip)) + if avg > maxAllocs { + t.Fatalf("expected %d allocs in ReceiveFrom, got %v", maxAllocs, avg) + } +} + +func BenchmarkReceiveFrom(b *testing.B) { + roundTrip := setUpReceiveFrom(b) + for i := 0; i < b.N; i++ { + roundTrip() + } +} + func BenchmarkReceiveFrom_Native(b *testing.B) { recvConn, err := net.ListenPacket("udp4", "127.0.0.1:0") if err != nil { diff --git a/wgengine/monitor/monitor_darwin_tailscaled.go b/wgengine/monitor/monitor_darwin_tailscaled.go new file mode 100644 index 000000000..f7123cf65 --- /dev/null +++ b/wgengine/monitor/monitor_darwin_tailscaled.go @@ -0,0 +1,72 @@ +// 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. + +// +build darwin,!redo + +package monitor + +import ( + "bufio" + "errors" + "os/exec" + + "tailscale.com/syncs" + "tailscale.com/types/logger" +) + +// unspecifiedMessage is a minimal message implementation that should not +// be ignored. In general, OS-specific implementations should use better +// types and avoid this if they can. +type unspecifiedMessage struct{} + +func (unspecifiedMessage) ignore() bool { return false } + +func newOSMon(logf logger.Logf) (osMon, error) { + return new(routeMonitorSubProcMon), nil +} + +// routeMonitorSubProcMon is a very simple (temporary? but I know +// better) monitor implementation for darwin in tailscaled-mode where +// we can just shell out to "route -n monitor". It waits for any input +// but doesn't parse it. Then we poll to see if something is different. +type routeMonitorSubProcMon struct { + closed syncs.AtomicBool + cmd *exec.Cmd // of "/sbin/route -n monitor" + br *bufio.Reader + buf []byte +} + +func (m *routeMonitorSubProcMon) Close() error { + m.closed.Set(true) + if m.cmd != nil { + m.cmd.Process.Kill() + m.cmd = nil + } + return nil +} + +func (m *routeMonitorSubProcMon) Receive() (message, error) { + if m.closed.Get() { + return nil, errors.New("monitor closed") + } + if m.cmd == nil { + cmd := exec.Command("/sbin/route", "-n", "monitor") + outPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + m.br = bufio.NewReader(outPipe) + m.cmd = cmd + m.buf = make([]byte, 16<<10) + } + _, err := m.br.Read(m.buf) + if err != nil { + m.Close() + return nil, err + } + return unspecifiedMessage{}, nil +} diff --git a/wgengine/monitor/monitor_unsupported.go b/wgengine/monitor/monitor_unsupported.go index a54990c02..a779536e6 100644 --- a/wgengine/monitor/monitor_unsupported.go +++ b/wgengine/monitor/monitor_unsupported.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !linux,!freebsd,!windows android +// +build !linux,!freebsd,!windows,!darwin android darwin,redo package monitor diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 72651bd41..b2b21fcba 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -28,9 +28,9 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/waiter" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/net/packet" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" @@ -63,7 +63,7 @@ func Impl(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock. log.Fatal(err) } - e.AddNetworkMapCallback(func(nm *controlclient.NetworkMap) { + e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) { oldIPs := make(map[tcpip.Address]bool) for _, ip := range ipstack.AllAddresses()[nicID] { oldIPs[ip.AddressWithPrefix.Address] = true diff --git a/wgengine/pendopen.go b/wgengine/pendopen.go index a4d0a6f8b..2ede0429a 100644 --- a/wgengine/pendopen.go +++ b/wgengine/pendopen.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "tailscale.com/ipn/ipnstate" "tailscale.com/net/flowtrack" "tailscale.com/net/packet" "tailscale.com/wgengine/filter" @@ -30,6 +31,12 @@ func debugConnectFailures() bool { type pendingOpenFlow struct { timer *time.Timer // until giving up on the flow + + // guarded by userspaceEngine.mu: + + // problem is non-zero if we got a MaybeBroken (non-terminal) + // TSMP "reject" header. + problem packet.TailscaleRejectReason } func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) { @@ -45,6 +52,17 @@ func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) { return true } +func (e *userspaceEngine) noteFlowProblemFromPeer(f flowtrack.Tuple, problem packet.TailscaleRejectReason) { + e.mu.Lock() + defer e.mu.Unlock() + of, ok := e.pendOpen[f] + if !ok { + // Not a tracked flow (likely already removed) + return + } + of.problem = problem +} + func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) { res = filter.Accept // always @@ -54,7 +72,9 @@ func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) if !ok { return } - if f := rh.Flow(); e.removeFlow(f) { + if rh.MaybeBroken { + e.noteFlowProblemFromPeer(rh.Flow(), rh.Reason) + } else if f := rh.Flow(); e.removeFlow(f) { e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason) } return @@ -106,14 +126,20 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.TUN func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { e.mu.Lock() - if _, ok := e.pendOpen[flow]; !ok { + of, ok := e.pendOpen[flow] + if !ok { // Not a tracked flow, or already handled & deleted. e.mu.Unlock() return } delete(e.pendOpen, flow) + problem := of.problem e.mu.Unlock() + if !problem.IsZero() { + e.logf("open-conn-track: timeout opening %v; peer reported problem: %v", flow, problem) + } + // Diagnose why it might've timed out. n, ok := e.magicConn.PeerForIP(flow.Dst.IP) if !ok { @@ -133,7 +159,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { lastSeen = *n.LastSeen } - var ps *PeerStatus + var ps *ipnstate.PeerStatusLite if st, err := e.getStatus(); err == nil { for _, v := range st.Peers { if v.NodeKey == n.Key { diff --git a/wgengine/router/router.go b/wgengine/router/router.go index c65a0b806..9c3f1003f 100644 --- a/wgengine/router/router.go +++ b/wgengine/router/router.go @@ -11,6 +11,7 @@ import ( "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" "tailscale.com/types/logger" + "tailscale.com/types/preftype" "tailscale.com/wgengine/router/dns" ) @@ -53,29 +54,6 @@ func Cleanup(logf logger.Logf, interfaceName string) { cleanup(logf, interfaceName) } -// NetfilterMode is the firewall management mode to use when -// programming the Linux network stack. -type NetfilterMode int - -const ( - NetfilterOff NetfilterMode = iota // remove all tailscale netfilter state - NetfilterNoDivert // manage tailscale chains, but don't call them - NetfilterOn // manage tailscale chains and call them from main chains -) - -func (m NetfilterMode) String() string { - switch m { - case NetfilterOff: - return "off" - case NetfilterNoDivert: - return "nodivert" - case NetfilterOn: - return "on" - default: - return "???" - } -} - // Config is the subset of Tailscale configuration that is relevant to // the OS's network stack. type Config struct { @@ -86,9 +64,9 @@ type Config struct { // Linux-only things below, ignored on other platforms. - SubnetRoutes []netaddr.IPPrefix // subnets being advertised to other Tailscale nodes - SNATSubnetRoutes bool // SNAT traffic to local subnets - NetfilterMode NetfilterMode // how much to manage netfilter rules + SubnetRoutes []netaddr.IPPrefix // subnets being advertised to other Tailscale nodes + SNATSubnetRoutes bool // SNAT traffic to local subnets + NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules } // shutdownConfig is a routing configuration that removes all router diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index d6e10dac1..c3724d77f 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -21,10 +21,17 @@ import ( "inet.af/netaddr" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" + "tailscale.com/types/preftype" "tailscale.com/version/distro" "tailscale.com/wgengine/router/dns" ) +const ( + netfilterOff = preftype.NetfilterOff + netfilterNoDivert = preftype.NetfilterNoDivert + netfilterOn = preftype.NetfilterOn +) + // The following bits are added to packet marks for Tailscale use. // // We tried to pick bits sufficiently out of the way that it's @@ -89,7 +96,7 @@ type linuxRouter struct { addrs map[netaddr.IPPrefix]bool routes map[netaddr.IPPrefix]bool snatSubnetRoutes bool - netfilterMode NetfilterMode + netfilterMode preftype.NetfilterMode // Various feature checks for the network stack. ipRuleAvailable bool @@ -148,7 +155,7 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, ne return &linuxRouter{ logf: logf, tunname: tunname, - netfilterMode: NetfilterOff, + netfilterMode: netfilterOff, ipRuleAvailable: ipRuleAvailable, v6Available: supportsV6, @@ -168,7 +175,7 @@ func (r *linuxRouter) Up() error { if err := r.addIPRules(); err != nil { return err } - if err := r.setNetfilterMode(NetfilterOff); err != nil { + if err := r.setNetfilterMode(netfilterOff); err != nil { return err } if err := r.upInterface(); err != nil { @@ -188,7 +195,7 @@ func (r *linuxRouter) Close() error { if err := r.delIPRules(); err != nil { return err } - if err := r.setNetfilterMode(NetfilterOff); err != nil { + if err := r.setNetfilterMode(netfilterOff); err != nil { return err } @@ -246,9 +253,9 @@ func (r *linuxRouter) Set(cfg *Config) error { // mode. Netfilter state is created or deleted appropriately to // reflect the new mode, and r.snatSubnetRoutes is updated to reflect // the current state of subnet SNATing. -func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { +func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error { if distro.Get() == distro.Synology { - mode = NetfilterOff + mode = netfilterOff } if r.netfilterMode == mode { return nil @@ -264,9 +271,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { reprocess := false switch mode { - case NetfilterOff: + case netfilterOff: switch r.netfilterMode { - case NetfilterNoDivert: + case netfilterNoDivert: if err := r.delNetfilterBase(); err != nil { return err } @@ -276,7 +283,7 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { // This can happen if someone left a ref to // this table somewhere else. } - case NetfilterOn: + case netfilterOn: if err := r.delNetfilterHooks(); err != nil { return err } @@ -291,9 +298,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { } } r.snatSubnetRoutes = false - case NetfilterNoDivert: + case netfilterNoDivert: switch r.netfilterMode { - case NetfilterOff: + case netfilterOff: reprocess = true if err := r.addNetfilterChains(); err != nil { return err @@ -302,12 +309,12 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { return err } r.snatSubnetRoutes = false - case NetfilterOn: + case netfilterOn: if err := r.delNetfilterHooks(); err != nil { return err } } - case NetfilterOn: + case netfilterOn: // Because of bugs in old version of iptables-compat, // we can't add a "-j ts-forward" rule to FORWARD // while ts-forward contains an "-m mark" rule. But @@ -315,7 +322,7 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { // So we have to delNetFilterBase, then add the hooks, // then re-addNetFilterBase, just in case. switch r.netfilterMode { - case NetfilterOff: + case netfilterOff: reprocess = true if err := r.addNetfilterChains(); err != nil { return err @@ -330,7 +337,7 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error { return err } r.snatSubnetRoutes = false - case NetfilterNoDivert: + case netfilterNoDivert: reprocess = true if err := r.delNetfilterBase(); err != nil { return err @@ -397,7 +404,7 @@ func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error { // addLoopbackRule adds a firewall rule to permit loopback traffic to // a local Tailscale IP. func (r *linuxRouter) addLoopbackRule(addr netaddr.IP) error { - if r.netfilterMode == NetfilterOff { + if r.netfilterMode == netfilterOff { return nil } @@ -419,7 +426,7 @@ func (r *linuxRouter) addLoopbackRule(addr netaddr.IP) error { // delLoopbackRule removes the firewall rule permitting loopback // traffic to a Tailscale IP. func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error { - if r.netfilterMode == NetfilterOff { + if r.netfilterMode == netfilterOff { return nil } @@ -903,7 +910,7 @@ func (r *linuxRouter) delNetfilterHooks() error { // addSNATRule adds a netfilter rule to SNAT traffic destined for // local subnets. func (r *linuxRouter) addSNATRule() error { - if r.netfilterMode == NetfilterOff { + if r.netfilterMode == netfilterOff { return nil } @@ -922,7 +929,7 @@ func (r *linuxRouter) addSNATRule() error { // delSNATRule removes the netfilter rule to SNAT traffic destined for // local subnets. Fails if the rule does not exist. func (r *linuxRouter) delSNATRule() error { - if r.netfilterMode == NetfilterOff { + if r.netfilterMode == netfilterOff { return nil } diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index bcc93af8f..8298c6d07 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -58,7 +58,7 @@ up` + basic, name: "local addr only", in: &Config{ LocalAddrs: mustCIDRs("100.101.102.103/10"), - NetfilterMode: NetfilterOff, + NetfilterMode: netfilterOff, }, want: ` up @@ -70,7 +70,7 @@ ip addr add 100.101.102.103/10 dev tailscale0` + basic, in: &Config{ LocalAddrs: mustCIDRs("100.101.102.103/10"), Routes: mustCIDRs("100.100.100.100/32", "192.168.16.0/24"), - NetfilterMode: NetfilterOff, + NetfilterMode: netfilterOff, }, want: ` up @@ -85,7 +85,7 @@ ip route add 192.168.16.0/24 dev tailscale0 table 52` + basic, LocalAddrs: mustCIDRs("100.101.102.103/10"), Routes: mustCIDRs("100.100.100.100/32", "192.168.16.0/24"), SubnetRoutes: mustCIDRs("200.0.0.0/8"), - NetfilterMode: NetfilterOff, + NetfilterMode: netfilterOff, }, want: ` up @@ -101,7 +101,7 @@ ip route add 192.168.16.0/24 dev tailscale0 table 52` + basic, Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), SubnetRoutes: mustCIDRs("200.0.0.0/8"), SNATSubnetRoutes: true, - NetfilterMode: NetfilterOn, + NetfilterMode: netfilterOn, }, want: ` up @@ -133,7 +133,7 @@ v6/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE in: &Config{ LocalAddrs: mustCIDRs("100.101.102.104/10"), Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), - NetfilterMode: NetfilterOn, + NetfilterMode: netfilterOn, }, want: ` up @@ -166,7 +166,7 @@ v6/nat/POSTROUTING -j ts-postrouting Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), SubnetRoutes: mustCIDRs("200.0.0.0/8"), SNATSubnetRoutes: false, - NetfilterMode: NetfilterOn, + NetfilterMode: netfilterOn, }, want: ` up @@ -196,7 +196,7 @@ v6/nat/POSTROUTING -j ts-postrouting in: &Config{ LocalAddrs: mustCIDRs("100.101.102.104/10"), Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), - NetfilterMode: NetfilterOn, + NetfilterMode: netfilterOn, }, want: ` up @@ -227,7 +227,7 @@ v6/nat/POSTROUTING -j ts-postrouting in: &Config{ LocalAddrs: mustCIDRs("100.101.102.104/10"), Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), - NetfilterMode: NetfilterNoDivert, + NetfilterMode: netfilterNoDivert, }, want: ` up @@ -251,7 +251,7 @@ v6/filter/ts-forward -o tailscale0 -j ACCEPT in: &Config{ LocalAddrs: mustCIDRs("100.101.102.104/10"), Routes: mustCIDRs("100.100.100.100/32", "10.0.0.0/8"), - NetfilterMode: NetfilterOn, + NetfilterMode: netfilterOn, }, want: ` up diff --git a/wgengine/router/router_userspace_bsd.go b/wgengine/router/router_userspace_bsd.go index 33848c8bb..fb81d62fb 100644 --- a/wgengine/router/router_userspace_bsd.go +++ b/wgengine/router/router_userspace_bsd.go @@ -7,7 +7,6 @@ package router import ( - "errors" "fmt" "log" "os/exec" @@ -23,7 +22,7 @@ import ( type userspaceBSDRouter struct { logf logger.Logf tunname string - local netaddr.IPPrefix + local []netaddr.IPPrefix routes map[netaddr.IPPrefix]struct{} dns *dns.Manager @@ -47,6 +46,38 @@ func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device }, nil } +func (r *userspaceBSDRouter) addrsToRemove(newLocalAddrs []netaddr.IPPrefix) (remove []netaddr.IPPrefix) { + for _, cur := range r.local { + found := false + for _, v := range newLocalAddrs { + found = (v == cur) + if found { + break + } + } + if !found { + remove = append(remove, cur) + } + } + return +} + +func (r *userspaceBSDRouter) addrsToAdd(newLocalAddrs []netaddr.IPPrefix) (add []netaddr.IPPrefix) { + for _, cur := range newLocalAddrs { + found := false + for _, v := range r.local { + found = (v == cur) + if found { + break + } + } + if !found { + add = append(add, cur) + } + } + return +} + func cmd(args ...string) *exec.Cmd { if len(args) == 0 { log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args) @@ -63,45 +94,40 @@ func (r *userspaceBSDRouter) Up() error { return nil } -func (r *userspaceBSDRouter) Set(cfg *Config) error { +func inet(p netaddr.IPPrefix) string { + if p.IP.Is6() { + return "inet6" + } + return "inet" +} + +func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) { if cfg == nil { cfg = &shutdownConfig } - if len(cfg.LocalAddrs) == 0 { - return nil - } - // TODO: support configuring multiple local addrs on interface. - if len(cfg.LocalAddrs) != 1 { - return errors.New("freebsd doesn't support setting multiple local addrs yet") - } - localAddr := cfg.LocalAddrs[0] var errq error - - // Update the address. - if localAddr != r.local { - // If the interface is already set, remove it. - if !r.local.IsZero() { - addrdel := []string{"ifconfig", r.tunname, - "inet", r.local.String(), "-alias"} - out, err := cmd(addrdel...).CombinedOutput() - if err != nil { - r.logf("addr del failed: %v: %v\n%s", addrdel, err, out) - if errq == nil { - errq = err - } - } + setErr := func(err error) { + if errq == nil { + errq = err } + } - // Add the interface. - addradd := []string{"ifconfig", r.tunname, - "inet", localAddr.String(), localAddr.IP.String()} - out, err := cmd(addradd...).CombinedOutput() + // Update the addresses. + for _, addr := range r.addrsToRemove(cfg.LocalAddrs) { + arg := []string{"ifconfig", r.tunname, inet(addr), addr.String(), "-alias"} + out, err := cmd(arg...).CombinedOutput() if err != nil { - r.logf("addr add failed: %v: %v\n%s", addradd, err, out) - if errq == nil { - errq = err - } + r.logf("addr del failed: %v => %v\n%s", arg, err, out) + setErr(err) + } + } + for _, addr := range r.addrsToAdd(cfg.LocalAddrs) { + arg := []string{"ifconfig", r.tunname, inet(addr), addr.String(), addr.IP.String()} + out, err := cmd(arg...).CombinedOutput() + if err != nil { + r.logf("addr add failed: %v => %v\n%s", arg, err, out) + setErr(err) } } @@ -120,14 +146,12 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error { del = "delete" } routedel := []string{"route", "-q", "-n", - del, "-inet", nstr, + del, "-" + inet(route), nstr, "-iface", r.tunname} out, err := cmd(routedel...).CombinedOutput() if err != nil { r.logf("route del failed: %v: %v\n%s", routedel, err, out) - if errq == nil { - errq = err - } + setErr(err) } } } @@ -138,24 +162,25 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error { nip := net.IP.Mask(net.Mask) nstr := fmt.Sprintf("%v/%d", nip, route.Bits) routeadd := []string{"route", "-q", "-n", - "add", "-inet", nstr, + "add", "-" + inet(route), nstr, "-iface", r.tunname} out, err := cmd(routeadd...).CombinedOutput() if err != nil { r.logf("addr add failed: %v: %v\n%s", routeadd, err, out) - if errq == nil { - errq = err - } + setErr(err) } } } // Store the interface and routes so we know what to change on an update. - r.local = localAddr + if errq == nil { + r.local = append([]netaddr.IPPrefix{}, cfg.LocalAddrs...) + } r.routes = newRoutes if err := r.dns.Set(cfg.DNS); err != nil { - errq = fmt.Errorf("dns set: %v", err) + r.logf("DNS set: %v", err) + setErr(err) } return errq diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go index 0194ef0a1..b600709d3 100644 --- a/wgengine/router/router_windows.go +++ b/wgengine/router/router_windows.go @@ -7,6 +7,7 @@ package router import ( "context" "fmt" + "os" "os/exec" "sync" "syscall" @@ -121,11 +122,12 @@ func cleanup(logf logger.Logf, interfaceName string) { type firewallTweaker struct { logf logger.Logf - mu sync.Mutex - running bool // doAsyncSet goroutine is running - known bool // firewall is in known state (in lastVal) - want []string // next value we want, or "" to delete the firewall rule - lastVal []string // last set value, if known + mu sync.Mutex + didProcRule bool + running bool // doAsyncSet goroutine is running + known bool // firewall is in known state (in lastVal) + want []string // next value we want, or "" to delete the firewall rule + lastVal []string // last set value, if known } func (ft *firewallTweaker) clear() { ft.set(nil) } @@ -177,6 +179,7 @@ func (ft *firewallTweaker) doAsyncSet() { return } needClear := !ft.known || len(ft.lastVal) > 0 || len(val) == 0 + needProcRule := !ft.didProcRule ft.mu.Unlock() if needClear { @@ -189,6 +192,37 @@ func (ft *firewallTweaker) doAsyncSet() { d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in") ft.logf("cleared Tailscale-In firewall rules in %v", d) } + if needProcRule { + ft.logf("deleting any prior Tailscale-Process rule...") + d, err := ft.runFirewall("delete", "rule", "name=Tailscale-Process", "dir=in") // best effort + if err == nil { + ft.logf("removed old Tailscale-Process rule in %v", d) + } + var exe string + exe, err = os.Executable() + if err != nil { + ft.logf("failed to find Executable for Tailscale-Process rule: %v", err) + } else { + ft.logf("adding Tailscale-Process rule to allow UDP for %q ...", exe) + d, err = ft.runFirewall("add", "rule", "name=Tailscale-Process", + "dir=in", + "action=allow", + "edge=yes", + "program="+exe, + "protocol=udp", + "profile=any", + "enable=yes", + ) + if err != nil { + ft.logf("error adding Tailscale-Process rule: %v", err) + } else { + ft.mu.Lock() + ft.didProcRule = true + ft.mu.Unlock() + ft.logf("added Tailscale-Process rule in %v", d) + } + } + } var err error for _, cidr := range val { ft.logf("adding Tailscale-In rule to allow %v ...", cidr) diff --git a/wgengine/tsdns/tsdns_server_test.go b/wgengine/tsdns/tsdns_server_test.go index bffb8b869..df9047fc6 100644 --- a/wgengine/tsdns/tsdns_server_test.go +++ b/wgengine/tsdns/tsdns_server_test.go @@ -5,6 +5,9 @@ package tsdns import ( + "log" + "testing" + "github.com/miekg/dns" "inet.af/netaddr" ) @@ -71,7 +74,7 @@ func resolveToNXDOMAIN(w dns.ResponseWriter, req *dns.Msg) { w.WriteMsg(m) } -func serveDNS(addr string) (*dns.Server, chan error) { +func serveDNS(tb testing.TB, addr string) (*dns.Server, chan error) { server := &dns.Server{Addr: addr, Net: "udp"} waitch := make(chan struct{}) @@ -79,7 +82,11 @@ func serveDNS(addr string) (*dns.Server, chan error) { errch := make(chan error, 1) go func() { - errch <- server.ListenAndServe() + err := server.ListenAndServe() + if err != nil { + log.Printf("ListenAndServe(%q): %v", addr, err) + } + errch <- err close(errch) }() diff --git a/wgengine/tsdns/tsdns_test.go b/wgengine/tsdns/tsdns_test.go index 95d32dfbb..a2f56a168 100644 --- a/wgengine/tsdns/tsdns_test.go +++ b/wgengine/tsdns/tsdns_test.go @@ -274,14 +274,27 @@ func TestResolveReverse(t *testing.T) { } } +func ipv6Works() bool { + c, err := net.Listen("tcp", "[::1]:0") + if err != nil { + return false + } + c.Close() + return true +} + func TestDelegate(t *testing.T) { tstest.ResourceCheck(t) + if !ipv6Works() { + t.Skip("skipping test that requires localhost IPv6") + } + dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site.")) dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN) - v4server, v4errch := serveDNS("127.0.0.1:0") - v6server, v6errch := serveDNS("[::1]:0") + v4server, v4errch := serveDNS(t, "127.0.0.1:0") + v6server, v6errch := serveDNS(t, "[::1]:0") defer func() { if err := <-v4errch; err != nil { @@ -371,7 +384,7 @@ func TestDelegate(t *testing.T) { func TestDelegateCollision(t *testing.T) { dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site.")) - server, errch := serveDNS("127.0.0.1:0") + server, errch := serveDNS(t, "127.0.0.1:0") defer func() { if err := <-errch; err != nil { t.Errorf("server error: %v", err) @@ -473,7 +486,7 @@ func TestConcurrentSetMap(t *testing.T) { func TestConcurrentSetUpstreams(t *testing.T) { dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site.")) - server, errch := serveDNS("127.0.0.1:0") + server, errch := serveDNS(t, "127.0.0.1:0") defer func() { if err := <-errch; err != nil { t.Errorf("server error: %v", err) @@ -752,7 +765,7 @@ func TestTrimRDNSBonjourPrefix(t *testing.T) { func BenchmarkFull(b *testing.B) { dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site.")) - server, errch := serveDNS("127.0.0.1:0") + server, errch := serveDNS(b, "127.0.0.1:0") defer func() { if err := <-errch; err != nil { b.Errorf("server error: %v", err) diff --git a/wgengine/tstun/tun.go b/wgengine/tstun/tun.go index 8a68f40f0..92af1b8b0 100644 --- a/wgengine/tstun/tun.go +++ b/wgengine/tstun/tun.go @@ -215,7 +215,17 @@ func (t *TUN) poll() { } } +var magicDNSIPPort = netaddr.MustParseIPPort("100.100.100.100:0") + func (t *TUN) filterOut(p *packet.Parsed) filter.Response { + // Fake ICMP echo responses to MagicDNS (100.100.100.100). + if p.IsEchoRequest() && p.Dst == magicDNSIPPort { + header := p.ICMP4Header() + header.ToResponse() + outp := packet.Generate(&header, p.Payload()) + t.InjectInboundCopy(outp) + return filter.DropSilently // don't pass on to OS; already handled + } if t.PreFilterOut != nil { if res := t.PreFilterOut(p, t); res.IsDrop() { @@ -259,6 +269,8 @@ func (t *TUN) IdleDuration() time.Duration { func (t *TUN) Read(buf []byte, offset int) (int, error) { var n int + wasInjectedPacket := false + select { case <-t.closed: return 0, io.EOF @@ -273,9 +285,7 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) { t.bufferConsumed <- struct{}{} } else { // If the packet is not from t.buffer, then it is an injected packet. - // In this case, we return early to bypass filtering - t.noteActivity() - return n, nil + wasInjectedPacket = true } } @@ -289,6 +299,12 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) { } } + // For injected packets, we return early to bypass filtering. + if wasInjectedPacket { + t.noteActivity() + return n, nil + } + if !t.disableFilter { response := t.filterOut(p) if response != filter.Accept { diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 8638b3d38..f3ce131c9 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -36,6 +36,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/types/netmap" "tailscale.com/types/wgkey" "tailscale.com/version" "tailscale.com/version/distro" @@ -170,16 +171,16 @@ func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16, impl FakeImplFu // NewUserspaceEngine creates the named tun device and returns a // Tailscale Engine running on it. -func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (Engine, error) { - if tunname == "" { +func NewUserspaceEngine(logf logger.Logf, tunName string, listenPort uint16) (Engine, error) { + if tunName == "" { return nil, fmt.Errorf("--tun name must not be blank") } - logf("Starting userspace wireguard engine with tun device %q", tunname) + logf("Starting userspace wireguard engine with tun device %q", tunName) - tun, err := tun.CreateTUN(tunname, minimalMTU) + tun, err := tun.CreateTUN(tunName, minimalMTU) if err != nil { - diagnoseTUNFailure(logf) + diagnoseTUNFailure(tunName, logf) logf("CreateTUN: %v", err) return nil, err } @@ -308,16 +309,20 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) { // Ping every single-IP that peer routes. // These synthetic packets are used to traverse NATs. var ips []netaddr.IP - allowedIPs := deviceAllowedIPs.EntriesForPeer(peer) - for _, ipNet := range allowedIPs { - if ones, bits := ipNet.Mask.Size(); ones == bits && ones != 0 { - ip, ok := netaddr.FromStdIP(ipNet.IP) - if !ok { - continue - } + var allowedIPs []netaddr.IPPrefix + deviceAllowedIPs.EntriesForPeer(peer, func(stdIP net.IP, cidr uint) bool { + ip, ok := netaddr.FromStdIP(stdIP) + if !ok { + logf("[unexpected] bad IP from deviceAllowedIPs.EntriesForPeer: %v", stdIP) + return true + } + ipp := netaddr.IPPrefix{IP: ip, Bits: uint8(cidr)} + allowedIPs = append(allowedIPs, ipp) + if ipp.IsSingleIP() { ips = append(ips, ip) } - } + return true + }) if len(ips) > 0 { go e.pinger(peerWGKey, ips) } else { @@ -1070,20 +1075,15 @@ func (e *userspaceEngine) getStatus() (*Status, error) { defer pw.Close() // TODO(apenwarr): get rid of silly uapi stuff for in-process comms // FIXME: get notified of status changes instead of polling. - filter := device.IPCGetFilter{ - // The allowed_ips are somewhat expensive to compute and they're - // unused below; request that they not be sent instead. - FilterAllowedIPs: true, - } - err := e.wgdev.IpcGetOperationFiltered(pw, filter) + err := e.wgdev.IpcGetOperation(pw) if err != nil { err = fmt.Errorf("IpcGetOperation: %w", err) } errc <- err }() - pp := make(map[wgkey.Key]*PeerStatus) - p := &PeerStatus{} + pp := make(map[wgkey.Key]*ipnstate.PeerStatusLite) + p := &ipnstate.PeerStatusLite{} var hst1, hst2, n int64 @@ -1115,20 +1115,20 @@ func (e *userspaceEngine) getStatus() (*Status, error) { if err != nil { return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line) } - p = &PeerStatus{} + p = &ipnstate.PeerStatusLite{} pp[wgkey.Key(pk)] = p key := tailcfg.NodeKey(pk) p.NodeKey = key case "rx_bytes": n, err = mem.ParseInt(v, 10, 64) - p.RxBytes = ByteCount(n) + p.RxBytes = n if err != nil { return nil, fmt.Errorf("IpcGetOperation: rx_bytes invalid: %#v", line) } case "tx_bytes": n, err = mem.ParseInt(v, 10, 64) - p.TxBytes = ByteCount(n) + p.TxBytes = n if err != nil { return nil, fmt.Errorf("IpcGetOperation: tx_bytes invalid: %#v", line) } @@ -1154,7 +1154,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) { e.mu.Lock() defer e.mu.Unlock() - var peers []PeerStatus + var peers []ipnstate.PeerStatusLite for _, pk := range e.peerSequence { if p, ok := pp[pk]; ok { // ignore idle ones not in wireguard-go's config peers = append(peers, *p) @@ -1320,7 +1320,7 @@ func (e *userspaceEngine) SetDERPMap(dm *tailcfg.DERPMap) { e.magicConn.SetDERPMap(dm) } -func (e *userspaceEngine) SetNetworkMap(nm *controlclient.NetworkMap) { +func (e *userspaceEngine) SetNetworkMap(nm *netmap.NetworkMap) { e.magicConn.SetNetworkMap(nm) e.mu.Lock() callbacks := make([]NetworkMapCallback, 0, 4) @@ -1363,16 +1363,27 @@ func (e *userspaceEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { // the system and log some diagnostic info that might help debug why // TUN failed. Because TUN's already failed and things the program's // about to end, we might as well log a lot. -func diagnoseTUNFailure(logf logger.Logf) { +func diagnoseTUNFailure(tunName string, logf logger.Logf) { switch runtime.GOOS { case "linux": - diagnoseLinuxTUNFailure(logf) + diagnoseLinuxTUNFailure(tunName, logf) + case "darwin": + diagnoseDarwinTUNFailure(tunName, logf) default: logf("no TUN failure diagnostics for OS %q", runtime.GOOS) } } -func diagnoseLinuxTUNFailure(logf logger.Logf) { +func diagnoseDarwinTUNFailure(tunName string, logf logger.Logf) { + if os.Getuid() != 0 { + logf("failed to create TUN device as non-root user; use 'sudo tailscaled', or run under launchd with 'sudo tailscaled install-system-daemon'") + } + if tunName != "utun" { + logf("failed to create TUN device %q; try using tun device \"utun\" instead for automatic selection", tunName) + } +} + +func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf) { kernel, err := exec.Command("uname", "-r").Output() kernel = bytes.TrimSpace(kernel) if err != nil { diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index 91b5fe04e..130ce4610 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -13,10 +13,10 @@ import ( "time" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" "tailscale.com/net/interfaces" "tailscale.com/tailcfg" + "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" @@ -107,7 +107,7 @@ func (e *watchdogEngine) SetLinkChangeCallback(cb func(major bool, newState *int func (e *watchdogEngine) SetDERPMap(m *tailcfg.DERPMap) { e.watchdog("SetDERPMap", func() { e.wrap.SetDERPMap(m) }) } -func (e *watchdogEngine) SetNetworkMap(nm *controlclient.NetworkMap) { +func (e *watchdogEngine) SetNetworkMap(nm *netmap.NetworkMap) { e.watchdog("SetNetworkMap", func() { e.wrap.SetNetworkMap(nm) }) } func (e *watchdogEngine) AddNetworkMapCallback(callback NetworkMapCallback) func() { diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go index af86b36d6..2928e47d2 100644 --- a/wgengine/wgcfg/config.go +++ b/wgengine/wgcfg/config.go @@ -9,6 +9,11 @@ import ( "inet.af/netaddr" ) +// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key +// and is then the sole wireguard endpoint for peers with a non-zero discovery key. +// This form is then recognize by magicsock's CreateEndpoint. +const EndpointDiscoSuffix = ".disco.tailscale:12345" + // Config is a WireGuard configuration. // It only supports the set of things Tailscale uses. type Config struct { diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go new file mode 100644 index 000000000..36dc065c8 --- /dev/null +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -0,0 +1,127 @@ +// 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 nmcfg converts a controlclient.NetMap into a wgcfg config. +package nmcfg + +import ( + "fmt" + "net" + "strconv" + "strings" + + "inet.af/netaddr" + "tailscale.com/control/controlclient" + "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" + "tailscale.com/types/logger" + "tailscale.com/types/netmap" + "tailscale.com/wgengine/wgcfg" +) + +func nodeDebugName(n *tailcfg.Node) string { + name := n.Name + if name == "" { + name = n.Hostinfo.Hostname + } + if i := strings.Index(name, "."); i != -1 { + name = name[:i] + } + if name == "" && len(n.Addresses) != 0 { + return n.Addresses[0].String() + } + return name +} + +// cidrIsSubnet reports whether cidr is a non-default-route subnet +// exported by node that is not one of its own self addresses. +func cidrIsSubnet(node *tailcfg.Node, cidr netaddr.IPPrefix) bool { + if cidr.Bits == 0 { + return false + } + if !cidr.IsSingleIP() { + return true + } + for _, selfCIDR := range node.Addresses { + if cidr == selfCIDR { + return false + } + } + return true +} + +// WGCfg returns the NetworkMaps's Wireguard configuration. +func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags) (*wgcfg.Config, error) { + cfg := &wgcfg.Config{ + Name: "tailscale", + PrivateKey: wgcfg.PrivateKey(nm.PrivateKey), + Addresses: nm.Addresses, + ListenPort: nm.LocalPort, + Peers: make([]wgcfg.Peer, 0, len(nm.Peers)), + } + + for _, peer := range nm.Peers { + if controlclient.Debug.OnlyDisco && peer.DiscoKey.IsZero() { + continue + } + cfg.Peers = append(cfg.Peers, wgcfg.Peer{ + PublicKey: wgcfg.Key(peer.Key), + }) + cpeer := &cfg.Peers[len(cfg.Peers)-1] + if peer.KeepAlive { + cpeer.PersistentKeepalive = 25 // seconds + } + + if !peer.DiscoKey.IsZero() { + if err := appendEndpoint(cpeer, fmt.Sprintf("%x%s", peer.DiscoKey[:], wgcfg.EndpointDiscoSuffix)); err != nil { + return nil, err + } + cpeer.Endpoints = fmt.Sprintf("%x.disco.tailscale:12345", peer.DiscoKey[:]) + } else { + if err := appendEndpoint(cpeer, peer.DERP); err != nil { + return nil, err + } + for _, ep := range peer.Endpoints { + if err := appendEndpoint(cpeer, ep); err != nil { + return nil, err + } + } + } + for _, allowedIP := range peer.AllowedIPs { + if allowedIP.IsSingleIP() && tsaddr.IsTailscaleIP(allowedIP.IP) && (flags&netmap.AllowSingleHosts) == 0 { + logf("[v1] wgcfg: skipping node IP %v from %q (%v)", + allowedIP.IP, nodeDebugName(peer), peer.Key.ShortString()) + continue + } else if cidrIsSubnet(peer, allowedIP) { + if (flags & netmap.AllowSubnetRoutes) == 0 { + logf("[v1] wgcfg: not accepting subnet route %v from %q (%v)", + allowedIP, nodeDebugName(peer), peer.Key.ShortString()) + continue + } + } + cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP) + } + } + + return cfg, nil +} + +func appendEndpoint(peer *wgcfg.Peer, epStr string) error { + if epStr == "" { + return nil + } + _, port, err := net.SplitHostPort(epStr) + if err != nil { + return fmt.Errorf("malformed endpoint %q for peer %v", epStr, peer.PublicKey.ShortString()) + } + _, err = strconv.ParseUint(port, 10, 16) + if err != nil { + return fmt.Errorf("invalid port in endpoint %q for peer %v", epStr, peer.PublicKey.ShortString()) + } + if peer.Endpoints != "" { + peer.Endpoints += "," + } + peer.Endpoints += epStr + return nil +} diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 563888083..257d59f26 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -6,36 +6,23 @@ package wgengine import ( "errors" - "time" "inet.af/netaddr" - "tailscale.com/control/controlclient" "tailscale.com/ipn/ipnstate" "tailscale.com/net/interfaces" "tailscale.com/tailcfg" + "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" ) -// ByteCount is the number of bytes that have been sent or received. -// -// TODO: why is this a type? remove? -// TODO: document whether it's payload bytes only or if it includes framing overhead. -type ByteCount int64 - -type PeerStatus struct { - TxBytes, RxBytes ByteCount - LastHandshake time.Time - NodeKey tailcfg.NodeKey -} - // Status is the Engine status. // // TODO(bradfitz): remove this, subset of ipnstate? Need to migrate users. type Status struct { - Peers []PeerStatus + Peers []ipnstate.PeerStatusLite LocalAddrs []string // the set of possible endpoints for the magic conn DERPs int // number of active DERP connections } @@ -51,7 +38,7 @@ type NetInfoCallback func(*tailcfg.NetInfo) // NetworkMapCallback is the type used by callbacks that hook // into network map updates. -type NetworkMapCallback func(*controlclient.NetworkMap) +type NetworkMapCallback func(*netmap.NetworkMap) // someHandle is allocated so its pointer address acts as a unique // map key handle. (It needs to have non-zero size for Go to guarantee @@ -121,7 +108,7 @@ type Engine interface { // ignored as as it might be disabled; get it from SetDERPMap // instead. // The network map should only be read from. - SetNetworkMap(*controlclient.NetworkMap) + SetNetworkMap(*netmap.NetworkMap) // AddNetworkMapCallback adds a function to a list of callbacks // that are called when the network map updates. It returns a diff --git a/wgengine/wglog/wglog.go b/wgengine/wglog/wglog.go index 7786edd82..ed3827b4e 100644 --- a/wgengine/wglog/wglog.go +++ b/wgengine/wglog/wglog.go @@ -59,11 +59,9 @@ func NewLogger(logf logger.Logf) *Logger { // but there's not much we can do about that. logf("%s", new) } - std := logger.StdLogger(wrapper) ret.DeviceLogger = &device.Logger{ - Debug: std, - Info: std, - Error: std, + Verbosef: logger.WithPrefix(wrapper, "[v2] "), + Errorf: wrapper, } return ret } diff --git a/wgengine/wglog/wglog_test.go b/wgengine/wglog/wglog_test.go index 0b93a130a..077981e41 100644 --- a/wgengine/wglog/wglog_test.go +++ b/wgengine/wglog/wglog_test.go @@ -46,12 +46,11 @@ func TestLogger(t *testing.T) { // Then if logf also attempts to write into the channel, it'll fail. c <- "" } - x.DeviceLogger.Info.Println(tt.in) + x.DeviceLogger.Errorf(tt.in) got := <-c if tt.omit { continue } - tt.want += "\n" if got != tt.want { t.Errorf("Println(%q) = %q want %q", tt.in, got, tt.want) } diff --git a/wgengine/winnet/winnet.go b/wgengine/winnet/winnet.go index be76fd9ca..086b07638 100644 --- a/wgengine/winnet/winnet.go +++ b/wgengine/winnet/winnet.go @@ -2,13 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build windows + package winnet import ( "fmt" + "unsafe" + "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" - "unsafe" ) const CLSID_NetworkListManager = "{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"