diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index a6d304332..3947f4ff5 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -15,6 +15,7 @@ import ( "net/url" "strconv" + "tailscale.com/ipn/ipnstate" "tailscale.com/safesocket" "tailscale.com/tailcfg" ) @@ -79,6 +80,7 @@ func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, erro return r, nil } +// Goroutines returns a dump of the Tailscale daemon's current goroutines. func Goroutines(ctx context.Context) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil) if err != nil { @@ -98,3 +100,34 @@ func Goroutines(ctx context.Context) ([]byte, error) { } return body, nil } + +// Status returns the Tailscale daemon's status. +func Status(ctx context.Context) (*ipnstate.Status, error) { + return status(ctx, "") +} + +// StatusWithPeers returns the Tailscale daemon's status, without the peer info. +func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) { + return status(ctx, "?peers=false") +} + +func status(ctx context.Context, queryString string) (*ipnstate.Status, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/status"+queryString, nil) + if err != nil { + return nil, err + } + res, err := DoLocalRequest(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + body, _ := ioutil.ReadAll(res.Body) + return nil, fmt.Errorf("HTTP %s: %s", res.Status, body) + } + st := new(ipnstate.Status) + if err := json.NewDecoder(res.Body).Decode(st); err != nil { + return nil, err + } + return st, nil +} diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 254210ab6..1d7f95766 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -9,6 +9,7 @@ package cli import ( "context" "flag" + "fmt" "log" "net" "os" @@ -16,6 +17,7 @@ import ( "runtime" "strings" "syscall" + "text/tabwriter" "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/ipn" @@ -53,9 +55,7 @@ func Run(args []string) error { ShortUsage: "tailscale [flags] [command flags]", ShortHelp: "The easiest, most secure way to use WireGuard.", LongHelp: strings.TrimSpace(` -For help on subcommands, add -help after: "tailscale status -help". - -All flags can use single or double hyphen prefixes (-help or --help). +For help on subcommands, add --help after: "tailscale status --help". This CLI is still under active development. Commands and flags will change in the future. @@ -64,12 +64,17 @@ change in the future. upCmd, downCmd, netcheckCmd, + ipCmd, statusCmd, pingCmd, versionCmd, }, - FlagSet: rootfs, - Exec: func(context.Context, []string) error { return flag.ErrHelp }, + FlagSet: rootfs, + Exec: func(context.Context, []string) error { return flag.ErrHelp }, + UsageFunc: usageFunc, + } + for _, c := range rootCmd.Subcommands { + c.UsageFunc = usageFunc } // Don't advertise the debug command, but it exists. @@ -147,3 +152,72 @@ func strSliceContains(ss []string, s string) bool { } return false } + +func usageFunc(c *ffcli.Command) string { + var b strings.Builder + + fmt.Fprintf(&b, "USAGE\n") + if c.ShortUsage != "" { + fmt.Fprintf(&b, " %s\n", c.ShortUsage) + } else { + fmt.Fprintf(&b, " %s\n", c.Name) + } + fmt.Fprintf(&b, "\n") + + if c.LongHelp != "" { + fmt.Fprintf(&b, "%s\n\n", c.LongHelp) + } + + if len(c.Subcommands) > 0 { + fmt.Fprintf(&b, "SUBCOMMANDS\n") + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) + for _, subcommand := range c.Subcommands { + fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp) + } + tw.Flush() + fmt.Fprintf(&b, "\n") + } + + if countFlags(c.FlagSet) > 0 { + fmt.Fprintf(&b, "FLAGS\n") + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) + c.FlagSet.VisitAll(func(f *flag.Flag) { + var s string + name, usage := flag.UnquoteUsage(f) + if isBoolFlag(f) { + s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name) + } else { + s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments. + if len(name) > 0 { + s += " " + name + } + } + // Four spaces before the tab triggers good alignment + // for both 4- and 8-space tab stops. + s += "\n \t" + s += strings.ReplaceAll(usage, "\n", "\n \t") + + if f.DefValue != "" { + s += fmt.Sprintf(" (default %s)", f.DefValue) + } + + fmt.Fprintln(&b, s) + }) + tw.Flush() + fmt.Fprintf(&b, "\n") + } + + return strings.TrimSpace(b.String()) +} + +func isBoolFlag(f *flag.Flag) bool { + bf, ok := f.Value.(interface { + IsBoolFlag() bool + }) + return ok && bf.IsBoolFlag() +} + +func countFlags(fs *flag.FlagSet) (n int) { + fs.VisitAll(func(*flag.Flag) { n++ }) + return n +} diff --git a/cmd/tailscale/cli/down.go b/cmd/tailscale/cli/down.go index 7e53c0cf2..dd1e8491f 100644 --- a/cmd/tailscale/cli/down.go +++ b/cmd/tailscale/cli/down.go @@ -6,10 +6,12 @@ package cli import ( "context" + "fmt" "log" "time" "github.com/peterbourgon/ff/v2/ffcli" + "tailscale.com/client/tailscale" "tailscale.com/ipn" ) @@ -26,6 +28,16 @@ func runDown(ctx context.Context, args []string) error { log.Fatalf("too many non-flag arguments: %q", args) } + st, err := tailscale.Status(ctx) + if err != nil { + return fmt.Errorf("error fetching current status: %w", err) + } + if st.BackendState == "Stopped" { + log.Printf("already stopped") + return nil + } + log.Printf("was in state %q", st.BackendState) + c, bc, ctx, cancel := connect(ctx) defer cancel() @@ -38,17 +50,6 @@ func runDown(ctx context.Context, args []string) error { if n.ErrMessage != nil { log.Fatal(*n.ErrMessage) } - if n.Status != nil { - cur := n.Status.BackendState - switch cur { - case "Stopped": - log.Printf("already stopped") - cancel() - default: - log.Printf("was in state %q", cur) - } - return - } if n.State != nil { log.Printf("now in state %q", *n.State) if *n.State == ipn.Stopped { @@ -58,7 +59,6 @@ func runDown(ctx context.Context, args []string) error { } }) - bc.RequestStatus() bc.SetWantRunning(false) pump(ctx, bc, c) diff --git a/cmd/tailscale/cli/ip.go b/cmd/tailscale/cli/ip.go new file mode 100644 index 000000000..053ea165e --- /dev/null +++ b/cmd/tailscale/cli/ip.go @@ -0,0 +1,69 @@ +// 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 cli + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/peterbourgon/ff/v2/ffcli" + "tailscale.com/client/tailscale" +) + +var ipCmd = &ffcli.Command{ + Name: "ip", + ShortUsage: "ip [-4] [-6]", + ShortHelp: "Show this machine's current Tailscale IP address(es)", + Exec: runIP, + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("ip", flag.ExitOnError) + fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address") + fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address") + return fs + })(), +} + +var ipArgs struct { + want4 bool + want6 bool +} + +func runIP(ctx context.Context, args []string) error { + if len(args) > 0 { + return errors.New("unknown arguments") + } + v4, v6 := ipArgs.want4, ipArgs.want6 + if v4 && v6 { + return errors.New("tailscale up -4 and -6 are mutually exclusive") + } + if !v4 && !v6 { + v4, v6 = true, true + } + st, err := tailscale.Status(ctx) + if err != nil { + return err + } + if len(st.TailscaleIPs) == 0 { + return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState) + } + match := false + for _, ip := range st.TailscaleIPs { + if ip.Is4() && v4 || ip.Is6() && v6 { + match = true + fmt.Println(ip) + } + } + if !match { + if ipArgs.want4 { + return errors.New("no Tailscale IPv4 address") + } + if ipArgs.want6 { + return errors.New("no Tailscale IPv6 address") + } + } + return nil +} diff --git a/cmd/tailscale/cli/ping.go b/cmd/tailscale/cli/ping.go index f98a08d4f..c82eb7902 100644 --- a/cmd/tailscale/cli/ping.go +++ b/cmd/tailscale/cli/ping.go @@ -15,6 +15,7 @@ import ( "time" "github.com/peterbourgon/ff/v2/ffcli" + "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" ) @@ -47,6 +48,7 @@ relay node. fs := flag.NewFlagSet("ping", flag.ExitOnError) fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output") fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established") + fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)") fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send") fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping") return fs @@ -57,6 +59,7 @@ var pingArgs struct { num int untilDirect bool verbose bool + tsmp bool timeout time.Duration } @@ -69,7 +72,6 @@ func runPing(ctx context.Context, args []string) error { } var ip string prc := make(chan *ipnstate.PingResult, 1) - stc := make(chan *ipnstate.Status, 1) bc.SetNotifyCallback(func(n ipn.Notify) { if n.ErrMessage != nil { log.Fatal(*n.ErrMessage) @@ -77,9 +79,6 @@ func runPing(ctx context.Context, args []string) error { if pr := n.PingResult; pr != nil && pr.IP == ip { prc <- pr } - if n.Status != nil { - stc <- n.Status - } }) go pump(ctx, bc, c) @@ -92,17 +91,15 @@ func runPing(ctx context.Context, args []string) error { // Otherwise, try to resolve it first from the network peer list. if ip == "" { - bc.RequestStatus() - select { - case st := <-stc: - for _, ps := range st.Peer { - if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName { - ip = ps.TailAddr - break - } + st, err := tailscale.Status(ctx) + if err != nil { + return err + } + for _, ps := range st.Peer { + if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName { + ip = ps.TailAddr + break } - case <-ctx.Done(): - return ctx.Err() } } @@ -125,7 +122,7 @@ func runPing(ctx context.Context, args []string) error { anyPong := false for { n++ - bc.Ping(ip) + bc.Ping(ip, pingArgs.tsmp) timer := time.NewTimer(pingArgs.timeout) select { case <-timer.C: @@ -140,8 +137,16 @@ func runPing(ctx context.Context, args []string) error { if pr.DERPRegionID != 0 { via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode) } + if pingArgs.tsmp { + // TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries? + // For now just say it came via TSMP. + via = "TSMP" + } anyPong = true fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency) + if pingArgs.tsmp { + return nil + } if pr.Endpoint != "" && pingArgs.untilDirect { return nil } diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index 851b0c2bd..72b843f2d 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -10,7 +10,6 @@ import ( "encoding/json" "flag" "fmt" - "log" "net" "net/http" "os" @@ -19,6 +18,7 @@ import ( "github.com/peterbourgon/ff/v2/ffcli" "github.com/toqueteos/webbrowser" + "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/net/interfaces" @@ -27,7 +27,7 @@ import ( var statusCmd = &ffcli.Command{ Name: "status", - ShortUsage: "status [-active] [-web] [-json]", + ShortUsage: "status [--active] [--web] [--json]", ShortHelp: "Show state of tailscaled and its connections", Exec: runStatus, FlagSet: (func() *flag.FlagSet { @@ -53,47 +53,8 @@ var statusArgs struct { peers bool // in CLI mode, show status of peer machines } -func getStatusFromServer(ctx context.Context, c net.Conn, bc *ipn.BackendClient) func() (*ipnstate.Status, error) { - ch := make(chan *ipnstate.Status, 1) - bc.SetNotifyCallback(func(n ipn.Notify) { - if n.ErrMessage != nil { - log.Fatal(*n.ErrMessage) - } - if n.Status != nil { - select { - case ch <- n.Status: - default: - // A status update from somebody else's request. - // Ignoring this matters mostly for "tailscale status -web" - // mode, otherwise the channel send would block forever - // and pump would stop reading from tailscaled, which - // previously caused tailscaled to block (while holding - // a mutex), backing up unrelated clients. - // See https://github.com/tailscale/tailscale/issues/1234 - } - } - }) - go pump(ctx, bc, c) - - return func() (*ipnstate.Status, error) { - bc.RequestStatus() - select { - case st := <-ch: - return st, nil - case <-ctx.Done(): - return nil, ctx.Err() - } - } -} - func runStatus(ctx context.Context, args []string) error { - c, bc, ctx, cancel := connect(ctx) - defer cancel() - - bc.AllowVersionSkew = true - - getStatus := getStatusFromServer(ctx, c, bc) - st, err := getStatus() + st, err := tailscale.Status(ctx) if err != nil { return err } @@ -131,7 +92,7 @@ func runStatus(ctx context.Context, args []string) error { http.NotFound(w, r) return } - st, err := getStatus() + st, err := tailscale.Status(ctx) if err != nil { http.Error(w, err.Error(), 500) return diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 75acae665..0a66eb139 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -21,6 +21,7 @@ import ( "github.com/peterbourgon/ff/v2/ffcli" "inet.af/netaddr" + "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/tailcfg" "tailscale.com/types/preftype" @@ -48,11 +49,11 @@ specify any flags, options are reset to their default. 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)") + upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. \"tag:eng,tag:montreal,tag:ssh\")") upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key") upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS") if runtime.GOOS == "linux" || isBSD(runtime.GOOS) { - upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)") + upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")") upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet") } if runtime.GOOS == "linux" { @@ -253,7 +254,7 @@ func runUp(ctx context.Context, args []string) error { defer cancel() if !prefs.ExitNodeIP.IsZero() { - st, err := getStatusFromServer(ctx, c, bc)() + st, err := tailscale.Status(ctx) if err != nil { fatalf("can't fetch status from tailscaled: %v", err) } @@ -315,7 +316,7 @@ func runUp(ctx context.Context, args []string) error { // supports server mode, though, the transition to StateStore // is only half complete. Only server mode uses it, and the // Windows service (~tailscaled) is the one that computes the - // StateKey based on the connection idenity. So for now, just + // StateKey based on the connection identity. So for now, just // do as the Windows GUI's always done: if runtime.GOOS == "windows" { // The Windows service will set this as needed based diff --git a/cmd/tailscale/cli/version.go b/cmd/tailscale/cli/version.go index a6bbb6912..2c6f97a3b 100644 --- a/cmd/tailscale/cli/version.go +++ b/cmd/tailscale/cli/version.go @@ -11,7 +11,7 @@ import ( "log" "github.com/peterbourgon/ff/v2/ffcli" - "tailscale.com/ipn" + "tailscale.com/client/tailscale" "tailscale.com/version" ) @@ -42,29 +42,10 @@ func runVersion(ctx context.Context, args []string) error { fmt.Printf("Client: %s\n", version.String()) - c, bc, ctx, cancel := connect(ctx) - defer cancel() - - bc.AllowVersionSkew = true - - done := make(chan struct{}) - - bc.SetNotifyCallback(func(n ipn.Notify) { - if n.ErrMessage != nil { - log.Fatal(*n.ErrMessage) - } - if n.Status != nil { - fmt.Printf("Daemon: %s\n", n.Version) - close(done) - } - }) - go pump(ctx, bc, c) - - bc.RequestStatus() - select { - case <-done: - return nil - case <-ctx.Done(): - return ctx.Err() + st, err := tailscale.StatusWithoutPeers(ctx) + if err != nil { + return err } + fmt.Printf("Daemon: %s\n", st.Version) + return nil } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 2e722db5c..74ae1d54e 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -39,6 +39,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ W tailscale.com/tsconst from tailscale.com/net/interfaces tailscale.com/types/empty from tailscale.com/ipn + tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ tailscale.com/types/key from tailscale.com/derp+ tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+ tailscale.com/types/netmap from tailscale.com/ipn @@ -50,7 +51,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/types/wgkey from tailscale.com/types/netmap+ tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ W tailscale.com/util/endian from tailscale.com/net/netns - LW tailscale.com/util/lineread from tailscale.com/net/interfaces + L 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/filter from tailscale.com/types/netmap @@ -65,7 +66,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+ golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ - golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal 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 @@ -73,8 +73,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep golang.org/x/net/idna from golang.org/x/net/http/httpguts+ 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/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+ @@ -135,13 +133,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep html from tailscale.com/ipn/ipnstate io from bufio+ io/fs from crypto/rand+ - io/ioutil from golang.org/x/oauth2/internal+ + io/ioutil from golang.org/x/sys/cpu+ log from expvar+ math from compress/flate+ math/big from crypto/dsa+ math/bits from compress/flate+ math/rand from math/big+ - mime from golang.org/x/oauth2/internal+ + mime from mime/multipart+ mime/multipart from net/http mime/quotedprintable from mime/multipart net from crypto/tls+ @@ -165,7 +163,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep 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/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c9cd378e5..d91a960c4 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -3,10 +3,11 @@ 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 L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router + W 💣 github.com/github/certstore from tailscale.com/control/controlclient 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/godbus/dbus/v5 from tailscale.com/net/dns github.com/google/btree from inet.af/netstack/tcpip/header+ L github.com/josharian/native from github.com/mdlayher/netlink+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor @@ -19,6 +20,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 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 + W github.com/pkg/errors from github.com/github/certstore 💣 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/ipc from github.com/tailscale/wireguard-go/device @@ -88,6 +90,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/logtail/backoff from tailscale.com/control/controlclient+ tailscale.com/logtail/filch from tailscale.com/logpolicy tailscale.com/metrics from tailscale.com/derp + tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+ tailscale.com/net/dnscache from tailscale.com/control/controlclient+ tailscale.com/net/dnsfallback from tailscale.com/control/controlclient tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+ @@ -102,6 +105,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ + tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ tailscale.com/paths from tailscale.com/cmd/tailscaled+ tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/ipn/ipnserver @@ -113,6 +117,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/tstime from tailscale.com/wgengine/magicsock tailscale.com/types/empty from tailscale.com/control/controlclient+ tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled + tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ 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+ @@ -124,13 +129,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 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/util/dnsname from tailscale.com/wgengine/tsdns+ + tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+ LW tailscale.com/util/endian from tailscale.com/net/netns+ - LW tailscale.com/util/lineread from tailscale.com/control/controlclient+ + L tailscale.com/util/lineread from tailscale.com/control/controlclient+ tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/systemd from tailscale.com/control/controlclient+ - tailscale.com/util/winutil from tailscale.com/logpolicy + tailscale.com/util/winutil from tailscale.com/logpolicy+ tailscale.com/version from tailscale.com/cmd/tailscaled+ tailscale.com/version/distro from tailscale.com/control/controlclient+ tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ @@ -139,9 +144,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 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/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/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal tailscale.com/wgengine/wglog from tailscale.com/wgengine @@ -159,7 +161,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+ 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/http/httpguts from net/http golang.org/x/net/http/httpproxy from net/http @@ -169,8 +170,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 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+ - 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+ @@ -241,7 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de math/big from crypto/dsa+ math/bits from compress/flate+ math/rand from github.com/mdlayher/netlink+ - mime from golang.org/x/oauth2/internal+ + mime from mime/multipart+ mime/multipart from net/http mime/quotedprintable from mime/multipart net from crypto/tls+ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index a760db708..931072b55 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -32,6 +32,7 @@ import ( "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" "tailscale.com/net/socks5" + "tailscale.com/net/tstun" "tailscale.com/paths" "tailscale.com/types/flagtype" "tailscale.com/types/logger" @@ -43,7 +44,6 @@ import ( "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/netstack" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/tstun" ) // globalStateKey is the ipn.StateKey that tailscaled loads on @@ -316,18 +316,7 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, is var errs []error for _, name := range strings.Split(args.tunname, ",") { logf("wgengine.NewUserspaceEngine(tun %q) ...", name) - conf := wgengine.Config{ - ListenPort: args.port, - LinkMonitor: linkMon, - } - isUserspace = name == "userspace-networking" - if isUserspace { - conf.TUN = tstun.NewFakeTUN() - conf.RouterGen = router.NewFake - } else { - conf.TUNName = name - } - e, err := wgengine.NewUserspaceEngine(logf, conf) + e, isUserspace, err = tryEngine(logf, linkMon, name) if err == nil { return e, isUserspace, nil } @@ -337,6 +326,33 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, is return nil, false, multierror.New(errs) } +func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, isUserspace bool, err error) { + conf := wgengine.Config{ + ListenPort: args.port, + LinkMonitor: linkMon, + } + isUserspace = name == "userspace-networking" + if !isUserspace { + dev, err := tstun.New(logf, name) + if err != nil { + tstun.Diagnose(logf, name) + return nil, false, err + } + conf.Tun = dev + r, err := router.New(logf, dev) + if err != nil { + dev.Close() + return nil, false, err + } + conf.Router = r + } + e, err = wgengine.NewUserspaceEngine(logf, conf) + if err != nil { + return nil, isUserspace, err + } + return e, isUserspace, nil +} + func newDebugMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", pprof.Index) diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index cf97bef4a..b57190b7e 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -30,10 +30,12 @@ import ( "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/ipn/ipnserver" "tailscale.com/logpolicy" + "tailscale.com/net/tstun" "tailscale.com/tempfork/wireguard-windows/firewall" "tailscale.com/types/logger" "tailscale.com/version" "tailscale.com/wgengine" + "tailscale.com/wgengine/router" ) const serviceName = "Tailscale" @@ -159,11 +161,23 @@ func startIPNServer(ctx context.Context, logid string) error { var err error getEngine := func() (wgengine.Engine, error) { + dev, err := tstun.New(logf, "Tailscale") + if err != nil { + return nil, err + } + r, err := router.New(logf, dev) + if err != nil { + dev.Close() + return nil, err + } eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{ - TUNName: "Tailscale", + Tun: dev, + Router: r, ListenPort: 41641, }) if err != nil { + r.Close() + dev.Close() return nil, err } return wgengine.NewWatchdog(eng), nil diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 2549bd9af..c731666e1 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -17,7 +17,6 @@ import ( "sync" "time" - "golang.org/x/oauth2" "tailscale.com/health" "tailscale.com/logtail/backoff" "tailscale.com/tailcfg" @@ -102,10 +101,10 @@ func (s Status) String() string { type LoginGoal struct { _ structs.Incomparable - wantLoggedIn bool // true if we *want* to be logged in - token *oauth2.Token // oauth token to use when logging in - flags LoginFlags // flags to use when logging in - url string // auth url that needs to be visited + wantLoggedIn bool // true if we *want* to be logged in + token *tailcfg.Oauth2Token // oauth token to use when logging in + flags LoginFlags // flags to use when logging in + url string // auth url that needs to be visited } // Client connects to a tailcontrol server for a node. @@ -668,7 +667,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ c.mu.Unlock() } -func (c *Client) Login(t *oauth2.Token, flags LoginFlags) { +func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) { c.logf("client.Login(%v, %v)", t != nil, flags) c.mu.Lock() diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 2977ece67..57631b72e 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -31,7 +31,6 @@ import ( "time" "golang.org/x/crypto/nacl/box" - "golang.org/x/oauth2" "inet.af/netaddr" "tailscale.com/health" "tailscale.com/log/logheap" @@ -266,7 +265,7 @@ func (c *Direct) TryLogout(ctx context.Context) error { return nil } -func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) { +func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) { c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags) return c.doLoginOrRegen(ctx, t, flags, false, "") } @@ -276,7 +275,7 @@ func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newUrl string, e return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url) } -func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) { +func (c *Direct) doLoginOrRegen(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) { mustregen, url, err := c.doLogin(ctx, t, flags, regen, url) if err != nil { return url, err @@ -288,7 +287,7 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi return url, err } -func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) { +func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) { c.mu.Lock() persist := c.persist tryingNewKey := c.tryingNewKey @@ -352,12 +351,14 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, err = errors.New("hostinfo: BackendLogID missing") return regen, url, err } + now := time.Now().Round(time.Second) request := tailcfg.RegisterRequest{ Version: 1, OldNodeKey: tailcfg.NodeKey(oldNodeKey), NodeKey: tailcfg.NodeKey(tryingNewKey.Public()), Hostinfo: hostinfo, Followup: url, + Timestamp: &now, } c.logf("RegisterReq: onode=%v node=%v fup=%v", request.OldNodeKey.ShortString(), @@ -366,6 +367,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, request.Auth.Provider = persist.Provider request.Auth.LoginName = persist.LoginName request.Auth.AuthKey = authKey + err = signRegisterRequest(&request, c.serverURL, c.serverKey, c.machinePrivKey.Public()) + if err != nil { + // If signing failed, clear all related fields + request.SignatureType = tailcfg.SignatureNone + request.Timestamp = nil + request.DeviceCert = nil + request.Signature = nil + + // Don't log the common error types. Signatures are not usually enabled, + // so these are expected. + if err != errCertificateNotConfigured && err != errNoCertStore { + c.logf("RegisterReq sign error: %v", err) + } + } bodyData, err := encode(request, &serverKey, &c.machinePrivKey) if err != nil { return regen, url, err diff --git a/control/controlclient/sign.go b/control/controlclient/sign.go new file mode 100644 index 000000000..83b35f6f7 --- /dev/null +++ b/control/controlclient/sign.go @@ -0,0 +1,31 @@ +// 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 controlclient + +import ( + "crypto" + "errors" + "fmt" + "time" + + "tailscale.com/types/wgkey" +) + +var ( + errNoCertStore = errors.New("no certificate store") + errCertificateNotConfigured = errors.New("no certificate subject configured") +) + +// HashRegisterRequest generates the hash required sign or verify a +// tailcfg.RegisterRequest with tailcfg.SignatureV1. +func HashRegisterRequest(ts time.Time, serverURL string, deviceCert []byte, serverPubKey, machinePubKey wgkey.Key) []byte { + h := crypto.SHA256.New() + + // hash.Hash.Write never returns an error, so we don't check for one here. + fmt.Fprintf(h, "%s%s%s%s%s", + ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey) + + return h.Sum(nil) +} diff --git a/control/controlclient/sign_supported.go b/control/controlclient/sign_supported.go new file mode 100644 index 000000000..9eadcafd0 --- /dev/null +++ b/control/controlclient/sign_supported.go @@ -0,0 +1,160 @@ +// 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 windows,cgo + +// darwin,cgo is also supported by certstore but machineCertificateSubject will +// need to be loaded by a different mechanism, so this is not currently enabled +// on darwin. + +package controlclient + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "sync" + + "github.com/github/certstore" + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" + "tailscale.com/util/winutil" +) + +var getMachineCertificateSubjectOnce struct { + sync.Once + v string // Subject of machine certificate to search for +} + +// getMachineCertificateSubject returns the exact name of a Subject that needs +// to be present in an identity's certificate chain to sign a RegisterRequest, +// formatted as per pkix.Name.String(). The Subject may be that of the identity +// itself, an intermediate CA or the root CA. +// +// If getMachineCertificateSubject() returns "" then no lookup will occur and +// each RegisterRequest will be unsigned. +// +// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA" +func getMachineCertificateSubject() string { + getMachineCertificateSubjectOnce.Do(func() { + getMachineCertificateSubjectOnce.v = winutil.GetRegString("MachineCertificateSubject", "") + }) + + return getMachineCertificateSubjectOnce.v +} + +var ( + errNoMatch = errors.New("no matching certificate") + errBadRequest = errors.New("malformed request") +) + +// findIdentity locates an identity from the Windows or Darwin certificate +// store. It returns the first certificate with a matching Subject anywhere in +// its certificate chain, so it is possible to search for the leaf certificate, +// intermediate CA or root CA. If err is nil then the returned identity will +// never be nil (if no identity is found, the error errNoMatch will be +// returned). If an identity is returned then its certificate chain is also +// returned. +func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x509.Certificate, error) { + ids, err := st.Identities() + if err != nil { + return nil, nil, err + } + + var selected certstore.Identity + var chain []*x509.Certificate + + for _, id := range ids { + chain, err = id.CertificateChain() + if err != nil { + continue + } + + if chain[0].PublicKeyAlgorithm != x509.RSA { + continue + } + + for _, c := range chain { + if c.Subject.String() == subject { + selected = id + break + } + } + } + + for _, id := range ids { + if id != selected { + id.Close() + } + } + + if selected == nil { + return nil, nil, errNoMatch + } + + return selected, chain, nil +} + +// signRegisterRequest looks for a suitable machine identity from the local +// system certificate store, and if one is found, signs the RegisterRequest +// using that identity's public key. In addition to the signature, the full +// certificate chain is included so that the control server can validate the +// certificate from a copy of the root CA's certificate. +func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("signRegisterRequest: %w", err) + } + }() + + if req.Timestamp == nil { + return errBadRequest + } + + machineCertificateSubject := getMachineCertificateSubject() + if machineCertificateSubject == "" { + return errCertificateNotConfigured + } + + st, err := certstore.Open(certstore.System) + if err != nil { + return fmt.Errorf("open cert store: %w", err) + } + defer st.Close() + + id, chain, err := findIdentity(machineCertificateSubject, st) + if err != nil { + return fmt.Errorf("find identity: %w", err) + } + defer id.Close() + + signer, err := id.Signer() + if err != nil { + return fmt.Errorf("create signer: %w", err) + } + + cl := 0 + for _, c := range chain { + cl += len(c.Raw) + } + req.DeviceCert = make([]byte, 0, cl) + for _, c := range chain { + req.DeviceCert = append(req.DeviceCert, c.Raw...) + } + + h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey) + + req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA256, + }) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + req.SignatureType = tailcfg.SignatureV1 + + return nil +} diff --git a/control/controlclient/sign_unsupported.go b/control/controlclient/sign_unsupported.go new file mode 100644 index 000000000..e20ced316 --- /dev/null +++ b/control/controlclient/sign_unsupported.go @@ -0,0 +1,17 @@ +// 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 !windows !cgo + +package controlclient + +import ( + "tailscale.com/tailcfg" + "tailscale.com/types/wgkey" +) + +// signRegisterRequest on non-supported platforms always returns errNoCertStore. +func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error { + return errNoCertStore +} diff --git a/go.mod b/go.mod index 4d4fa34c4..3cfff12dd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/coreos/go-iptables v0.4.5 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/github/certstore v0.1.0 github.com/gliderlabs/ssh v0.2.2 github.com/go-multierror/multierror v1.0.2 github.com/go-ole/go-ole v1.2.4 @@ -23,20 +24,18 @@ require ( github.com/peterbourgon/ff/v2 v2.0.0 github.com/pkg/errors v0.9.1 // indirect github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 - github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 + github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0 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-20210317152858-513c2a44f670 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 - golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 - google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b // indirect gopkg.in/yaml.v2 v2.2.8 // indirect honnef.co/go/tools v0.1.0 inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 @@ -44,3 +43,5 @@ require ( inet.af/peercred v0.0.0-20210302202138-56e694897155 rsc.io/goversion v1.2.0 ) + +replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 diff --git a/go.sum b/go.sum index f954a508b..ce753994b 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,5 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= @@ -47,95 +13,42 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 h1:TGPWAij+nY2FB7TlyUTqTmYvXJon/AZAfRMYc/76K80= +github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2/go.mod h1:Sgb3YVYOB2iCO06NJ6We5gjXe7uxxM3zPYoEXjuTKno= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo= github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE= github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w= github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= @@ -148,8 +61,6 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do= github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -196,7 +107,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= @@ -209,6 +119,10 @@ github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBW github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= 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/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a h1:tQ7Y0ALSe5109GMFB7TVtfNBsVcAuM422hVSJrXWMTE= +github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924= +github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0 h1:7KFBvUmm3TW/K+bAN22D7M6xSSoY/39s+PajaNBGrLw= +github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0/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= @@ -217,15 +131,8 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk= @@ -234,77 +141,29 @@ go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1: go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc= go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -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= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -314,59 +173,26 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/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= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84 h1:duBc5zuJsmJXYOVVE/6PxejI+N3AaCqKjtsoLn1Je5Q= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -376,6 +202,7 @@ golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -383,60 +210,16 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -446,84 +229,9 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE= golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA= +golang.zx2c4.com/wireguard v0.0.20201118/go.mod h1:Dz+cq5bnrai9EpgYj4GDof/+qaGzbRWbeaAOs1bUYa0= golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs= golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b h1:jEdfCm+8YTWSYgU4L7Nq0jjU+q9RxIhi0cXLTY+Ih3A= -google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= @@ -534,13 +242,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c= honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM= inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4= @@ -549,8 +250,5 @@ inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJ inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM= inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE= inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/health/health.go b/health/health.go index 81d346d15..3da92b8a4 100644 --- a/health/health.go +++ b/health/health.go @@ -35,6 +35,7 @@ var ( lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest ipnState string ipnWantRunning bool + anyInterfaceUp = true // until told otherwise ) // Subsystem is the name of a subsystem whose health can be monitored. @@ -195,6 +196,14 @@ func SetIPNState(state string, wantRunning bool) { selfCheckLocked() } +// SetAnyInterfaceUp sets whether any network interface is up. +func SetAnyInterfaceUp(up bool) { + mu.Lock() + defer mu.Unlock() + anyInterfaceUp = up + selfCheckLocked() +} + func timerSelfCheck() { mu.Lock() defer mu.Unlock() @@ -213,6 +222,9 @@ func selfCheckLocked() { } func overallErrorLocked() error { + if !anyInterfaceUp { + return errors.New("network down") + } if ipnState != "Running" || !ipnWantRunning { return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning) } diff --git a/internal/deepprint/deepprint_test.go b/internal/deepprint/deepprint_test.go index e5b2b0924..c7e2031f2 100644 --- a/internal/deepprint/deepprint_test.go +++ b/internal/deepprint/deepprint_test.go @@ -9,8 +9,8 @@ import ( "testing" "inet.af/netaddr" + "tailscale.com/net/dns" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/router/dns" "tailscale.com/wgengine/wgcfg" ) diff --git a/ipn/backend.go b/ipn/backend.go index 9352853b1..dee548c54 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - "golang.org/x/oauth2" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "tailscale.com/types/empty" @@ -28,7 +27,7 @@ const ( Running ) -// GoogleIDToken Type is the oauth2.Token.TokenType for the Google +// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google // ID tokens used by the Android client. const GoogleIDTokenType = "ts_android_google_login" @@ -65,7 +64,6 @@ type Notify struct { 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 @@ -143,7 +141,7 @@ type Backend interface { // eventually. StartLoginInteractive() // Login logs in with an OAuth2 token. - Login(token *oauth2.Token) + Login(token *tailcfg.Oauth2Token) // Logout terminates the current login session and stops the // wireguard engine. Logout() @@ -159,9 +157,6 @@ type Backend interface { // counts. Connection events are emitted automatically without // polling. RequestEngineStatus() - // RequestStatus requests that a full Status update - // notification is sent. - RequestStatus() // FakeExpireAfter pretends that the current key is going to // expire after duration x. This is useful for testing GUIs to // make sure they react properly with keys that are going to @@ -170,5 +165,5 @@ type Backend interface { // Ping attempts to start connecting to the given IP and sends a Notify // with its PingResult. If the host is down, there might never // be a PingResult sent. The cmd/tailscale CLI client adds a timeout. - Ping(ip string) + Ping(ip string, useTSMP bool) } diff --git a/ipn/fake_test.go b/ipn/fake_test.go index e918f77f0..eef580f57 100644 --- a/ipn/fake_test.go +++ b/ipn/fake_test.go @@ -8,8 +8,8 @@ import ( "log" "time" - "golang.org/x/oauth2" "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" "tailscale.com/types/netmap" ) @@ -46,7 +46,7 @@ func (b *FakeBackend) StartLoginInteractive() { b.login() } -func (b *FakeBackend) Login(token *oauth2.Token) { +func (b *FakeBackend) Login(token *tailcfg.Oauth2Token) { b.login() } @@ -87,14 +87,10 @@ func (b *FakeBackend) RequestEngineStatus() { b.notify(Notify{Engine: &EngineStatus{}}) } -func (b *FakeBackend) RequestStatus() { - b.notify(Notify{Status: &ipnstate.Status{}}) -} - func (b *FakeBackend) FakeExpireAfter(x time.Duration) { b.notify(Notify{NetMap: &netmap.NetworkMap{}}) } -func (b *FakeBackend) Ping(ip string) { +func (b *FakeBackend) Ping(ip string, useTSMP bool) { b.notify(Notify{PingResult: &ipnstate.PingResult{}}) } diff --git a/ipn/handle.go b/ipn/handle.go index 91b757f56..54a61140e 100644 --- a/ipn/handle.go +++ b/ipn/handle.go @@ -8,8 +8,8 @@ import ( "sync" "time" - "golang.org/x/oauth2" "inet.af/netaddr" + "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/netmap" ) @@ -155,7 +155,7 @@ func (h *Handle) StartLoginInteractive() { h.b.StartLoginInteractive() } -func (h *Handle) Login(token *oauth2.Token) { +func (h *Handle) Login(token *tailcfg.Oauth2Token) { h.b.Login(token) } @@ -167,10 +167,6 @@ func (h *Handle) RequestEngineStatus() { h.b.RequestEngineStatus() } -func (h *Handle) RequestStatus() { - h.b.RequestStatus() -} - func (h *Handle) FakeExpireAfter(x time.Duration) { h.b.FakeExpireAfter(x) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 38c9323d7..f6173a763 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -9,13 +9,14 @@ import ( "context" "errors" "fmt" + "net" "os" "runtime" + "strconv" "strings" "sync" "time" - "golang.org/x/oauth2" "inet.af/netaddr" "tailscale.com/control/controlclient" "tailscale.com/health" @@ -23,6 +24,7 @@ import ( "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/ipn/policy" + "tailscale.com/net/dns" "tailscale.com/net/interfaces" "tailscale.com/net/tsaddr" "tailscale.com/portlist" @@ -38,8 +40,6 @@ import ( "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/router/dns" - "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg/nmcfg" ) @@ -96,15 +96,16 @@ type LocalBackend struct { // hostinfo is mutated in-place while mu is held. hostinfo *tailcfg.Hostinfo // netMap is not mutated in-place once set. - netMap *netmap.NetworkMap - nodeByAddr map[netaddr.IP]*tailcfg.Node - activeLogin string // last logged LoginName from netMap - engineStatus ipn.EngineStatus - endpoints []string - blocked bool - authURL string - interact bool - prevIfState *interfaces.State + netMap *netmap.NetworkMap + nodeByAddr map[netaddr.IP]*tailcfg.Node + activeLogin string // last logged LoginName from netMap + engineStatus ipn.EngineStatus + endpoints []string + blocked bool + authURL string + interact bool + prevIfState *interfaces.State + peerAPIListeners []*peerAPIListener // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -144,6 +145,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge b.statusChanged = sync.NewCond(&b.statusLock) linkMon := e.GetLinkMonitor() + b.prevIfState = linkMon.InterfaceState() // Call our linkChange code once with the current state, and // then also whenever it changes: b.linkChange(false, linkMon.InterfaceState()) @@ -218,52 +220,82 @@ func (b *LocalBackend) Status() *ipnstate.Status { return sb.Status() } +// StatusWithoutPeers is like Status but omits any details +// of peers. +func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status { + sb := new(ipnstate.StatusBuilder) + b.updateStatus(sb, nil) + return sb.Status() +} + // UpdateStatus implements ipnstate.StatusUpdater. func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) { b.e.UpdateStatus(sb) + b.updateStatus(sb, b.populatePeerStatusLocked) +} +// updateStatus populates sb with status. +// +// extraLocked, if non-nil, is called while b.mu is still held. +func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) { b.mu.Lock() defer b.mu.Unlock() - - sb.SetBackendState(b.state.String()) - sb.SetAuthURL(b.authURL) - + sb.MutateStatus(func(s *ipnstate.Status) { + s.Version = version.Long + s.BackendState = b.state.String() + s.AuthURL = b.authURL + if b.netMap != nil { + s.MagicDNSSuffix = b.netMap.MagicDNSSuffix() + } + }) + sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) { + for _, pln := range b.peerAPIListeners { + ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr) + } + }) // TODO: hostinfo, and its networkinfo // TODO: EngineStatus copy (and deprecate it?) - if b.netMap != nil { - sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix()) - for id, up := range b.netMap.UserProfiles { - sb.AddUser(id, up) + + if extraLocked != nil { + extraLocked(sb) + } +} + +func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { + if b.netMap == nil { + return + } + for id, up := range b.netMap.UserProfiles { + sb.AddUser(id, up) + } + for _, p := range b.netMap.Peers { + var lastSeen time.Time + if p.LastSeen != nil { + lastSeen = *p.LastSeen } - for _, p := range b.netMap.Peers { - var lastSeen time.Time - if p.LastSeen != nil { - lastSeen = *p.LastSeen + var tailAddr string + for _, addr := range p.Addresses { + // The peer struct currently only allows a single + // Tailscale IP address. For compatibility with the + // old display, make sure it's the IPv4 address. + if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) { + tailAddr = addr.IP.String() + break } - var tailAddr string - for _, addr := range p.Addresses { - // The peer struct currently only allows a single - // Tailscale IP address. For compatibility with the - // old display, make sure it's the IPv4 address. - if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) { - tailAddr = addr.IP.String() - break - } - } - sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{ - InNetworkMap: true, - UserID: p.User, - TailAddr: tailAddr, - HostName: p.Hostinfo.Hostname, - DNSName: p.Name, - OS: p.Hostinfo.OS, - KeepAlive: p.KeepAlive, - Created: p.Created, - LastSeen: lastSeen, - ShareeNode: p.Hostinfo.ShareeNode, - ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID, - }) } + sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{ + InNetworkMap: true, + UserID: p.User, + TailAddr: tailAddr, + HostName: p.Hostinfo.Hostname, + DNSName: p.Name, + OS: p.Hostinfo.OS, + KeepAlive: p.KeepAlive, + Created: p.Created, + LastSeen: lastSeen, + ShareeNode: p.Hostinfo.ShareeNode, + ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID, + }) } } @@ -697,10 +729,16 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{ netaddr.MustParseIPPrefix("192.168.0.0/16"), netaddr.MustParseIPPrefix("172.16.0.0/12"), netaddr.MustParseIPPrefix("10.0.0.0/8"), + // IPv4 link-local + netaddr.MustParseIPPrefix("169.254.0.0/16"), + // IPv4 multicast + netaddr.MustParseIPPrefix("224.0.0.0/4"), // Tailscale IPv4 range tsaddr.CGNATRange(), // IPv6 Link-local addresses netaddr.MustParseIPPrefix("fe80::/10"), + // IPv6 multicast + netaddr.MustParseIPPrefix("ff00::/8"), // Tailscale IPv6 range tsaddr.TailscaleULARange(), } @@ -710,6 +748,7 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { var b netaddr.IPSetBuilder b.AddPrefix(route) + var hostIPs []netaddr.IP err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) { if tsaddr.IsTailscaleIP(pfx.IP) { return @@ -717,11 +756,25 @@ func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) { if pfx.IsSingleIP() { return } + hostIPs = append(hostIPs, pfx.IP) b.RemovePrefix(pfx) }) if err != nil { return nil, err } + + // Having removed all the LAN subnets, re-add the hosts's own + // IPs. It's fine for clients to connect to an exit node's public + // IP address, just not the attached subnet. + // + // Truly forbidden subnets (in removeFromDefaultRoute) will still + // be stripped back out by the next step. + for _, ip := range hostIPs { + if route.Contains(ip) { + b.Add(ip) + } + } + for _, pfx := range removeFromDefaultRoute { b.RemovePrefix(pfx) } @@ -796,8 +849,8 @@ func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) { } set(netMap.Name, netMap.Addresses) - dnsMap := tsdns.NewMap(nameToIP, magicDNSRootDomains(netMap)) - // map diff will be logged in tsdns.Resolver.SetMap. + dnsMap := dns.NewMap(nameToIP, magicDNSRootDomains(netMap)) + // map diff will be logged in dns.Resolver.SetMap. b.e.SetDNSMap(dnsMap) } @@ -1078,7 +1131,7 @@ func (b *LocalBackend) getEngineStatus() ipn.EngineStatus { } // Login implements Backend. -func (b *LocalBackend) Login(token *oauth2.Token) { +func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) { b.mu.Lock() b.assertClientLocked() c := b.c @@ -1129,13 +1182,13 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) { b.send(ipn.Notify{NetMap: b.netMap}) } -func (b *LocalBackend) Ping(ipStr string) { +func (b *LocalBackend) Ping(ipStr string, useTSMP bool) { ip, err := netaddr.ParseIP(ipStr) if err != nil { b.logf("ignoring Ping request to invalid IP %q", ipStr) return } - b.e.Ping(ip, func(pr *ipnstate.PingResult) { + b.e.Ping(ip, useTSMP, func(pr *ipnstate.PingResult) { b.send(ipn.Notify{PingResult: pr}) }) } @@ -1382,6 +1435,45 @@ func (b *LocalBackend) authReconfig() { return } b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err) + + b.initPeerAPIListener() +} + +func (b *LocalBackend) initPeerAPIListener() { + b.mu.Lock() + defer b.mu.Unlock() + + for _, pln := range b.peerAPIListeners { + pln.ln.Close() + } + b.peerAPIListeners = nil + + if len(b.netMap.Addresses) == 0 || b.netMap.SelfNode == nil { + return + } + + var tunName string + if ge, ok := b.e.(wgengine.InternalsGetter); ok { + tunDev, _ := ge.GetInternals() + tunName, _ = tunDev.Name() + } + + for _, a := range b.netMap.Addresses { + ln, err := peerAPIListen(a.IP, b.prevIfState, tunName) + if err != nil { + b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err) + continue + } + pln := &peerAPIListener{ + ln: ln, + lb: b, + selfNode: b.netMap.SelfNode, + } + pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port())) + + go pln.serve() + b.peerAPIListeners = append(b.peerAPIListeners, pln) + } } // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. @@ -1617,12 +1709,6 @@ func (b *LocalBackend) RequestEngineStatus() { b.e.RequestStatus() } -// RequestStatus implements Backend. -func (b *LocalBackend) RequestStatus() { - st := b.Status() - b.send(ipn.Notify{Status: st}) -} - // stateMachine updates the state machine state based on other things // that have happened. It is invoked from the various callbacks that // feed events into LocalBackend. diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 667cc2287..2a6fc9315 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -9,6 +9,7 @@ import ( "testing" "inet.af/netaddr" + "tailscale.com/net/interfaces" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/netmap" @@ -122,11 +123,21 @@ func TestNetworkMapCompare(t *testing.T) { } } +func inRemove(ip netaddr.IP) bool { + for _, pfx := range removeFromDefaultRoute { + if pfx.Contains(ip) { + return true + } + } + return false +} + func TestShrinkDefaultRoute(t *testing.T) { tests := []struct { - route string - in []string - out []string + route string + in []string + out []string + localIPFn func(netaddr.IP) bool // true if this machine's local IP address should be "in" after shrinking. }{ { route: "0.0.0.0/0", @@ -139,19 +150,24 @@ func TestShrinkDefaultRoute(t *testing.T) { "172.16.0.1", "172.31.255.255", "100.101.102.103", + "224.0.0.1", + "169.254.169.254", // Some random IPv6 stuff that shouldn't be in a v4 // default route. "fe80::", "2601::1", }, + localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is4() }, }, { route: "::/0", in: []string{"::1", "2601::1"}, out: []string{ "fe80::1", + "ff00::1", tsaddr.TailscaleULARange().IP.String(), }, + localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() }, }, } @@ -171,6 +187,16 @@ func TestShrinkDefaultRoute(t *testing.T) { t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip) } } + ips, _, err := interfaces.LocalAddresses() + if err != nil { + t.Fatal(err) + } + for _, ip := range ips { + want := test.localIPFn(ip) + if gotContains := got.Contains(ip); gotContains != want { + t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want) + } + } } } diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go new file mode 100644 index 000000000..390b45efc --- /dev/null +++ b/ipn/ipnlocal/peerapi.go @@ -0,0 +1,167 @@ +// 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 ipnlocal + +import ( + "context" + "errors" + "fmt" + "hash/crc32" + "html" + "io" + "net" + "net/http" + "runtime" + "strconv" + + "inet.af/netaddr" + "tailscale.com/net/interfaces" + "tailscale.com/tailcfg" +) + +var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error + +func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (ln net.Listener, err error) { + ipStr := ip.String() + + var lc net.ListenConfig + if initListenConfig != nil { + // On iOS/macOS, this sets the lc.Control hook to + // setsockopt the interface index to bind to, to get + // out of the network sandbox. + if err := initListenConfig(&lc, ip, ifState, tunIfName); err != nil { + return nil, err + } + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + ipStr = "" + } + } + + tcp4or6 := "tcp4" + if ip.Is6() { + tcp4or6 = "tcp6" + } + + // Make a best effort to pick a deterministic port number for + // the ip The lower three bytes are the same for IPv4 and IPv6 + // Tailscale addresses (at least currently), so we'll usually + // get the same port number on both address families for + // dev/debugging purposes, which is nice. But it's not so + // deterministic that people will bake this into clients. + // We try a few times just in case something's already + // listening on that port (on all interfaces, probably). + for try := uint8(0); try < 5; try++ { + a16 := ip.As16() + hashData := a16[len(a16)-3:] + hashData[0] += try + tryPort := (32 << 10) | uint16(crc32.ChecksumIEEE(hashData)) + ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, strconv.Itoa(int(tryPort)))) + if err == nil { + return ln, nil + } + } + // Fall back to random ephemeral port. + return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0")) +} + +type peerAPIListener struct { + ln net.Listener + lb *LocalBackend + urlStr string + selfNode *tailcfg.Node +} + +func (pln *peerAPIListener) Port() int { + ta, ok := pln.ln.Addr().(*net.TCPAddr) + if !ok { + return 0 + } + return ta.Port +} + +func (pln *peerAPIListener) serve() { + defer pln.ln.Close() + logf := pln.lb.logf + for { + c, err := pln.ln.Accept() + if errors.Is(err, net.ErrClosed) { + return + } + if err != nil { + logf("peerapi.Accept: %v", err) + return + } + ta, ok := c.RemoteAddr().(*net.TCPAddr) + if !ok { + c.Close() + logf("peerapi: unexpected RemoteAddr %#v", c.RemoteAddr()) + continue + } + ipp, ok := netaddr.FromStdAddr(ta.IP, ta.Port, "") + if !ok { + logf("peerapi: bogus TCPAddr %#v", ta) + c.Close() + continue + } + peerNode, peerUser, ok := pln.lb.WhoIs(ipp) + if !ok { + logf("peerapi: unknown peer %v", ipp) + c.Close() + continue + } + h := &peerAPIHandler{ + isSelf: pln.selfNode.User == peerNode.User, + remoteAddr: ipp, + peerNode: peerNode, + peerUser: peerUser, + lb: pln.lb, + } + httpServer := &http.Server{ + Handler: h, + } + go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c}) + } +} + +type oneConnListener struct { + net.Listener + conn net.Conn +} + +func (l *oneConnListener) Accept() (c net.Conn, err error) { + c = l.conn + if c == nil { + err = io.EOF + return + } + err = nil + l.conn = nil + return +} + +func (l *oneConnListener) Close() error { return nil } + +// peerAPIHandler serves the Peer API for a source specific client. +type peerAPIHandler struct { + remoteAddr netaddr.IPPort + isSelf bool // whether peerNode is owned by same user as this node + peerNode *tailcfg.Node // peerNode is who's making the request + peerUser tailcfg.UserProfile // profile of peerNode + lb *LocalBackend +} + +func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + who := h.peerUser.DisplayName + fmt.Fprintf(w, ` + + +

Hello, %s (%v)

+This is my Tailscale device. Your device is %v. +`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName)) + + if h.isSelf { + fmt.Fprintf(w, "

You are the owner of this node.\n") + } +} diff --git a/ipn/ipnlocal/peerapi_macios_ext.go b/ipn/ipnlocal/peerapi_macios_ext.go new file mode 100644 index 000000000..a75e18eed --- /dev/null +++ b/ipn/ipnlocal/peerapi_macios_ext.go @@ -0,0 +1,54 @@ +// 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 ios,redo + +package ipnlocal + +import ( + "fmt" + "log" + "net" + "strings" + "syscall" + + "golang.org/x/sys/unix" + "inet.af/netaddr" + "tailscale.com/net/interfaces" +) + +func init() { + initListenConfig = initListenConfigNetworkExtension +} + +// initListenConfigNetworkExtension configures nc for listening on IP +// through the iOS/macOS Network/System Extension (Packet Tunnel +// Provider) sandbox. +func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *interfaces.State, tunIfName string) error { + tunIf, ok := st.Interface[tunIfName] + if !ok { + return fmt.Errorf("no interface with name %q", tunIfName) + } + nc.Control = func(network, address string, c syscall.RawConn) error { + var sockErr error + err := c.Control(func(fd uintptr) { + + 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 + } + + sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index) + log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr) + }) + if err != nil { + return err + } + return sockErr + } + return nil +} diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index b89f7bb08..de9d208f7 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -26,7 +26,14 @@ import ( // Status represents the entire state of the IPN network. type Status struct { + // Version is the daemon's long version (see version.Long). + Version string + + // BackendState is an ipn.State string value: + // "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped", + // "Starting", "Running". BackendState string + AuthURL string // current URL provided by control to authorize client TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node Self *PeerStatus @@ -80,6 +87,8 @@ type PeerStatus struct { KeepAlive bool ExitNode bool // true if this is the currently selected exit node. + PeerAPIURL []string + // ShareeNode indicates this node exists in the netmap because // it's owned by a shared-to user and that node might connect // to us. These nodes should be hidden by "tailscale status" @@ -105,22 +114,16 @@ type StatusBuilder struct { st Status } -func (sb *StatusBuilder) SetBackendState(v string) { +// MutateStatus calls f with the status to mutate. +// +// It may not assume other fields of status are already populated, and +// may not retain or write to the Status after f returns. +// +// MutateStatus acquires a lock so f must not call back into sb. +func (sb *StatusBuilder) MutateStatus(f func(*Status)) { sb.mu.Lock() defer sb.mu.Unlock() - sb.st.BackendState = v -} - -func (sb *StatusBuilder) SetAuthURL(v string) { - sb.mu.Lock() - defer sb.mu.Unlock() - sb.st.AuthURL = v -} - -func (sb *StatusBuilder) SetMagicDNSSuffix(v string) { - sb.mu.Lock() - defer sb.mu.Unlock() - sb.st.MagicDNSSuffix = v + f(&sb.st) } func (sb *StatusBuilder) Status() *Status { @@ -130,11 +133,19 @@ func (sb *StatusBuilder) Status() *Status { return &sb.st } -// SetSelfStatus sets the status of the local machine. -func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) { +// MutateSelfStatus calls f with the PeerStatus of our own node to mutate. +// +// It may not assume other fields of status are already populated, and +// may not retain or write to the Status after f returns. +// +// MutateStatus acquires a lock so f must not call back into sb. +func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) { sb.mu.Lock() defer sb.mu.Unlock() - sb.st.Self = ss + if sb.st.Self == nil { + sb.st.Self = new(PeerStatus) + } + f(sb.st.Self) } // AddUser adds a user profile to the status. @@ -394,10 +405,18 @@ type PingResult struct { Err string LatencySeconds float64 - Endpoint string // ip:port if direct UDP was used + // Endpoint is the ip:port if direct UDP was used. + // It is not currently set for TSMP pings. + Endpoint string - DERPRegionID int // non-zero if DERP was used - DERPRegionCode string // three-letter airport/region code if DERP was used + // DERPRegionID is non-zero DERP region ID if DERP was used. + // It is not currently set for TSMP pings. + DERPRegionID int + + // DERPRegionCode is the three-letter region code + // corresponding to DERPRegionID. + // It is not currently set for TSMP pings. + DERPRegionCode string // TODO(bradfitz): details like whether port mapping was used on either side? (Once supported) } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 4e0dba3da..169f18ba4 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -10,9 +10,11 @@ import ( "io" "net/http" "runtime" + "strconv" "inet.af/netaddr" "tailscale.com/ipn/ipnlocal" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" ) @@ -56,6 +58,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveWhoIs(w, r) case "/localapi/v0/goroutines": h.serveGoroutines(w, r) + case "/localapi/v0/status": + h.serveStatus(w, r) default: io.WriteString(w, "tailscaled\n") } @@ -109,3 +113,31 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write(buf) } + +func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) { + if !h.PermitRead { + http.Error(w, "status access denied", http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + var st *ipnstate.Status + if defBool(r.FormValue("peers"), true) { + st = h.b.Status() + } else { + st = h.b.StatusWithoutPeers() + } + e := json.NewEncoder(w) + e.SetIndent("", "\t") + e.Encode(st) +} + +func defBool(a string, def bool) bool { + if a == "" { + return def + } + v, err := strconv.ParseBool(a) + if err != nil { + return def + } + return v +} diff --git a/ipn/message.go b/ipn/message.go index 9b7783f98..905664c93 100644 --- a/ipn/message.go +++ b/ipn/message.go @@ -15,7 +15,7 @@ import ( "log" "time" - "golang.org/x/oauth2" + "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/structs" "tailscale.com/version" @@ -56,7 +56,8 @@ type FakeExpireAfterArgs struct { } type PingArgs struct { - IP string + IP string + UseTSMP bool } // Command is a command message that is JSON encoded and sent by a @@ -76,7 +77,7 @@ type Command struct { Quit *NoArgs Start *StartArgs StartLoginInteractive *NoArgs - Login *oauth2.Token + Login *tailcfg.Oauth2Token Logout *NoArgs SetPrefs *SetPrefsArgs SetWantRunning *bool @@ -173,11 +174,8 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error { if c := cmd.RequestEngineStatus; c != nil { bs.b.RequestEngineStatus() return nil - } else if c := cmd.RequestStatus; c != nil { - bs.b.RequestStatus() - return nil } else if c := cmd.Ping; c != nil { - bs.b.Ping(c.IP) + bs.b.Ping(c.IP, c.UseTSMP) return nil } @@ -299,7 +297,7 @@ func (bc *BackendClient) StartLoginInteractive() { bc.send(Command{StartLoginInteractive: &NoArgs{}}) } -func (bc *BackendClient) Login(token *oauth2.Token) { +func (bc *BackendClient) Login(token *tailcfg.Oauth2Token) { bc.send(Command{Login: token}) } @@ -323,8 +321,11 @@ func (bc *BackendClient) FakeExpireAfter(x time.Duration) { bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}}) } -func (bc *BackendClient) Ping(ip string) { - bc.send(Command{Ping: &PingArgs{IP: ip}}) +func (bc *BackendClient) Ping(ip string, useTSMP bool) { + bc.send(Command{Ping: &PingArgs{ + IP: ip, + UseTSMP: useTSMP, + }}) } func (bc *BackendClient) SetWantRunning(v bool) { diff --git a/ipn/message_test.go b/ipn/message_test.go index 4422a64ca..59a9eef25 100644 --- a/ipn/message_test.go +++ b/ipn/message_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "golang.org/x/oauth2" + "tailscale.com/tailcfg" "tailscale.com/tstest" ) @@ -176,7 +176,7 @@ func TestClientServer(t *testing.T) { h.Logout() flushUntil(NeedsLogin) - h.Login(&oauth2.Token{ + h.Login(&tailcfg.Oauth2Token{ AccessToken: "google_id_token", TokenType: GoogleIDTokenType, }) diff --git a/wgengine/router/dns/config.go b/net/dns/config.go similarity index 98% rename from wgengine/router/dns/config.go rename to net/dns/config.go index 2b6ff615a..db08df7aa 100644 --- a/wgengine/router/dns/config.go +++ b/net/dns/config.go @@ -22,8 +22,8 @@ type Config struct { // Note that Nameservers may still be applied to all queries // if the manager does not support per-domain settings. PerDomain bool - // Proxied indicates whether DNS requests are proxied through a tsdns.Resolver. - // This enables Magic DNS. + // Proxied indicates whether DNS requests are proxied through a dns.Resolver. + // This enables MagicDNS. Proxied bool } diff --git a/wgengine/router/dns/direct.go b/net/dns/direct.go similarity index 100% rename from wgengine/router/dns/direct.go rename to net/dns/direct.go diff --git a/net/dns/flush_windows.go b/net/dns/flush_windows.go new file mode 100644 index 000000000..3c7e7d645 --- /dev/null +++ b/net/dns/flush_windows.go @@ -0,0 +1,19 @@ +// 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 dns + +import ( + "fmt" + "os/exec" +) + +// Flush clears the local resolver cache. +func Flush() error { + out, err := exec.Command("ipconfig", "/flushdns").CombinedOutput() + if err != nil { + return fmt.Errorf("%v (output: %s)", err, out) + } + return nil +} diff --git a/wgengine/tsdns/forwarder.go b/net/dns/forwarder.go similarity index 99% rename from wgengine/tsdns/forwarder.go rename to net/dns/forwarder.go index 470748c4a..519c00027 100644 --- a/wgengine/tsdns/forwarder.go +++ b/net/dns/forwarder.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 tsdns +package dns import ( "bytes" @@ -316,7 +316,7 @@ func (c *fwdConn) send(packet []byte, dst net.Addr) { var b *backoff.Backoff // lazily initialized, since it is not needed in the common case backOff := func(err error) { if b == nil { - b = backoff.NewBackoff("tsdns-fwdConn-send", c.logf, 30*time.Second) + b = backoff.NewBackoff("dns-fwdConn-send", c.logf, 30*time.Second) } b.BackOff(context.Background(), err) } diff --git a/wgengine/router/dns/manager.go b/net/dns/manager.go similarity index 100% rename from wgengine/router/dns/manager.go rename to net/dns/manager.go diff --git a/wgengine/router/dns/manager_default.go b/net/dns/manager_default.go similarity index 100% rename from wgengine/router/dns/manager_default.go rename to net/dns/manager_default.go diff --git a/wgengine/router/dns/manager_freebsd.go b/net/dns/manager_freebsd.go similarity index 100% rename from wgengine/router/dns/manager_freebsd.go rename to net/dns/manager_freebsd.go diff --git a/wgengine/router/dns/manager_linux.go b/net/dns/manager_linux.go similarity index 100% rename from wgengine/router/dns/manager_linux.go rename to net/dns/manager_linux.go diff --git a/wgengine/router/dns/manager_openbsd.go b/net/dns/manager_openbsd.go similarity index 100% rename from wgengine/router/dns/manager_openbsd.go rename to net/dns/manager_openbsd.go diff --git a/wgengine/router/dns/manager_windows.go b/net/dns/manager_windows.go similarity index 100% rename from wgengine/router/dns/manager_windows.go rename to net/dns/manager_windows.go diff --git a/wgengine/tsdns/map.go b/net/dns/map.go similarity index 99% rename from wgengine/tsdns/map.go rename to net/dns/map.go index c51dbf59b..119b6cc0a 100644 --- a/wgengine/tsdns/map.go +++ b/net/dns/map.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 tsdns +package dns import ( "sort" diff --git a/wgengine/tsdns/map_test.go b/net/dns/map_test.go similarity index 99% rename from wgengine/tsdns/map_test.go rename to net/dns/map_test.go index dba9bb586..c438f95a0 100644 --- a/wgengine/tsdns/map_test.go +++ b/net/dns/map_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 tsdns +package dns import ( "fmt" diff --git a/wgengine/tsdns/neterr_darwin.go b/net/dns/neterr_darwin.go similarity index 97% rename from wgengine/tsdns/neterr_darwin.go rename to net/dns/neterr_darwin.go index 62bab6488..7fd621fc7 100644 --- a/wgengine/tsdns/neterr_darwin.go +++ b/net/dns/neterr_darwin.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 tsdns +package dns import ( "errors" diff --git a/wgengine/tsdns/neterr_other.go b/net/dns/neterr_other.go similarity index 95% rename from wgengine/tsdns/neterr_other.go rename to net/dns/neterr_other.go index d245d0c38..b652f6e8b 100644 --- a/wgengine/tsdns/neterr_other.go +++ b/net/dns/neterr_other.go @@ -4,7 +4,7 @@ // +build !darwin,!windows -package tsdns +package dns func networkIsDown(err error) bool { return false } func networkIsUnreachable(err error) bool { return false } diff --git a/wgengine/tsdns/neterr_windows.go b/net/dns/neterr_windows.go similarity index 97% rename from wgengine/tsdns/neterr_windows.go rename to net/dns/neterr_windows.go index 90f0db2ab..2b197ee2b 100644 --- a/wgengine/tsdns/neterr_windows.go +++ b/net/dns/neterr_windows.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 tsdns +package dns import ( "net" diff --git a/wgengine/router/dns/nm.go b/net/dns/nm.go similarity index 100% rename from wgengine/router/dns/nm.go rename to net/dns/nm.go diff --git a/wgengine/router/dns/noop.go b/net/dns/noop.go similarity index 100% rename from wgengine/router/dns/noop.go rename to net/dns/noop.go diff --git a/wgengine/router/dns/registry_windows.go b/net/dns/registry_windows.go similarity index 100% rename from wgengine/router/dns/registry_windows.go rename to net/dns/registry_windows.go diff --git a/wgengine/router/dns/resolvconf.go b/net/dns/resolvconf.go similarity index 100% rename from wgengine/router/dns/resolvconf.go rename to net/dns/resolvconf.go diff --git a/wgengine/router/dns/resolved.go b/net/dns/resolved.go similarity index 100% rename from wgengine/router/dns/resolved.go rename to net/dns/resolved.go diff --git a/wgengine/tsdns/tsdns.go b/net/dns/tsdns.go similarity index 99% rename from wgengine/tsdns/tsdns.go rename to net/dns/tsdns.go index b68b8c04e..2b530b81e 100644 --- a/wgengine/tsdns/tsdns.go +++ b/net/dns/tsdns.go @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package tsdns provides a Resolver capable of resolving +// Package dns provides a Resolver capable of resolving // domains on a Tailscale network. -package tsdns +package dns import ( "encoding/hex" @@ -100,7 +100,7 @@ type ResolverConfig struct { // The root domain must be in canonical form (with a trailing period). func NewResolver(config ResolverConfig) *Resolver { r := &Resolver{ - logf: logger.WithPrefix(config.Logf, "tsdns: "), + logf: logger.WithPrefix(config.Logf, "dns: "), linkMon: config.LinkMonitor, queue: make(chan Packet, queueSize), responses: make(chan Packet), diff --git a/wgengine/tsdns/tsdns_server_test.go b/net/dns/tsdns_server_test.go similarity index 99% rename from wgengine/tsdns/tsdns_server_test.go rename to net/dns/tsdns_server_test.go index df9047fc6..95544ba18 100644 --- a/wgengine/tsdns/tsdns_server_test.go +++ b/net/dns/tsdns_server_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 tsdns +package dns import ( "log" diff --git a/wgengine/tsdns/tsdns_test.go b/net/dns/tsdns_test.go similarity index 99% rename from wgengine/tsdns/tsdns_test.go rename to net/dns/tsdns_test.go index 66a62d107..59bcd8ec1 100644 --- a/wgengine/tsdns/tsdns_test.go +++ b/net/dns/tsdns_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 tsdns +package dns import ( "bytes" diff --git a/net/flowtrack/flowtrack.go b/net/flowtrack/flowtrack.go index 5fcd4ab20..8387145d3 100644 --- a/net/flowtrack/flowtrack.go +++ b/net/flowtrack/flowtrack.go @@ -15,16 +15,18 @@ import ( "fmt" "inet.af/netaddr" + "tailscale.com/types/ipproto" ) -// Tuple is a 4-tuple of source and destination IP and port. +// Tuple is a 5-tuple of proto, source and destination IP and port. type Tuple struct { - Src netaddr.IPPort - Dst netaddr.IPPort + Proto ipproto.Proto + Src netaddr.IPPort + Dst netaddr.IPPort } func (t Tuple) String() string { - return fmt.Sprintf("(%v => %v)", t.Src, t.Dst) + return fmt.Sprintf("(%v %v => %v)", t.Proto, t.Src, t.Dst) } // Cache is an LRU cache keyed by Tuple. diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 3a0ffeb0b..f2988af34 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -6,10 +6,10 @@ package interfaces import ( + "bytes" "fmt" "net" "net/http" - "reflect" "runtime" "sort" "strings" @@ -190,6 +190,9 @@ func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error { } } } + sort.Slice(pfxs, func(i, j int) bool { + return pfxs[i].IP.Less(pfxs[j].IP) + }) fn(Interface{iface}, pfxs) } return nil @@ -204,7 +207,7 @@ type State struct { // IPPrefix, where the IP is the interface IP address and Bits is // the subnet mask. InterfaceIPs map[string][]netaddr.IPPrefix - InterfaceUp map[string]bool + Interface map[string]Interface // HaveV6Global is whether this machine has an IPv6 global address // on some non-Tailscale interface that's up. @@ -235,14 +238,14 @@ type State struct { func (s *State) String() string { var sb strings.Builder fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface) - ifs := make([]string, 0, len(s.InterfaceUp)) - for k := range s.InterfaceUp { + ifs := make([]string, 0, len(s.Interface)) + for k := range s.Interface { if anyInterestingIP(s.InterfaceIPs[k]) { ifs = append(ifs, k) } } sort.Slice(ifs, func(i, j int) bool { - upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]] + upi, upj := s.Interface[ifs[i]].IsUp(), s.Interface[ifs[j]].IsUp() if upi != upj { // Up sorts before down. return upi @@ -253,7 +256,7 @@ func (s *State) String() string { if i > 0 { sb.WriteString(" ") } - if s.InterfaceUp[ifName] { + if s.Interface[ifName].IsUp() { fmt.Fprintf(&sb, "%s:[", ifName) needSpace := false for _, pfx := range s.InterfaceIPs[ifName] { @@ -286,10 +289,71 @@ func (s *State) String() string { return sb.String() } -func (s *State) Equal(s2 *State) bool { - return reflect.DeepEqual(s, s2) +// EqualFiltered reports whether s and s2 are equal, +// considering only interfaces in s for which filter returns true. +func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.IPPrefix) bool) bool { + if s == nil && s2 == nil { + return true + } + if s == nil || s2 == nil { + return false + } + if s.HaveV6Global != s2.HaveV6Global || + s.HaveV4 != s2.HaveV4 || + s.IsExpensive != s2.IsExpensive || + s.DefaultRouteInterface != s2.DefaultRouteInterface || + s.HTTPProxy != s2.HTTPProxy || + s.PAC != s2.PAC { + return false + } + for iname, i := range s.Interface { + ips := s.InterfaceIPs[iname] + if !filter(i, ips) { + continue + } + i2, ok := s2.Interface[iname] + if !ok { + return false + } + ips2, ok := s2.InterfaceIPs[iname] + if !ok { + return false + } + if !interfacesEqual(i, i2) || !prefixesEqual(ips, ips2) { + return false + } + } + return true } +func interfacesEqual(a, b Interface) bool { + return a.Index == b.Index && + a.MTU == b.MTU && + a.Name == b.Name && + a.Flags == b.Flags && + bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr)) +} + +func prefixesEqual(a, b []netaddr.IPPrefix) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if b[i] != v { + return false + } + } + return true +} + +// FilterInteresting reports whether i is an interesting non-Tailscale interface. +func FilterInteresting(i Interface, ips []netaddr.IPPrefix) bool { + return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips) +} + +// FilterAll always returns true, to use EqualFiltered against all interfaces. +func FilterAll(i Interface, ips []netaddr.IPPrefix) bool { return true } + func (s *State) HasPAC() bool { return s != nil && s.PAC != "" } // AnyInterfaceUp reports whether any interface seems like it has Internet access. @@ -297,41 +361,6 @@ func (s *State) AnyInterfaceUp() bool { return s != nil && (s.HaveV4 || s.HaveV6Global) } -// RemoveUninterestingInterfacesAndAddresses removes uninteresting IPs -// from InterfaceIPs, also removing from both the InterfaceIPs and -// InterfaceUp map any interfaces that don't have any interesting IPs. -func (s *State) RemoveUninterestingInterfacesAndAddresses() { - for ifName := range s.InterfaceUp { - ips := s.InterfaceIPs[ifName] - keep := ips[:0] - for _, pfx := range ips { - if isInterestingIP(pfx.IP) { - keep = append(keep, pfx) - } - } - if len(keep) == 0 { - delete(s.InterfaceUp, ifName) - delete(s.InterfaceIPs, ifName) - continue - } - if len(keep) < len(ips) { - s.InterfaceIPs[ifName] = keep - } - } -} - -// RemoveTailscaleInterfaces modifes s to remove any interfaces that -// are owned by this process. (TODO: make this true; currently it -// uses some heuristics) -func (s *State) RemoveTailscaleInterfaces() { - for name, pfxs := range s.InterfaceIPs { - if isTailscaleInterface(name, pfxs) { - delete(s.InterfaceIPs, name) - delete(s.InterfaceUp, name) - } - } -} - func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool { for _, pfx := range pfxs { if tsaddr.IsTailscaleIP(pfx.IP) { @@ -364,11 +393,11 @@ var getPAC func() string func GetState() (*State, error) { s := &State{ InterfaceIPs: make(map[string][]netaddr.IPPrefix), - InterfaceUp: make(map[string]bool), + Interface: make(map[string]Interface), } if err := ForeachInterface(func(ni Interface, pfxs []netaddr.IPPrefix) { ifUp := ni.IsUp() - s.InterfaceUp[ni.Name] = ifUp + s.Interface[ni.Name] = ni s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...) if !ifUp || isTailscaleInterface(ni.Name, pfxs) { return diff --git a/net/interfaces/interfaces_test.go b/net/interfaces/interfaces_test.go index 88948b579..ab0ee3734 100644 --- a/net/interfaces/interfaces_test.go +++ b/net/interfaces/interfaces_test.go @@ -5,6 +5,7 @@ package interfaces import ( + "encoding/json" "testing" ) @@ -13,7 +14,11 @@ func TestGetState(t *testing.T) { if err != nil { t.Fatal(err) } - t.Logf("Got: %#v", st) + j, err := json.MarshalIndent(st, "", "\t") + if err != nil { + t.Errorf("JSON: %v", err) + } + t.Logf("Got: %s", j) t.Logf("As string: %s", st) st2, err := GetState() @@ -21,14 +26,13 @@ func TestGetState(t *testing.T) { t.Fatal(err) } - if !st.Equal(st2) { + if !st.EqualFiltered(st2, FilterAll) { // let's assume nobody was changing the system network interfaces between // the two GetState calls. t.Fatal("two States back-to-back were not equal") } - st.RemoveTailscaleInterfaces() - t.Logf("As string without Tailscale:\n\t%s", st) + t.Logf("As string:\n\t%s", st) } func TestLikelyHomeRouterIP(t *testing.T) { diff --git a/net/interfaces/interfaces_windows.go b/net/interfaces/interfaces_windows.go index 19e9b48b4..91f679c97 100644 --- a/net/interfaces/interfaces_windows.go +++ b/net/interfaces/interfaces_windows.go @@ -7,17 +7,19 @@ package interfaces import ( "fmt" "log" + "net" "net/url" - "os/exec" "syscall" "unsafe" - "go4.org/mem" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "inet.af/netaddr" "tailscale.com/tsconst" - "tailscale.com/util/lineread" +) + +const ( + fallbackInterfaceMetric = uint32(0) // Used if we cannot get the actual interface metric ) func init() { @@ -25,58 +27,76 @@ func init() { getPAC = getPACWindows } -/* -Parse out 10.0.0.1 from: - -Z:\>route print -4 -=========================================================================== -Interface List - 15...aa 15 48 ff 1c 72 ......Red Hat VirtIO Ethernet Adapter - 5...........................Tailscale Tunnel - 1...........................Software Loopback Interface 1 -=========================================================================== - -IPv4 Route Table -=========================================================================== -Active Routes: -Network Destination Netmask Gateway Interface Metric - 0.0.0.0 0.0.0.0 10.0.0.1 10.0.28.63 5 - 10.0.0.0 255.255.0.0 On-link 10.0.28.63 261 - 10.0.28.63 255.255.255.255 On-link 10.0.28.63 261 - 10.0.42.0 255.255.255.0 100.103.42.106 100.103.42.106 5 - 10.0.255.255 255.255.255.255 On-link 10.0.28.63 261 - 34.193.248.174 255.255.255.255 100.103.42.106 100.103.42.106 5 - -*/ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) { - cmd := exec.Command("route", "print", "-4") - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - stdout, err := cmd.StdoutPipe() + rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET) if err != nil { + log.Printf("routerIP/GetIPForwardTable2 error: %v", err) return } - if err := cmd.Start(); err != nil { - return - } - defer cmd.Wait() - var f []mem.RO - lineread.Reader(stdout, func(lineb []byte) error { - line := mem.B(lineb) - if !mem.Contains(line, mem.S("0.0.0.0")) { - return nil + var ifaceMetricCache map[winipcfg.LUID]uint32 + + getIfaceMetric := func(luid winipcfg.LUID) (metric uint32) { + if ifaceMetricCache == nil { + ifaceMetricCache = make(map[winipcfg.LUID]uint32) + } else if m, ok := ifaceMetricCache[luid]; ok { + return m } - f = mem.AppendFields(f[:0], line) - if len(f) < 3 || !f[0].EqualString("0.0.0.0") || !f[1].EqualString("0.0.0.0") { - return nil + + if iface, err := luid.IPInterface(windows.AF_INET); err == nil { + metric = iface.Metric + } else { + log.Printf("routerIP/luid.IPInterface error: %v", err) + metric = fallbackInterfaceMetric } - ipm := f[2] - ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm))) - if err == nil && isPrivateIP(ip) { + + ifaceMetricCache[luid] = metric + return + } + + unspec := net.IPv4(0, 0, 0, 0) + var best *winipcfg.MibIPforwardRow2 // best (lowest metric) found so far, or nil + + for i := range rs { + r := &rs[i] + if r.Loopback || r.DestinationPrefix.PrefixLength != 0 || !r.DestinationPrefix.Prefix.IP().Equal(unspec) { + // Not a default route, so skip + continue + } + + ip, ok := netaddr.FromStdIP(r.NextHop.IP()) + if !ok { + // Not a valid gateway, so skip (won't happen though) + continue + } + + if best == nil { + best = r + ret = ip + continue + } + + // We can get here only if there are multiple default gateways defined (rare case), + // in which case we need to calculate the effective metric. + // Effective metric is sum of interface metric and route metric offset + if ifaceMetricCache == nil { + // If we're here it means that previous route still isn't updated, so update it + best.Metric += getIfaceMetric(best.InterfaceLUID) + } + r.Metric += getIfaceMetric(r.InterfaceLUID) + + if best.Metric > r.Metric || best.Metric == r.Metric && ret.Compare(ip) > 0 { + // Pick the route with lower metric, or lower IP if metrics are equal + best = r ret = ip } - return nil - }) + } + + if !ret.IsZero() && !isPrivateIP(ret) { + // Default route has a non-private gateway + return netaddr.IP{}, false + } + return ret, !ret.IsZero() } diff --git a/net/packet/header.go b/net/packet/header.go index 86680a5a7..5cf4ef650 100644 --- a/net/packet/header.go +++ b/net/packet/header.go @@ -10,6 +10,7 @@ import ( ) const tcpHeaderLength = 20 +const sctpHeaderLength = 12 // maxPacketLength is the largest length that all headers support. // IPv4 headers using uint16 for this forces an upper bound of 64KB. diff --git a/net/packet/icmp4.go b/net/packet/icmp4.go index 8a1568114..da4774887 100644 --- a/net/packet/icmp4.go +++ b/net/packet/icmp4.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // icmp4HeaderLength is the size of the ICMPv4 packet header, not // including the outer IP layer or the variable "response data" @@ -66,7 +70,7 @@ func (h ICMP4Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = ICMPv4 + h.IPProto = ipproto.ICMPv4 buf[20] = uint8(h.Type) buf[21] = uint8(h.Code) diff --git a/net/packet/ip4.go b/net/packet/ip4.go index 0240abaa1..2c090d9f1 100644 --- a/net/packet/ip4.go +++ b/net/packet/ip4.go @@ -9,6 +9,7 @@ import ( "errors" "inet.af/netaddr" + "tailscale.com/types/ipproto" ) // ip4HeaderLength is the length of an IPv4 header with no IP options. @@ -16,7 +17,7 @@ const ip4HeaderLength = 20 // IP4Header represents an IPv4 packet header. type IP4Header struct { - IPProto IPProto + IPProto ipproto.Proto IPID uint16 Src netaddr.IP Dst netaddr.IP diff --git a/net/packet/ip6.go b/net/packet/ip6.go index 59f605b32..e181f1dde 100644 --- a/net/packet/ip6.go +++ b/net/packet/ip6.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "inet.af/netaddr" + "tailscale.com/types/ipproto" ) // ip6HeaderLength is the length of an IPv6 header with no IP options. @@ -15,7 +16,7 @@ const ip6HeaderLength = 40 // IP6Header represents an IPv6 packet header. type IP6Header struct { - IPProto IPProto + IPProto ipproto.Proto IPID uint32 // only lower 20 bits used Src netaddr.IP Dst netaddr.IP diff --git a/net/packet/packet.go b/net/packet/packet.go index a88a1af7a..05c4a382f 100644 --- a/net/packet/packet.go +++ b/net/packet/packet.go @@ -11,9 +11,12 @@ import ( "strings" "inet.af/netaddr" + "tailscale.com/types/ipproto" "tailscale.com/types/strbuilder" ) +const unknown = ipproto.Unknown + // RFC1858: prevent overlapping fragment attacks. const minFrag = 60 + 20 // max IPv4 header + basic TCP header @@ -44,7 +47,7 @@ type Parsed struct { // 6), or 0 if the packet doesn't look like IPv4 or IPv6. IPVersion uint8 // IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0. - IPProto IPProto + IPProto ipproto.Proto // SrcIP4 is the source address. Family matches IPVersion. Port is // valid iff IPProto == TCP || IPProto == UDP. Src netaddr.IPPort @@ -100,7 +103,7 @@ func (q *Parsed) Decode(b []byte) { if len(b) < 1 { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } @@ -112,7 +115,7 @@ func (q *Parsed) Decode(b []byte) { q.decode6(b) default: q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown } } @@ -125,16 +128,16 @@ func (q *Parsed) StuffForTesting(len int) { func (q *Parsed) decode4(b []byte) { if len(b) < ip4HeaderLength { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } // Check that it's IPv4. - q.IPProto = IPProto(b[9]) + q.IPProto = ipproto.Proto(b[9]) q.length = int(binary.BigEndian.Uint16(b[2:4])) if len(b) < q.length { // Packet was cut off before full IPv4 length. - q.IPProto = Unknown + q.IPProto = unknown return } @@ -145,7 +148,7 @@ func (q *Parsed) decode4(b []byte) { q.subofs = int((b[0] & 0x0F) << 2) if q.subofs > q.length { // next-proto starts beyond end of packet. - q.IPProto = Unknown + q.IPProto = unknown return } sub := b[q.subofs:] @@ -170,29 +173,29 @@ func (q *Parsed) decode4(b []byte) { // This is the first fragment if moreFrags && len(sub) < minFrag { // Suspiciously short first fragment, dump it. - q.IPProto = Unknown + q.IPProto = unknown return } // otherwise, this is either non-fragmented (the usual case) // or a big enough initial fragment that we can read the // whole subprotocol header. switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: if len(sub) < icmp4HeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = 0 q.Dst.Port = 0 q.dataofs = q.subofs + icmp4HeaderLength return - case IGMP: + case ipproto.IGMP: // Keep IPProto, but don't parse anything else // out. return - case TCP: + case ipproto.TCP: if len(sub) < tcpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) @@ -201,21 +204,29 @@ func (q *Parsed) decode4(b []byte) { headerLength := (sub[12] & 0xF0) >> 2 q.dataofs = q.subofs + int(headerLength) return - case UDP: + case ipproto.UDP: if len(sub) < udpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) q.dataofs = q.subofs + udpHeaderLength return - case TSMP: + case ipproto.SCTP: + if len(sub) < sctpHeaderLength { + q.IPProto = unknown + return + } + q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) + q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) + return + case ipproto.TSMP: // Inter-tailscale messages. q.dataofs = q.subofs return default: - q.IPProto = Unknown + q.IPProto = unknown return } } else { @@ -223,7 +234,7 @@ func (q *Parsed) decode4(b []byte) { if fragOfs < minFrag { // First frag was suspiciously short, so we can't // trust the followup either. - q.IPProto = Unknown + q.IPProto = unknown return } // otherwise, we have to permit the fragment to slide through. @@ -232,7 +243,7 @@ func (q *Parsed) decode4(b []byte) { // but that would require statefulness. Anyway, receivers' // kernels know to drop fragments where the initial fragment // doesn't arrive. - q.IPProto = Fragment + q.IPProto = ipproto.Fragment return } } @@ -240,15 +251,15 @@ func (q *Parsed) decode4(b []byte) { func (q *Parsed) decode6(b []byte) { if len(b) < ip6HeaderLength { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } - q.IPProto = IPProto(b[6]) + q.IPProto = ipproto.Proto(b[6]) q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength if len(b) < q.length { // Packet was cut off before the full IPv6 length. - q.IPProto = Unknown + q.IPProto = unknown return } @@ -274,17 +285,17 @@ func (q *Parsed) decode6(b []byte) { sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination switch q.IPProto { - case ICMPv6: + case ipproto.ICMPv6: if len(sub) < icmp6HeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = 0 q.Dst.Port = 0 q.dataofs = q.subofs + icmp6HeaderLength - case TCP: + case ipproto.TCP: if len(sub) < tcpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) @@ -293,20 +304,28 @@ func (q *Parsed) decode6(b []byte) { headerLength := (sub[12] & 0xF0) >> 2 q.dataofs = q.subofs + int(headerLength) return - case UDP: + case ipproto.UDP: if len(sub) < udpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) q.dataofs = q.subofs + udpHeaderLength - case TSMP: + case ipproto.SCTP: + if len(sub) < sctpHeaderLength { + q.IPProto = unknown + return + } + q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) + q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) + return + case ipproto.TSMP: // Inter-tailscale messages. q.dataofs = q.subofs return default: - q.IPProto = Unknown + q.IPProto = unknown return } } @@ -324,6 +343,19 @@ func (q *Parsed) IP4Header() IP4Header { } } +func (q *Parsed) IP6Header() IP6Header { + if q.IPVersion != 6 { + panic("IP6Header called on non-IPv6 Parsed") + } + ipid := (binary.BigEndian.Uint32(q.b[:4]) << 12) >> 12 + return IP6Header{ + IPID: ipid, + IPProto: q.IPProto, + Src: q.Src.IP, + Dst: q.Dst.IP, + } +} + func (q *Parsed) ICMP4Header() ICMP4Header { if q.IPVersion != 4 { panic("IP4Header called on non-IPv4 Parsed") @@ -367,13 +399,13 @@ func (q *Parsed) IsTCPSyn() bool { // IsError reports whether q is an ICMP "Error" packet. func (q *Parsed) IsError() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: if len(q.b) < q.subofs+8 { return false } t := ICMP4Type(q.b[q.subofs]) return t == ICMP4Unreachable || t == ICMP4TimeExceeded - case ICMPv6: + case ipproto.ICMPv6: if len(q.b) < q.subofs+8 { return false } @@ -387,9 +419,9 @@ func (q *Parsed) IsError() bool { // IsEchoRequest reports whether q is an ICMP Echo Request. func (q *Parsed) IsEchoRequest() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode - case ICMPv6: + case ipproto.ICMPv6: return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode default: return false @@ -399,9 +431,9 @@ func (q *Parsed) IsEchoRequest() bool { // IsEchoRequest reports whether q is an IPv4 ICMP Echo Response. func (q *Parsed) IsEchoResponse() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode - case ICMPv6: + case ipproto.ICMPv6: return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode default: return false diff --git a/net/packet/packet_test.go b/net/packet/packet_test.go index 8bac5db4a..ac4fa33f3 100644 --- a/net/packet/packet_test.go +++ b/net/packet/packet_test.go @@ -10,6 +10,19 @@ import ( "testing" "inet.af/netaddr" + "tailscale.com/types/ipproto" +) + +const ( + Unknown = ipproto.Unknown + TCP = ipproto.TCP + UDP = ipproto.UDP + SCTP = ipproto.SCTP + IGMP = ipproto.IGMP + ICMPv4 = ipproto.ICMPv4 + ICMPv6 = ipproto.ICMPv6 + TSMP = ipproto.TSMP + Fragment = ipproto.Fragment ) func mustIPPort(s string) netaddr.IPPort { @@ -305,6 +318,39 @@ var ipv4TSMPDecode = Parsed{ Dst: mustIPPort("100.74.70.3:0"), } +// IPv4 SCTP +var sctpBuffer = []byte{ + // IPv4 header: + 0x45, 0x00, + 0x00, 0x20, // 20 + 12 bytes total + 0x00, 0x00, // ID + 0x00, 0x00, // Fragment + 0x40, // TTL + byte(SCTP), + // Checksum, unchecked: + 1, 2, + // source IP: + 0x64, 0x5e, 0x0c, 0x0e, + // dest IP: + 0x64, 0x4a, 0x46, 0x03, + // Src Port, Dest Port: + 0x00, 0x7b, 0x01, 0xc8, + // Verification tag: + 1, 2, 3, 4, + // Checksum: (unchecked) + 5, 6, 7, 8, +} + +var sctpDecode = Parsed{ + b: sctpBuffer, + subofs: 20, + length: 20 + 12, + IPVersion: 4, + IPProto: SCTP, + Src: mustIPPort("100.94.12.14:123"), + Dst: mustIPPort("100.74.70.3:456"), +} + func TestParsedString(t *testing.T) { tests := []struct { name string @@ -320,6 +366,7 @@ func TestParsedString(t *testing.T) { {"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"}, {"unknown", unknownPacketDecode, "Unknown{???}"}, {"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"}, + {"sctp", sctpDecode, "SCTP{100.94.12.14:123 > 100.74.70.3:456}"}, } for _, tt := range tests { @@ -357,6 +404,7 @@ func TestDecode(t *testing.T) { {"unknown", unknownPacketBuffer, unknownPacketDecode}, {"invalid4", invalid4RequestBuffer, invalid4RequestDecode}, {"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode}, + {"ipv4_sctp", sctpBuffer, sctpDecode}, } for _, tt := range tests { diff --git a/net/packet/tsmp.go b/net/packet/tsmp.go index 2346c9419..fb257556c 100644 --- a/net/packet/tsmp.go +++ b/net/packet/tsmp.go @@ -17,6 +17,7 @@ import ( "inet.af/netaddr" "tailscale.com/net/flowtrack" + "tailscale.com/types/ipproto" ) // TailscaleRejectedHeader is a TSMP message that says that one @@ -39,7 +40,7 @@ type TailscaleRejectedHeader struct { IPDst netaddr.IP // IPv4 or IPv6 header's dst IP Src netaddr.IPPort // rejected flow's src Dst netaddr.IPPort // rejected flow's dst - Proto IPProto // proto that was rejected (TCP or UDP) + Proto ipproto.Proto // proto that was rejected (TCP or UDP) Reason TailscaleRejectReason // why the connection was rejected // MaybeBroken is whether the rejection is non-terminal (the @@ -57,7 +58,7 @@ type TailscaleRejectedHeader struct { const rejectFlagBitMaybeBroken = 0x1 func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple { - return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst} + return flowtrack.Tuple{Proto: rh.Proto, Src: rh.Src, Dst: rh.Dst} } func (rh TailscaleRejectedHeader) String() string { @@ -69,6 +70,12 @@ type TSMPType uint8 const ( // TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader. TSMPTypeRejectedConn TSMPType = '!' + + // TSMPTypePing is the type byte for a TailscalePingRequest. + TSMPTypePing TSMPType = 'p' + + // TSMPTypePong is the type byte for a TailscalePongResponse. + TSMPTypePong TSMPType = 'o' ) type TailscaleRejectReason byte @@ -138,7 +145,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error { } if h.Src.IP.Is4() { iph := IP4Header{ - IPProto: TSMP, + IPProto: ipproto.TSMP, Src: h.IPSrc, Dst: h.IPDst, } @@ -146,7 +153,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error { buf = buf[ip4HeaderLength:] } else if h.Src.IP.Is6() { iph := IP6Header{ - IPProto: TSMP, + IPProto: ipproto.TSMP, Src: h.IPSrc, Dst: h.IPDst, } @@ -181,7 +188,7 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo return } h = TailscaleRejectedHeader{ - Proto: IPProto(p[1]), + Proto: ipproto.Proto(p[1]), Reason: TailscaleRejectReason(p[2]), IPSrc: pp.Src.IP, IPDst: pp.Dst.IP, @@ -194,3 +201,58 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo } return h, true } + +// TSMPPingRequest is a TSMP message that's like an ICMP ping request. +// +// On the wire, after the IP header, it's currently 9 bytes: +// * 'p' (TSMPTypePing) +// * 8 opaque ping bytes to copy back in the response +type TSMPPingRequest struct { + Data [8]byte +} + +func (pp *Parsed) AsTSMPPing() (h TSMPPingRequest, ok bool) { + if pp.IPProto != ipproto.TSMP { + return + } + p := pp.Payload() + if len(p) < 9 || p[0] != byte(TSMPTypePing) { + return + } + copy(h.Data[:], p[1:]) + return h, true +} + +type TSMPPongReply struct { + IPHeader Header + Data [8]byte +} + +func (pp *Parsed) AsTSMPPong() (data [8]byte, ok bool) { + if pp.IPProto != ipproto.TSMP { + return + } + p := pp.Payload() + if len(p) < 9 || p[0] != byte(TSMPTypePong) { + return + } + copy(data[:], p[1:]) + return data, true +} + +func (h TSMPPongReply) Len() int { + return h.IPHeader.Len() + 9 +} + +func (h TSMPPongReply) Marshal(buf []byte) error { + if len(buf) < h.Len() { + return errSmallBuffer + } + if err := h.IPHeader.Marshal(buf); err != nil { + return err + } + buf = buf[h.IPHeader.Len():] + buf[0] = byte(TSMPTypePong) + copy(buf[1:], h.Data[:]) + return nil +} diff --git a/net/packet/udp4.go b/net/packet/udp4.go index 82aa30179..ce179f89d 100644 --- a/net/packet/udp4.go +++ b/net/packet/udp4.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // udpHeaderLength is the size of the UDP packet header, not including // the outer IP header. @@ -31,7 +35,7 @@ func (h UDP4Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = UDP + h.IPProto = ipproto.UDP length := len(buf) - h.IP4Header.Len() binary.BigEndian.PutUint16(buf[20:22], h.SrcPort) diff --git a/net/packet/udp6.go b/net/packet/udp6.go index 0450eae9e..18213c1fb 100644 --- a/net/packet/udp6.go +++ b/net/packet/udp6.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // UDP6Header is an IPv6+UDP header. type UDP6Header struct { @@ -27,7 +31,7 @@ func (h UDP6Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = UDP + h.IPProto = ipproto.UDP length := len(buf) - h.IP6Header.Len() binary.BigEndian.PutUint16(buf[40:42], h.SrcPort) diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 44cf5cf23..9bf81326e 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -37,7 +37,7 @@ var ( ) // TailscaleServiceIP returns the listen address of services -// provided by Tailscale itself such as the Magic DNS proxy. +// provided by Tailscale itself such as the MagicDNS proxy. func TailscaleServiceIP() netaddr.IP { serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") }) return serviceIP.v diff --git a/wgengine/tstun/faketun.go b/net/tstun/fake.go similarity index 85% rename from wgengine/tstun/faketun.go rename to net/tstun/fake.go index 50880131a..f9c3a9d6f 100644 --- a/wgengine/tstun/faketun.go +++ b/net/tstun/fake.go @@ -16,10 +16,8 @@ type fakeTUN struct { closechan chan struct{} } -// NewFakeTUN returns a fake TUN device that does not depend on the -// operating system or any special permissions. -// It primarily exists for testing. -func NewFakeTUN() tun.Device { +// NewFake returns a tun.Device that does nothing. +func NewFake() tun.Device { return &fakeTUN{ evchan: make(chan tun.Event), closechan: make(chan struct{}), diff --git a/wgengine/ifstatus_noop.go b/net/tstun/ifstatus_noop.go similarity index 96% rename from wgengine/ifstatus_noop.go rename to net/tstun/ifstatus_noop.go index 7564d67ec..223be7949 100644 --- a/wgengine/ifstatus_noop.go +++ b/net/tstun/ifstatus_noop.go @@ -4,7 +4,7 @@ // +build !windows -package wgengine +package tstun import ( "time" diff --git a/wgengine/ifstatus_windows.go b/net/tstun/ifstatus_windows.go similarity index 99% rename from wgengine/ifstatus_windows.go rename to net/tstun/ifstatus_windows.go index 840b6cf39..840e50f4d 100644 --- a/wgengine/ifstatus_windows.go +++ b/net/tstun/ifstatus_windows.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 wgengine +package tstun import ( "fmt" diff --git a/net/tstun/tun.go b/net/tstun/tun.go new file mode 100644 index 000000000..d480d3244 --- /dev/null +++ b/net/tstun/tun.go @@ -0,0 +1,129 @@ +// 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 tun creates a tuntap device, working around OS-specific +// quirks if necessary. +package tstun + +import ( + "bytes" + "os" + "os/exec" + "runtime" + "time" + + "github.com/tailscale/wireguard-go/tun" + "tailscale.com/types/logger" + "tailscale.com/version/distro" +) + +// minimalMTU is the MTU we set on tailscale's TUN +// interface. wireguard-go defaults to 1420 bytes, which only works if +// the "outer" MTU is 1500 bytes. This breaks on DSL connections +// (typically 1492 MTU) and on GCE (1460 MTU?!). +// +// 1280 is the smallest MTU allowed for IPv6, which is a sensible +// "probably works everywhere" setting until we develop proper PMTU +// discovery. +const minimalMTU = 1280 + +// New returns a tun.Device for the requested device name. +func New(logf logger.Logf, tunName string) (tun.Device, error) { + dev, err := tun.CreateTUN(tunName, minimalMTU) + if err != nil { + return nil, err + } + if err := waitInterfaceUp(dev, 90*time.Second, logf); err != nil { + return nil, err + } + return dev, nil +} + +// Diagnose tries to explain a tuntap device creation failure. +// It pokes around the system and logs some diagnostic info that might +// help debug why tun creation failed. Because device creation has +// already failed and the program's about to end, log a lot. +func Diagnose(logf logger.Logf, tunName string) { + switch runtime.GOOS { + case "linux": + diagnoseLinuxTUNFailure(tunName, logf) + case "darwin": + diagnoseDarwinTUNFailure(tunName, logf) + default: + logf("no TUN failure diagnostics for OS %q", runtime.GOOS) + } +} + +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 { + logf("no TUN, and failed to look up kernel version: %v", err) + return + } + logf("Linux kernel version: %s", kernel) + + modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput() + if err == nil { + logf("'modprobe tun' successful") + // Either tun is currently loaded, or it's statically + // compiled into the kernel (which modprobe checks + // with /lib/modules/$(uname -r)/modules.builtin) + // + // So if there's a problem at this point, it's + // probably because /dev/net/tun doesn't exist. + const dev = "/dev/net/tun" + if fi, err := os.Stat(dev); err != nil { + logf("tun module loaded in kernel, but %s does not exist", dev) + } else { + logf("%s: %v", dev, fi.Mode()) + } + + // We failed to find why it failed. Just let our + // caller report the error it got from wireguard-go. + return + } + logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut) + + switch distro.Get() { + case distro.Debian: + dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput() + if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil { + logf("tun module not loaded nor found on disk") + return + } + if !bytes.Contains(dpkgOut, kernel) { + logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut) + } + case distro.Arch: + findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput() + if len(bytes.TrimSpace(findOut)) == 0 || err != nil { + logf("tun module not loaded nor found on disk") + return + } + if !bytes.Contains(findOut, kernel) { + logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut) + } + case distro.OpenWrt: + out, err := exec.Command("opkg", "list-installed").CombinedOutput() + if err != nil { + logf("error querying OpenWrt installed packages: %s", out) + return + } + for _, pkg := range []string{"kmod-tun", "ca-bundle"} { + if !bytes.Contains(out, []byte(pkg+" - ")) { + logf("Missing required package %s; run: opkg install %s", pkg, pkg) + } + } + } +} diff --git a/wgengine/tstun/tun_windows.go b/net/tstun/tun_windows.go similarity index 100% rename from wgengine/tstun/tun_windows.go rename to net/tstun/tun_windows.go diff --git a/wgengine/tstun/tun.go b/net/tstun/wrap.go similarity index 82% rename from wgengine/tstun/tun.go rename to net/tstun/wrap.go index 92af1b8b0..70225c52e 100644 --- a/wgengine/tstun/tun.go +++ b/net/tstun/wrap.go @@ -18,6 +18,7 @@ import ( "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" "tailscale.com/net/packet" + "tailscale.com/types/ipproto" "tailscale.com/types/logger" "tailscale.com/wgengine/filter" ) @@ -30,11 +31,11 @@ const maxBufferSize = device.MaxMessageSize const PacketStartOffset = device.MessageTransportHeaderSize // MaxPacketSize is the maximum size (in bytes) -// of a packet that can be injected into a tstun.TUN. +// of a packet that can be injected into a tstun.Wrapper. const MaxPacketSize = device.MaxContentSize var ( - // ErrClosed is returned when attempting an operation on a closed TUN. + // ErrClosed is returned when attempting an operation on a closed Wrapper. ErrClosed = errors.New("device closed") // ErrFiltered is returned when the acted-on packet is rejected by a filter. ErrFiltered = errors.New("packet dropped by filter") @@ -51,17 +52,14 @@ var ( // do not escape through {Pre,Post}Filter{In,Out}. var parsedPacketPool = sync.Pool{New: func() interface{} { return new(packet.Parsed) }} -// FilterFunc is a packet-filtering function with access to the TUN device. +// FilterFunc is a packet-filtering function with access to the Wrapper device. // It must not hold onto the packet struct, as its backing storage will be reused. -type FilterFunc func(*packet.Parsed, *TUN) filter.Response +type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response -// TUN wraps a tun.Device from wireguard-go, -// augmenting it with filtering and packet injection. -// All the added work happens in Read and Write: -// the other methods delegate to the underlying tdev. -type TUN struct { +// Wrapper augments a tun.Device with packet filtering and injection. +type Wrapper struct { logf logger.Logf - // tdev is the underlying TUN device. + // tdev is the underlying Wrapper device. tdev tun.Device closeOnce sync.Once @@ -108,12 +106,15 @@ type TUN struct { // PostFilterOut is the outbound filter function that runs after the main filter. PostFilterOut FilterFunc + // OnTSMPPongReceived, if non-nil, is called whenever a TSMP pong arrives. + OnTSMPPongReceived func(data [8]byte) + // disableFilter disables all filtering when set. This should only be used in tests. disableFilter bool } -func WrapTUN(logf logger.Logf, tdev tun.Device) *TUN { - tun := &TUN{ +func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper { + tun := &Wrapper{ logf: logger.WithPrefix(logf, "tstun: "), tdev: tdev, // bufferConsumed is conceptually a condition variable: @@ -136,12 +137,12 @@ func WrapTUN(logf logger.Logf, tdev tun.Device) *TUN { // SetDestIPActivityFuncs sets a map of funcs to run per packet // destination (the map keys). // -// The map ownership passes to the TUN. It must be non-nil. -func (t *TUN) SetDestIPActivityFuncs(m map[netaddr.IP]func()) { +// The map ownership passes to the Wrapper. It must be non-nil. +func (t *Wrapper) SetDestIPActivityFuncs(m map[netaddr.IP]func()) { t.destIPActivity.Store(m) } -func (t *TUN) Close() error { +func (t *Wrapper) Close() error { var err error t.closeOnce.Do(func() { // Other channels need not be closed: poll will exit gracefully after this. @@ -152,30 +153,30 @@ func (t *TUN) Close() error { return err } -func (t *TUN) Events() chan tun.Event { +func (t *Wrapper) Events() chan tun.Event { return t.tdev.Events() } -func (t *TUN) File() *os.File { +func (t *Wrapper) File() *os.File { return t.tdev.File() } -func (t *TUN) Flush() error { +func (t *Wrapper) Flush() error { return t.tdev.Flush() } -func (t *TUN) MTU() (int, error) { +func (t *Wrapper) MTU() (int, error) { return t.tdev.MTU() } -func (t *TUN) Name() (string, error) { +func (t *Wrapper) Name() (string, error) { return t.tdev.Name() } // poll polls t.tdev.Read, placing the oldest unconsumed packet into t.buffer. // This is needed because t.tdev.Read in general may block (it does on Windows), // so packets may be stuck in t.outbound if t.Read called t.tdev.Read directly. -func (t *TUN) poll() { +func (t *Wrapper) poll() { for { select { case <-t.closed: @@ -185,7 +186,7 @@ func (t *TUN) poll() { } // Read may use memory in t.buffer before PacketStartOffset for mandatory headers. - // This is the rationale behind the tun.TUN.{Read,Write} interfaces + // This is the rationale behind the tun.Wrapper.{Read,Write} interfaces // and the reason t.buffer has size MaxMessageSize and not MaxContentSize. n, err := t.tdev.Read(t.buffer[:], PacketStartOffset) if err != nil { @@ -217,7 +218,7 @@ func (t *TUN) poll() { var magicDNSIPPort = netaddr.MustParseIPPort("100.100.100.100:0") -func (t *TUN) filterOut(p *packet.Parsed) filter.Response { +func (t *Wrapper) 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() @@ -253,7 +254,7 @@ func (t *TUN) filterOut(p *packet.Parsed) filter.Response { } // noteActivity records that there was a read or write at the current time. -func (t *TUN) noteActivity() { +func (t *Wrapper) noteActivity() { atomic.StoreInt64(&t.lastActivityAtomic, time.Now().Unix()) } @@ -261,12 +262,12 @@ func (t *TUN) noteActivity() { // // Its value is only accurate to roughly second granularity. // If there's never been activity, the duration is since 1970. -func (t *TUN) IdleDuration() time.Duration { +func (t *Wrapper) IdleDuration() time.Duration { sec := atomic.LoadInt64(&t.lastActivityAtomic) return time.Since(time.Unix(sec, 0)) } -func (t *TUN) Read(buf []byte, offset int) (int, error) { +func (t *Wrapper) Read(buf []byte, offset int) (int, error) { var n int wasInjectedPacket := false @@ -317,11 +318,23 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) { return n, nil } -func (t *TUN) filterIn(buf []byte) filter.Response { +func (t *Wrapper) filterIn(buf []byte) filter.Response { p := parsedPacketPool.Get().(*packet.Parsed) defer parsedPacketPool.Put(p) p.Decode(buf) + if p.IPProto == ipproto.TSMP { + if pingReq, ok := p.AsTSMPPing(); ok { + t.noteActivity() + t.injectOutboundPong(p, pingReq) + return filter.DropSilently + } else if data, ok := p.AsTSMPPong(); ok { + if f := t.OnTSMPPongReceived; f != nil { + f(data) + } + } + } + if t.PreFilterIn != nil { if res := t.PreFilterIn(p, t); res.IsDrop() { return res @@ -340,7 +353,7 @@ func (t *TUN) filterIn(buf []byte) filter.Response { // Their host networking stack can translate this into ICMP // or whatnot as required. But notably, their GUI or tailscale CLI // can show them a rejection history with reasons. - if p.IPVersion == 4 && p.IPProto == packet.TCP && p.TCPFlags&packet.TCPSyn != 0 { + if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 { rj := packet.TailscaleRejectedHeader{ IPSrc: p.Dst.IP, IPDst: p.Src.IP, @@ -372,7 +385,7 @@ func (t *TUN) filterIn(buf []byte) filter.Response { // Write accepts an incoming packet. The packet begins at buf[offset:], // like wireguard-go/tun.Device.Write. -func (t *TUN) Write(buf []byte, offset int) (int, error) { +func (t *Wrapper) Write(buf []byte, offset int) (int, error) { if !t.disableFilter { res := t.filterIn(buf[offset:]) if res == filter.DropSilently { @@ -387,16 +400,16 @@ func (t *TUN) Write(buf []byte, offset int) (int, error) { return t.tdev.Write(buf, offset) } -func (t *TUN) GetFilter() *filter.Filter { +func (t *Wrapper) GetFilter() *filter.Filter { filt, _ := t.filter.Load().(*filter.Filter) return filt } -func (t *TUN) SetFilter(filt *filter.Filter) { +func (t *Wrapper) SetFilter(filt *filter.Filter) { t.filter.Store(filt) } -// InjectInboundDirect makes the TUN device behave as if a packet +// InjectInboundDirect makes the Wrapper device behave as if a packet // with the given contents was received from the network. // It blocks and does not take ownership of the packet. // The injected packet will not pass through inbound filters. @@ -404,7 +417,7 @@ func (t *TUN) SetFilter(filt *filter.Filter) { // The packet contents are to start at &buf[offset]. // offset must be greater or equal to PacketStartOffset. // The space before &buf[offset] will be used by Wireguard. -func (t *TUN) InjectInboundDirect(buf []byte, offset int) error { +func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error { if len(buf) > MaxPacketSize { return errPacketTooBig } @@ -423,7 +436,7 @@ func (t *TUN) InjectInboundDirect(buf []byte, offset int) error { // InjectInboundCopy takes a packet without leading space, // reallocates it to conform to the InjectInboundDirect interface // and calls InjectInboundDirect on it. Injecting a nil packet is a no-op. -func (t *TUN) InjectInboundCopy(packet []byte) error { +func (t *Wrapper) InjectInboundCopy(packet []byte) error { // We duplicate this check from InjectInboundDirect here // to avoid wasting an allocation on an oversized packet. if len(packet) > MaxPacketSize { @@ -439,12 +452,32 @@ func (t *TUN) InjectInboundCopy(packet []byte) error { return t.InjectInboundDirect(buf, PacketStartOffset) } -// InjectOutbound makes the TUN device behave as if a packet +func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingRequest) { + pong := packet.TSMPPongReply{ + Data: req.Data, + } + switch pp.IPVersion { + case 4: + h4 := pp.IP4Header() + h4.ToResponse() + pong.IPHeader = h4 + case 6: + h6 := pp.IP6Header() + h6.ToResponse() + pong.IPHeader = h6 + default: + return + } + + t.InjectOutbound(packet.Generate(pong, nil)) +} + +// InjectOutbound makes the Wrapper device behave as if a packet // with the given contents was sent to the network. // It does not block, but takes ownership of the packet. // The injected packet will not pass through outbound filters. // Injecting an empty packet is a no-op. -func (t *TUN) InjectOutbound(packet []byte) error { +func (t *Wrapper) InjectOutbound(packet []byte) error { if len(packet) > MaxPacketSize { return errPacketTooBig } @@ -459,7 +492,7 @@ func (t *TUN) InjectOutbound(packet []byte) error { } } -// Unwrap returns the underlying TUN device. -func (t *TUN) Unwrap() tun.Device { +// Unwrap returns the underlying tun.Device. +func (t *Wrapper) Unwrap() tun.Device { return t.tdev } diff --git a/wgengine/tstun/tun_test.go b/net/tstun/wrap_test.go similarity index 93% rename from wgengine/tstun/tun_test.go rename to net/tstun/wrap_test.go index 365d56b4d..4032b168b 100644 --- a/wgengine/tstun/tun_test.go +++ b/net/tstun/wrap_test.go @@ -16,6 +16,7 @@ import ( "github.com/tailscale/wireguard-go/tun/tuntest" "inet.af/netaddr" "tailscale.com/net/packet" + "tailscale.com/types/ipproto" "tailscale.com/types/logger" "tailscale.com/wgengine/filter" ) @@ -105,19 +106,23 @@ func netports(netPorts ...string) (ret []filter.NetPortRange) { return ret } -func setfilter(logf logger.Logf, tun *TUN) { +func setfilter(logf logger.Logf, tun *Wrapper) { + protos := []ipproto.Proto{ + ipproto.TCP, + ipproto.UDP, + } matches := []filter.Match{ - {Srcs: nets("5.6.7.8"), Dsts: netports("1.2.3.4:89-90")}, - {Srcs: nets("1.2.3.4"), Dsts: netports("5.6.7.8:98")}, + {IPProto: protos, Srcs: nets("5.6.7.8"), Dsts: netports("1.2.3.4:89-90")}, + {IPProto: protos, Srcs: nets("1.2.3.4"), Dsts: netports("5.6.7.8:98")}, } var sb netaddr.IPSetBuilder sb.AddPrefix(netaddr.MustParseIPPrefix("1.2.0.0/16")) tun.SetFilter(filter.New(matches, sb.IPSet(), sb.IPSet(), nil, logf)) } -func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *TUN) { +func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper) { chtun := tuntest.NewChannelTUN() - tun := WrapTUN(logf, chtun.TUN()) + tun := Wrap(logf, chtun.TUN()) if secure { setfilter(logf, tun) } else { @@ -126,9 +131,9 @@ func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *TUN) { return chtun, tun } -func newFakeTUN(logf logger.Logf, secure bool) (*fakeTUN, *TUN) { - ftun := NewFakeTUN() - tun := WrapTUN(logf, ftun) +func newFakeTUN(logf logger.Logf, secure bool) (*fakeTUN, *Wrapper) { + ftun := NewFake() + tun := Wrap(logf, ftun) if secure { setfilter(logf, tun) } else { @@ -269,7 +274,7 @@ func TestFilter(t *testing.T) { {"good_packet_out", out, false, udp4("1.2.3.4", "5.6.7.8", 98, 98)}, } - // A reader on the other end of the TUN. + // A reader on the other end of the tun. go func() { var recvbuf []byte for { @@ -372,11 +377,11 @@ func BenchmarkWrite(b *testing.B) { } func TestAtomic64Alignment(t *testing.T) { - off := unsafe.Offsetof(TUN{}.lastActivityAtomic) + off := unsafe.Offsetof(Wrapper{}.lastActivityAtomic) if off%8 != 0 { t.Errorf("offset %v not 8-byte aligned", off) } - c := new(TUN) + c := new(Wrapper) atomic.StoreInt64(&c.lastActivityAtomic, 123) } diff --git a/syncs/syncs.go b/syncs/syncs.go index 0139ad925..f319f6489 100644 --- a/syncs/syncs.go +++ b/syncs/syncs.go @@ -5,7 +5,10 @@ // Package syncs contains additional sync types and functionality. package syncs -import "sync/atomic" +import ( + "context" + "sync/atomic" +) // ClosedChan returns a channel that's already closed. func ClosedChan() <-chan struct{} { return closedChan } @@ -79,3 +82,45 @@ func (b *AtomicBool) Set(v bool) { func (b *AtomicBool) Get() bool { return atomic.LoadInt32((*int32)(b)) != 0 } + +// Semaphore is a counting semaphore. +// +// Use NewSemaphore to create one. +type Semaphore struct { + c chan struct{} +} + +// NewSemaphore returns a semaphore with resource count n. +func NewSemaphore(n int) Semaphore { + return Semaphore{c: make(chan struct{}, n)} +} + +// Acquire blocks until a resource is acquired. +func (s Semaphore) Acquire() { + s.c <- struct{}{} +} + +// AcquireContext reports whether the resource was acquired before the ctx was done. +func (s Semaphore) AcquireContext(ctx context.Context) bool { + select { + case s.c <- struct{}{}: + return true + case <-ctx.Done(): + return false + } +} + +// TryAcquire reports, without blocking, whether the resource was acquired. +func (s Semaphore) TryAcquire() bool { + select { + case s.c <- struct{}{}: + return true + default: + return false + } +} + +// Release releases a resource. +func (s Semaphore) Release() { + <-s.c +} diff --git a/syncs/syncs_test.go b/syncs/syncs_test.go index 9de72e22f..a6768e90b 100644 --- a/syncs/syncs_test.go +++ b/syncs/syncs_test.go @@ -4,7 +4,10 @@ package syncs -import "testing" +import ( + "context" + "testing" +) func TestWaitGroupChan(t *testing.T) { wg := NewWaitGroupChan() @@ -48,3 +51,25 @@ func TestClosedChan(t *testing.T) { } } } + +func TestSemaphore(t *testing.T) { + s := NewSemaphore(2) + s.Acquire() + if !s.TryAcquire() { + t.Fatal("want true") + } + if s.TryAcquire() { + t.Fatal("want false") + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + if s.AcquireContext(ctx) { + t.Fatal("want false") + } + s.Release() + if !s.AcquireContext(context.Background()) { + t.Fatal("want true") + } + s.Release() + s.Release() +} diff --git a/syncs/watchdog_test.go b/syncs/watchdog_test.go index b5cc3452e..116d00625 100644 --- a/syncs/watchdog_test.go +++ b/syncs/watchdog_test.go @@ -6,9 +6,12 @@ package syncs import ( "context" + "runtime" "sync" "testing" "time" + + "tailscale.com/util/cibuild" ) // Time-based tests are fundamentally flaky. @@ -46,6 +49,12 @@ func TestWatchContended(t *testing.T) { } func TestWatchMultipleValues(t *testing.T) { + if cibuild.On() && runtime.GOOS == "windows" { + // On the CI machine, it sometimes takes 500ms to start a new goroutine. + // When this happens, we don't get enough events quickly enough. + // Nothing's wrong, and it's not worth working around. Just skip the test. + t.Skip("flaky on Windows CI") + } mu := new(sync.Mutex) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // not necessary, but keep vet happy diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index f4992124e..30119b671 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -15,7 +15,6 @@ import ( "time" "go4.org/mem" - "golang.org/x/oauth2" "inet.af/netaddr" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -36,7 +35,8 @@ import ( // 10: 2021-01-17: client understands MapResponse.PeerSeenChange // 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping // 12: 2021-03-04: client understands PingRequest -const CurrentMapRequestVersion = 12 +// 13: 2021-03-19: client understands FilterRule.IPProto +const CurrentMapRequestVersion = 13 type StableID string @@ -539,6 +539,61 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool { return reflect.DeepEqual(h, h2) } +// SignatureType specifies a scheme for signing RegisterRequest messages. It +// specifies the crypto algorithms to use, the contents of what is signed, and +// any other relevant details. Historically, requests were unsigned so the zero +// value is SignatureNone. +type SignatureType int + +const ( + // SignatureNone indicates that there is no signature, no Timestamp is + // required (but may be specified if desired), and both DeviceCert and + // Signature should be empty. + SignatureNone = SignatureType(iota) + // SignatureUnknown represents an unknown signature scheme, which should + // be considered an error if seen. + SignatureUnknown + // SignatureV1 is computed as RSA-PSS-Sign(privateKeyForDeviceCert, + // SHA256(Timestamp || ServerIdentity || DeviceCert || ServerPubKey || + // MachinePubKey)). The PSS salt length is equal to hash length + // (rsa.PSSSaltLengthEqualsHash). Device cert is required. + SignatureV1 +) + +func (st SignatureType) MarshalText() ([]byte, error) { + return []byte(st.String()), nil +} + +func (st *SignatureType) UnmarshalText(b []byte) error { + switch string(b) { + case "signature-none": + *st = SignatureNone + case "signature-v1": + *st = SignatureV1 + default: + var val int + if _, err := fmt.Sscanf(string(b), "signature-unknown(%d)", &val); err != nil { + *st = SignatureType(val) + } else { + *st = SignatureUnknown + } + } + return nil +} + +func (st SignatureType) String() string { + switch st { + case SignatureNone: + return "signature-none" + case SignatureUnknown: + return "signature-unknown" + case SignatureV1: + return "signature-v1" + default: + return fmt.Sprintf("signature-unknown(%d)", int(st)) + } +} + // RegisterRequest is sent by a client to register the key for a node. // It is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box, // using the local machine key, and sent to: @@ -552,12 +607,19 @@ type RegisterRequest struct { _ structs.Incomparable // One of Provider/LoginName, Oauth2Token, or AuthKey is set. Provider, LoginName string - Oauth2Token *oauth2.Token + Oauth2Token *Oauth2Token AuthKey string } Expiry time.Time // requested key expiry, server policy may override Followup string // response waits until AuthURL is visited Hostinfo *Hostinfo + + // The following fields are not used for SignatureNone and are required for + // SignatureV1: + SignatureType SignatureType `json:",omitempty"` + Timestamp *time.Time `json:",omitempty"` // creation time of request to prevent replay + DeviceCert []byte `json:",omitempty"` // X.509 certificate for client device + Signature []byte `json:",omitempty"` // as described by SignatureType } // Clone makes a deep copy of RegisterRequest. @@ -574,6 +636,8 @@ func (req *RegisterRequest) Clone() *RegisterRequest { tok := *res.Auth.Oauth2Token res.Auth.Oauth2Token = &tok } + res.DeviceCert = append(res.DeviceCert[:0:0], res.DeviceCert...) + res.Signature = append(res.Signature[:0:0], res.Signature...) return res } @@ -694,6 +758,17 @@ type FilterRule struct { // DstPorts are the port ranges to allow once a source IP // matches (is in the CIDR described by SrcIPs & SrcBits). DstPorts []NetPortRange + + // IPProto are the IP protocol numbers to match. + // + // As a special case, nil or empty means TCP, UDP, and ICMP. + // + // Numbers outside the uint8 range (below 0 or above 255) are + // reserved for Tailscale's use. Unknown ones are ignored. + // + // Depending on the IPProto values, DstPorts may or may not be + // used. + IPProto []int `json:",omitempty"` } var FilterAllowAll = []FilterRule{ @@ -719,8 +794,8 @@ type DNSConfig struct { // Some OSes and OS configurations don't support per-domain DNS configuration, // in which case Nameservers applies to all DNS requests regardless of PerDomain's value. PerDomain bool - // Proxied indicates whether DNS requests are proxied through a tsdns.Resolver. - // This enables Magic DNS. It is togglable independently of PerDomain. + // Proxied indicates whether DNS requests are proxied through a dns.Resolver. + // This enables MagicDNS. It is togglable independently of PerDomain. Proxied bool } @@ -975,3 +1050,28 @@ type WhoIsResponse struct { Node *Node UserProfile *UserProfile } + +// Oauth2Token is a copy of golang.org/x/oauth2.Token, to avoid the +// go.mod dependency on App Engine and grpc, which was causing problems. +// All we actually needed was this struct on the client side. +type Oauth2Token struct { + // AccessToken is the token that authorizes and authenticates + // the requests. + AccessToken string `json:"access_token"` + + // TokenType is the type of token. + // The Type method returns either this or "Bearer", the default. + TokenType string `json:"token_type,omitempty"` + + // RefreshToken is a token that's used by the application + // (as opposed to the user) to refresh the access token + // if it expires. + RefreshToken string `json:"refresh_token,omitempty"` + + // Expiry is the optional expiration time of the access token. + // + // If zero, TokenSource implementations will reuse the same + // token forever and RefreshToken or equivalent + // mechanisms for that TokenSource will not be used. + Expiry time.Time `json:"expiry,omitempty"` +} diff --git a/tstest/natlab/natlab.go b/tstest/natlab/natlab.go index df2611be4..2d3f3ae70 100644 --- a/tstest/natlab/natlab.go +++ b/tstest/natlab/natlab.go @@ -26,7 +26,6 @@ import ( "sync" "time" - wgconn "github.com/tailscale/wireguard-go/conn" "inet.af/netaddr" ) @@ -759,8 +758,7 @@ func (c *conn) canRead() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { - // TODO: when we switch to Go 1.16, replace this with net.ErrClosed - return wgconn.NetErrClosed + return net.ErrClosed } if !c.readDeadline.IsZero() && c.readDeadline.Before(time.Now()) { return errors.New("read deadline exceeded") diff --git a/net/packet/ip.go b/types/ipproto/ipproto.go similarity index 72% rename from net/packet/ip.go rename to types/ipproto/ipproto.go index 34194f344..5c9d101f6 100644 --- a/net/packet/ip.go +++ b/types/ipproto/ipproto.go @@ -1,28 +1,32 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// 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 packet +// Package ipproto contains IP Protocol constants. +package ipproto -// IPProto is an IP subprotocol as defined by the IANA protocol +import "fmt" + +// Proto is an IP subprotocol as defined by the IANA protocol // numbers list // (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml), // or the special values Unknown or Fragment. -type IPProto uint8 +type Proto uint8 const ( // Unknown represents an unknown or unsupported protocol; it's // deliberately the zero value. Strictly speaking the zero // value is IPv6 hop-by-hop extensions, but we don't support // those, so this is still technically correct. - Unknown IPProto = 0x00 + Unknown Proto = 0x00 // Values from the IANA registry. - ICMPv4 IPProto = 0x01 - IGMP IPProto = 0x02 - ICMPv6 IPProto = 0x3a - TCP IPProto = 0x06 - UDP IPProto = 0x11 + ICMPv4 Proto = 0x01 + IGMP Proto = 0x02 + ICMPv6 Proto = 0x3a + TCP Proto = 0x06 + UDP Proto = 0x11 + SCTP Proto = 0x84 // TSMP is the Tailscale Message Protocol (our ICMP-ish // thing), an IP protocol used only between Tailscale nodes @@ -33,7 +37,7 @@ const ( // scheme". We never accept these from the host OS stack nor // send them to the host network stack. It's only used between // nodes. - TSMP IPProto = 99 + TSMP Proto = 99 // Fragment represents any non-first IP fragment, for which we // don't have the sub-protocol header (and therefore can't @@ -41,11 +45,13 @@ const ( // // 0xFF is reserved in the IANA registry, so we steal it for // internal use. - Fragment IPProto = 0xFF + Fragment Proto = 0xFF ) -func (p IPProto) String() string { +func (p Proto) String() string { switch p { + case Unknown: + return "Unknown" case Fragment: return "Frag" case ICMPv4: @@ -58,9 +64,11 @@ func (p IPProto) String() string { return "UDP" case TCP: return "TCP" + case SCTP: + return "SCTP" case TSMP: return "TSMP" default: - return "Unknown" + return fmt.Sprintf("IPProto-%d", int(p)) } } diff --git a/wgengine/filter/filter.go b/wgengine/filter/filter.go index cbb985114..3c4964c34 100644 --- a/wgengine/filter/filter.go +++ b/wgengine/filter/filter.go @@ -14,6 +14,7 @@ import ( "inet.af/netaddr" "tailscale.com/net/flowtrack" "tailscale.com/net/packet" + "tailscale.com/types/ipproto" "tailscale.com/types/logger" ) @@ -182,6 +183,7 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches { var ret matches for _, m := range ms { var retm Match + retm.IPProto = m.IPProto for _, src := range m.Srcs { if keep(src.IP) { retm.Srcs = append(retm.Srcs, src) @@ -266,7 +268,7 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response { } pkt.Src.IP = srcIP pkt.Dst.IP = dstIP - pkt.IPProto = packet.TCP + pkt.IPProto = ipproto.TCP pkt.TCPFlags = packet.TCPSyn pkt.Src.Port = 0 pkt.Dst.Port = dstPort @@ -324,7 +326,7 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) { } switch q.IPProto { - case packet.ICMPv4: + case ipproto.ICMPv4: if q.IsEchoResponse() || q.IsError() { // ICMP responses are allowed. // TODO(apenwarr): consider using conntrack state. @@ -336,7 +338,7 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) { // If any port is open to an IP, allow ICMP to it. return Accept, "icmp ok" } - case packet.TCP: + case ipproto.TCP: // For TCP, we want to allow *outgoing* connections, // which means we want to allow return packets on those // connections. To make this restriction work, we need to @@ -351,20 +353,20 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) { if f.matches4.match(q) { return Accept, "tcp ok" } - case packet.UDP: - t := flowtrack.Tuple{Src: q.Src, Dst: q.Dst} + case ipproto.UDP, ipproto.SCTP: + t := flowtrack.Tuple{Proto: q.IPProto, Src: q.Src, Dst: q.Dst} f.state.mu.Lock() _, ok := f.state.lru.Get(t) f.state.mu.Unlock() if ok { - return Accept, "udp cached" + return Accept, "cached" } if f.matches4.match(q) { - return Accept, "udp ok" + return Accept, "ok" } - case packet.TSMP: + case ipproto.TSMP: return Accept, "tsmp ok" default: return Drop, "Unknown proto" @@ -381,7 +383,7 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) { } switch q.IPProto { - case packet.ICMPv6: + case ipproto.ICMPv6: if q.IsEchoResponse() || q.IsError() { // ICMP responses are allowed. // TODO(apenwarr): consider using conntrack state. @@ -393,7 +395,7 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) { // If any port is open to an IP, allow ICMP to it. return Accept, "icmp ok" } - case packet.TCP: + case ipproto.TCP: // For TCP, we want to allow *outgoing* connections, // which means we want to allow return packets on those // connections. To make this restriction work, we need to @@ -402,25 +404,27 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) { // can't be initiated without first sending a SYN. // It happens to also be much faster. // TODO(apenwarr): Skip the rest of decoding in this path? - if q.IPProto == packet.TCP && !q.IsTCPSyn() { + if q.IPProto == ipproto.TCP && !q.IsTCPSyn() { return Accept, "tcp non-syn" } if f.matches6.match(q) { return Accept, "tcp ok" } - case packet.UDP: - t := flowtrack.Tuple{Src: q.Src, Dst: q.Dst} + case ipproto.UDP, ipproto.SCTP: + t := flowtrack.Tuple{Proto: q.IPProto, Src: q.Src, Dst: q.Dst} f.state.mu.Lock() _, ok := f.state.lru.Get(t) f.state.mu.Unlock() if ok { - return Accept, "udp cached" + return Accept, "cached" } if f.matches6.match(q) { - return Accept, "udp ok" + return Accept, "ok" } + case ipproto.TSMP: + return Accept, "tsmp ok" default: return Drop, "Unknown proto" } @@ -429,15 +433,16 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) { // runIn runs the output-specific part of the filter logic. func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) { - if q.IPProto != packet.UDP { - return Accept, "ok out" + switch q.IPProto { + case ipproto.UDP, ipproto.SCTP: + tuple := flowtrack.Tuple{ + Proto: q.IPProto, + Src: q.Dst, Dst: q.Src, // src/dst reversed + } + f.state.mu.Lock() + f.state.lru.Add(tuple, nil) + f.state.mu.Unlock() } - - tuple := flowtrack.Tuple{Src: q.Dst, Dst: q.Src} // src/dst reversed - - f.state.mu.Lock() - f.state.lru.Add(tuple, nil) - f.state.mu.Unlock() return Accept, "ok out" } @@ -485,11 +490,11 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response { } switch q.IPProto { - case packet.Unknown: + case ipproto.Unknown: // Unknown packets are dangerous; always drop them. f.logRateLimit(rf, q, dir, Drop, "unknown") return Drop - case packet.Fragment: + case ipproto.Fragment: // Fragments after the first always need to be passed through. // Very small fragments are considered Junk by Parsed. f.logRateLimit(rf, q, dir, Accept, "fragment") @@ -513,5 +518,5 @@ func omitDropLogging(p *packet.Parsed, dir direction) bool { return false } - return p.Dst.IP.IsMulticast() || (p.Dst.IP.IsLinkLocalUnicast() && p.Dst.IP != gcpDNSAddr) || p.IPProto == packet.IGMP + return p.Dst.IP.IsMulticast() || (p.Dst.IP.IsLinkLocalUnicast() && p.Dst.IP != gcpDNSAddr) || p.IPProto == ipproto.IGMP } diff --git a/wgengine/filter/filter_test.go b/wgengine/filter/filter_test.go index 9f98761f6..ca807a5fd 100644 --- a/wgengine/filter/filter_test.go +++ b/wgengine/filter/filter_test.go @@ -7,6 +7,7 @@ package filter import ( "encoding/hex" "fmt" + "reflect" "strconv" "strings" "testing" @@ -16,19 +17,32 @@ import ( "inet.af/netaddr" "tailscale.com/net/packet" "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" + "tailscale.com/types/ipproto" "tailscale.com/types/logger" ) func newFilter(logf logger.Logf) *Filter { + m := func(srcs []netaddr.IPPrefix, dsts []NetPortRange, protos ...ipproto.Proto) Match { + if protos == nil { + protos = defaultProtos + } + return Match{ + IPProto: protos, + Srcs: srcs, + Dsts: dsts, + } + } matches := []Match{ - {Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("1.2.3.4:22", "5.6.7.8:23-24")}, - {Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("5.6.7.8:27-28")}, - {Srcs: nets("2.2.2.2"), Dsts: netports("8.1.1.1:22")}, - {Srcs: nets("0.0.0.0/0"), Dsts: netports("100.122.98.50:*")}, - {Srcs: nets("0.0.0.0/0"), Dsts: netports("0.0.0.0/0:443")}, - {Srcs: nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), Dsts: netports("1.2.3.4:999")}, - {Srcs: nets("::1", "::2"), Dsts: netports("2001::1:22", "2001::2:22")}, - {Srcs: nets("::/0"), Dsts: netports("::/0:443")}, + m(nets("8.1.1.1", "8.2.2.2"), netports("1.2.3.4:22", "5.6.7.8:23-24")), + m(nets("9.1.1.1", "9.2.2.2"), netports("1.2.3.4:22", "5.6.7.8:23-24"), ipproto.SCTP), + m(nets("8.1.1.1", "8.2.2.2"), netports("5.6.7.8:27-28")), + m(nets("2.2.2.2"), netports("8.1.1.1:22")), + m(nets("0.0.0.0/0"), netports("100.122.98.50:*")), + m(nets("0.0.0.0/0"), netports("0.0.0.0/0:443")), + m(nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), netports("1.2.3.4:999")), + m(nets("::1", "::2"), netports("2001::1:22", "2001::2:22")), + m(nets("::/0"), netports("::/0:443")), } // Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8, @@ -52,43 +66,48 @@ func TestFilter(t *testing.T) { } tests := []InOut{ // allow 8.1.1.1 => 1.2.3.4:22 - {Accept, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22)}, - {Accept, parsed(packet.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0)}, - {Drop, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 0)}, - {Accept, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 22)}, - {Drop, parsed(packet.TCP, "8.1.1.1", "1.2.3.4", 0, 21)}, + {Accept, parsed(ipproto.TCP, "8.1.1.1", "1.2.3.4", 999, 22)}, + {Accept, parsed(ipproto.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0)}, + {Drop, parsed(ipproto.TCP, "8.1.1.1", "1.2.3.4", 0, 0)}, + {Accept, parsed(ipproto.TCP, "8.1.1.1", "1.2.3.4", 0, 22)}, + {Drop, parsed(ipproto.TCP, "8.1.1.1", "1.2.3.4", 0, 21)}, // allow 8.2.2.2. => 1.2.3.4:22 - {Accept, parsed(packet.TCP, "8.2.2.2", "1.2.3.4", 0, 22)}, - {Drop, parsed(packet.TCP, "8.2.2.2", "1.2.3.4", 0, 23)}, - {Drop, parsed(packet.TCP, "8.3.3.3", "1.2.3.4", 0, 22)}, + {Accept, parsed(ipproto.TCP, "8.2.2.2", "1.2.3.4", 0, 22)}, + {Drop, parsed(ipproto.TCP, "8.2.2.2", "1.2.3.4", 0, 23)}, + {Drop, parsed(ipproto.TCP, "8.3.3.3", "1.2.3.4", 0, 22)}, // allow 8.1.1.1 => 5.6.7.8:23-24 - {Accept, parsed(packet.TCP, "8.1.1.1", "5.6.7.8", 0, 23)}, - {Accept, parsed(packet.TCP, "8.1.1.1", "5.6.7.8", 0, 24)}, - {Drop, parsed(packet.TCP, "8.1.1.3", "5.6.7.8", 0, 24)}, - {Drop, parsed(packet.TCP, "8.1.1.1", "5.6.7.8", 0, 22)}, + {Accept, parsed(ipproto.TCP, "8.1.1.1", "5.6.7.8", 0, 23)}, + {Accept, parsed(ipproto.TCP, "8.1.1.1", "5.6.7.8", 0, 24)}, + {Drop, parsed(ipproto.TCP, "8.1.1.3", "5.6.7.8", 0, 24)}, + {Drop, parsed(ipproto.TCP, "8.1.1.1", "5.6.7.8", 0, 22)}, // allow * => *:443 - {Accept, parsed(packet.TCP, "17.34.51.68", "8.1.34.51", 0, 443)}, - {Drop, parsed(packet.TCP, "17.34.51.68", "8.1.34.51", 0, 444)}, + {Accept, parsed(ipproto.TCP, "17.34.51.68", "8.1.34.51", 0, 443)}, + {Drop, parsed(ipproto.TCP, "17.34.51.68", "8.1.34.51", 0, 444)}, // allow * => 100.122.98.50:* - {Accept, parsed(packet.TCP, "17.34.51.68", "100.122.98.50", 0, 999)}, - {Accept, parsed(packet.TCP, "17.34.51.68", "100.122.98.50", 0, 0)}, + {Accept, parsed(ipproto.TCP, "17.34.51.68", "100.122.98.50", 0, 999)}, + {Accept, parsed(ipproto.TCP, "17.34.51.68", "100.122.98.50", 0, 0)}, // allow ::1, ::2 => [2001::1]:22 - {Accept, parsed(packet.TCP, "::1", "2001::1", 0, 22)}, - {Accept, parsed(packet.ICMPv6, "::1", "2001::1", 0, 0)}, - {Accept, parsed(packet.TCP, "::2", "2001::1", 0, 22)}, - {Accept, parsed(packet.TCP, "::2", "2001::2", 0, 22)}, - {Drop, parsed(packet.TCP, "::1", "2001::1", 0, 23)}, - {Drop, parsed(packet.TCP, "::1", "2001::3", 0, 22)}, - {Drop, parsed(packet.TCP, "::3", "2001::1", 0, 22)}, + {Accept, parsed(ipproto.TCP, "::1", "2001::1", 0, 22)}, + {Accept, parsed(ipproto.ICMPv6, "::1", "2001::1", 0, 0)}, + {Accept, parsed(ipproto.TCP, "::2", "2001::1", 0, 22)}, + {Accept, parsed(ipproto.TCP, "::2", "2001::2", 0, 22)}, + {Drop, parsed(ipproto.TCP, "::1", "2001::1", 0, 23)}, + {Drop, parsed(ipproto.TCP, "::1", "2001::3", 0, 22)}, + {Drop, parsed(ipproto.TCP, "::3", "2001::1", 0, 22)}, // allow * => *:443 - {Accept, parsed(packet.TCP, "::1", "2001::1", 0, 443)}, - {Drop, parsed(packet.TCP, "::1", "2001::1", 0, 444)}, + {Accept, parsed(ipproto.TCP, "::1", "2001::1", 0, 443)}, + {Drop, parsed(ipproto.TCP, "::1", "2001::1", 0, 444)}, // localNets prefilter - accepted by policy filter, but // unexpected dst IP. - {Drop, parsed(packet.TCP, "8.1.1.1", "16.32.48.64", 0, 443)}, - {Drop, parsed(packet.TCP, "1::", "2602::1", 0, 443)}, + {Drop, parsed(ipproto.TCP, "8.1.1.1", "16.32.48.64", 0, 443)}, + {Drop, parsed(ipproto.TCP, "1::", "2602::1", 0, 443)}, + + // Don't allow protocols not specified by filter + {Drop, parsed(ipproto.SCTP, "8.1.1.1", "1.2.3.4", 999, 22)}, + // But SCTP is allowed for 9.1.1.1 + {Accept, parsed(ipproto.SCTP, "9.1.1.1", "1.2.3.4", 999, 22)}, } for i, test := range tests { aclFunc := acl.runIn4 @@ -98,7 +117,7 @@ func TestFilter(t *testing.T) { if got, why := aclFunc(&test.p); test.want != got { t.Errorf("#%d runIn got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p) } - if test.p.IPProto == packet.TCP { + if test.p.IPProto == ipproto.TCP { var got Response if test.p.IPVersion == 4 { got = acl.CheckTCP(test.p.Src.IP, test.p.Dst.IP, test.p.Dst.Port) @@ -109,7 +128,7 @@ func TestFilter(t *testing.T) { t.Errorf("#%d CheckTCP got=%v want=%v packet:%v", i, got, test.want, test.p) } // TCP and UDP are treated equivalently in the filter - verify that. - test.p.IPProto = packet.UDP + test.p.IPProto = ipproto.UDP if got, why := aclFunc(&test.p); test.want != got { t.Errorf("#%d runIn (UDP) got=%v want=%v why=%q packet:%v", i, got, test.want, why, test.p) } @@ -123,8 +142,8 @@ func TestUDPState(t *testing.T) { acl := newFilter(t.Logf) flags := LogDrops | LogAccepts - a4 := parsed(packet.UDP, "119.119.119.119", "102.102.102.102", 4242, 4343) - b4 := parsed(packet.UDP, "102.102.102.102", "119.119.119.119", 4343, 4242) + a4 := parsed(ipproto.UDP, "119.119.119.119", "102.102.102.102", 4242, 4343) + b4 := parsed(ipproto.UDP, "102.102.102.102", "119.119.119.119", 4343, 4242) // Unsollicited UDP traffic gets dropped if got := acl.RunIn(&a4, flags); got != Drop { @@ -139,8 +158,8 @@ func TestUDPState(t *testing.T) { t.Fatalf("incoming response packet not accepted, got=%v: %v", got, a4) } - a6 := parsed(packet.UDP, "2001::2", "2001::1", 4242, 4343) - b6 := parsed(packet.UDP, "2001::1", "2001::2", 4343, 4242) + a6 := parsed(ipproto.UDP, "2001::2", "2001::1", 4242, 4343) + b6 := parsed(ipproto.UDP, "2001::1", "2001::2", 4343, 4242) // Unsollicited UDP traffic gets dropped if got := acl.RunIn(&a6, flags); got != Drop { @@ -159,10 +178,10 @@ func TestUDPState(t *testing.T) { func TestNoAllocs(t *testing.T) { acl := newFilter(t.Logf) - tcp4Packet := raw4(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0) - udp4Packet := raw4(packet.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0) - tcp6Packet := raw6(packet.TCP, "2001::1", "2001::2", 999, 22, 0) - udp6Packet := raw6(packet.UDP, "2001::1", "2001::2", 999, 22, 0) + tcp4Packet := raw4(ipproto.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0) + udp4Packet := raw4(ipproto.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0) + tcp6Packet := raw6(ipproto.TCP, "2001::1", "2001::2", 999, 22, 0) + udp6Packet := raw6(ipproto.UDP, "2001::1", "2001::2", 999, 22, 0) tests := []struct { name string @@ -243,13 +262,13 @@ func TestParseIPSet(t *testing.T) { } func BenchmarkFilter(b *testing.B) { - tcp4Packet := raw4(packet.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0) - udp4Packet := raw4(packet.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0) - icmp4Packet := raw4(packet.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0, 0) + tcp4Packet := raw4(ipproto.TCP, "8.1.1.1", "1.2.3.4", 999, 22, 0) + udp4Packet := raw4(ipproto.UDP, "8.1.1.1", "1.2.3.4", 999, 22, 0) + icmp4Packet := raw4(ipproto.ICMPv4, "8.1.1.1", "1.2.3.4", 0, 0, 0) - tcp6Packet := raw6(packet.TCP, "::1", "2001::1", 999, 22, 0) - udp6Packet := raw6(packet.UDP, "::1", "2001::1", 999, 22, 0) - icmp6Packet := raw6(packet.ICMPv6, "::1", "2001::1", 0, 0, 0) + tcp6Packet := raw6(ipproto.TCP, "::1", "2001::1", 999, 22, 0) + udp6Packet := raw6(ipproto.UDP, "::1", "2001::1", 999, 22, 0) + icmp6Packet := raw6(ipproto.ICMPv6, "::1", "2001::1", 0, 0, 0) benches := []struct { name string @@ -296,11 +315,11 @@ func TestPreFilter(t *testing.T) { }{ {"empty", Accept, []byte{}}, {"short", Drop, []byte("short")}, - {"junk", Drop, raw4default(packet.Unknown, 10)}, - {"fragment", Accept, raw4default(packet.Fragment, 40)}, - {"tcp", noVerdict, raw4default(packet.TCP, 0)}, - {"udp", noVerdict, raw4default(packet.UDP, 0)}, - {"icmp", noVerdict, raw4default(packet.ICMPv4, 0)}, + {"junk", Drop, raw4default(ipproto.Unknown, 10)}, + {"fragment", Accept, raw4default(ipproto.Fragment, 40)}, + {"tcp", noVerdict, raw4default(ipproto.TCP, 0)}, + {"udp", noVerdict, raw4default(ipproto.UDP, 0)}, + {"icmp", noVerdict, raw4default(ipproto.ICMPv4, 0)}, } f := NewAllowNone(t.Logf, &netaddr.IPSet{}) for _, testPacket := range packets { @@ -322,7 +341,7 @@ func TestOmitDropLogging(t *testing.T) { }{ { name: "v4_tcp_out", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP}, dir: out, want: false, }, @@ -420,73 +439,73 @@ func TestLoggingPrivacy(t *testing.T) { }{ { name: "ts_to_ts_v4_out", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: ts4, Dst: ts4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: ts4, Dst: ts4}, dir: out, logged: true, }, { name: "ts_to_internet_v4_out", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: ts4, Dst: internet4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: ts4, Dst: internet4}, dir: out, logged: false, }, { name: "internet_to_ts_v4_out", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: internet4, Dst: ts4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: internet4, Dst: ts4}, dir: out, logged: false, }, { name: "ts_to_ts_v4_in", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: ts4, Dst: ts4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: ts4, Dst: ts4}, dir: in, logged: true, }, { name: "ts_to_internet_v4_in", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: ts4, Dst: internet4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: ts4, Dst: internet4}, dir: in, logged: false, }, { name: "internet_to_ts_v4_in", - pkt: &packet.Parsed{IPVersion: 4, IPProto: packet.TCP, Src: internet4, Dst: ts4}, + pkt: &packet.Parsed{IPVersion: 4, IPProto: ipproto.TCP, Src: internet4, Dst: ts4}, dir: in, logged: false, }, { name: "ts_to_ts_v6_out", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: ts6, Dst: ts6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: ts6, Dst: ts6}, dir: out, logged: true, }, { name: "ts_to_internet_v6_out", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: ts6, Dst: internet6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: ts6, Dst: internet6}, dir: out, logged: false, }, { name: "internet_to_ts_v6_out", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: internet6, Dst: ts6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: internet6, Dst: ts6}, dir: out, logged: false, }, { name: "ts_to_ts_v6_in", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: ts6, Dst: ts6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: ts6, Dst: ts6}, dir: in, logged: true, }, { name: "ts_to_internet_v6_in", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: ts6, Dst: internet6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: ts6, Dst: internet6}, dir: in, logged: false, }, { name: "internet_to_ts_v6_in", - pkt: &packet.Parsed{IPVersion: 6, IPProto: packet.TCP, Src: internet6, Dst: ts6}, + pkt: &packet.Parsed{IPVersion: 6, IPProto: ipproto.TCP, Src: internet6, Dst: ts6}, dir: in, logged: false, }, @@ -520,7 +539,7 @@ func mustIP(s string) netaddr.IP { return ip } -func parsed(proto packet.IPProto, src, dst string, sport, dport uint16) packet.Parsed { +func parsed(proto ipproto.Proto, src, dst string, sport, dport uint16) packet.Parsed { sip, dip := mustIP(src), mustIP(dst) var ret packet.Parsed @@ -541,7 +560,7 @@ func parsed(proto packet.IPProto, src, dst string, sport, dport uint16) packet.P return ret } -func raw6(proto packet.IPProto, src, dst string, sport, dport uint16, trimLen int) []byte { +func raw6(proto ipproto.Proto, src, dst string, sport, dport uint16, trimLen int) []byte { u := packet.UDP6Header{ IP6Header: packet.IP6Header{ Src: mustIP(src), @@ -570,7 +589,7 @@ func raw6(proto packet.IPProto, src, dst string, sport, dport uint16, trimLen in } } -func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength int) []byte { +func raw4(proto ipproto.Proto, src, dst string, sport, dport uint16, trimLength int) []byte { u := packet.UDP4Header{ IP4Header: packet.IP4Header{ Src: mustIP(src), @@ -588,7 +607,7 @@ func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength // UDP marshaling clobbers IPProto, so override it here. switch proto { - case packet.Unknown, packet.Fragment: + case ipproto.Unknown, ipproto.Fragment: default: u.IP4Header.IPProto = proto } @@ -596,7 +615,7 @@ func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength panic(err) } - if proto == packet.Fragment { + if proto == ipproto.Fragment { // Set some fragment offset. This makes the IP // checksum wrong, but we don't validate the checksum // when parsing. @@ -610,7 +629,7 @@ func raw4(proto packet.IPProto, src, dst string, sport, dport uint16, trimLength } } -func raw4default(proto packet.IPProto, trimLength int) []byte { +func raw4default(proto ipproto.Proto, trimLength int) []byte { return raw4(proto, "8.8.8.8", "8.8.8.8", 53, 53, trimLength) } @@ -707,3 +726,91 @@ func netports(netPorts ...string) (ret []NetPortRange) { } return ret } + +func TestMatchesFromFilterRules(t *testing.T) { + tests := []struct { + name string + in []tailcfg.FilterRule + want []Match + }{ + { + name: "empty", + want: []Match{}, + }, + { + name: "implicit_protos", + in: []tailcfg.FilterRule{ + { + SrcIPs: []string{"100.64.1.1"}, + DstPorts: []tailcfg.NetPortRange{{ + IP: "*", + Ports: tailcfg.PortRange{First: 22, Last: 22}, + }}, + }, + }, + want: []Match{ + { + IPProto: []ipproto.Proto{ + ipproto.TCP, + ipproto.UDP, + ipproto.ICMPv4, + ipproto.ICMPv6, + }, + Dsts: []NetPortRange{ + { + Net: netaddr.MustParseIPPrefix("0.0.0.0/0"), + Ports: PortRange{22, 22}, + }, + { + Net: netaddr.MustParseIPPrefix("::0/0"), + Ports: PortRange{22, 22}, + }, + }, + Srcs: []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("100.64.1.1/32"), + }, + }, + }, + }, + { + name: "explicit_protos", + in: []tailcfg.FilterRule{ + { + IPProto: []int{int(ipproto.TCP)}, + SrcIPs: []string{"100.64.1.1"}, + DstPorts: []tailcfg.NetPortRange{{ + IP: "1.2.0.0/16", + Ports: tailcfg.PortRange{First: 22, Last: 22}, + }}, + }, + }, + want: []Match{ + { + IPProto: []ipproto.Proto{ + ipproto.TCP, + }, + Dsts: []NetPortRange{ + { + Net: netaddr.MustParseIPPrefix("1.2.0.0/16"), + Ports: PortRange{22, 22}, + }, + }, + Srcs: []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("100.64.1.1/32"), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MatchesFromFilterRules(tt.in) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("wrong\n got: %v\nwant: %v\n", got, tt.want) + } + }) + } +} diff --git a/wgengine/filter/match.go b/wgengine/filter/match.go index c30c37552..a1b356113 100644 --- a/wgengine/filter/match.go +++ b/wgengine/filter/match.go @@ -10,6 +10,7 @@ import ( "inet.af/netaddr" "tailscale.com/net/packet" + "tailscale.com/types/ipproto" ) //go:generate go run tailscale.com/cmd/cloner --type=Match --output=match_clone.go @@ -47,11 +48,13 @@ func (npr NetPortRange) String() string { // Match matches packets from any IP address in Srcs to any ip:port in // Dsts. type Match struct { - Dsts []NetPortRange - Srcs []netaddr.IPPrefix + IPProto []ipproto.Proto // required set (no default value at this layer) + Dsts []NetPortRange + Srcs []netaddr.IPPrefix } func (m Match) String() string { + // TODO(bradfitz): use strings.Builder, add String tests srcs := []string{} for _, src := range m.Srcs { srcs = append(srcs, src.String()) @@ -72,13 +75,16 @@ func (m Match) String() string { } else { ds = "[" + strings.Join(dsts, ",") + "]" } - return fmt.Sprintf("%v=>%v", ss, ds) + return fmt.Sprintf("%v%v=>%v", m.IPProto, ss, ds) } type matches []Match func (ms matches) match(q *packet.Parsed) bool { for _, m := range ms { + if !protoInList(q.IPProto, m.IPProto) { + continue + } if !ipInList(q.Src.IP, m.Srcs) { continue } @@ -117,3 +123,12 @@ func ipInList(ip netaddr.IP, netlist []netaddr.IPPrefix) bool { } return false } + +func protoInList(proto ipproto.Proto, valid []ipproto.Proto) bool { + for _, v := range valid { + if proto == v { + return true + } + } + return false +} diff --git a/wgengine/filter/match_clone.go b/wgengine/filter/match_clone.go index 571664bd5..04874ddec 100644 --- a/wgengine/filter/match_clone.go +++ b/wgengine/filter/match_clone.go @@ -8,6 +8,7 @@ package filter import ( "inet.af/netaddr" + "tailscale.com/types/ipproto" ) // Clone makes a deep copy of Match. @@ -18,6 +19,7 @@ func (src *Match) Clone() *Match { } dst := new(Match) *dst = *src + dst.IPProto = append(src.IPProto[:0:0], src.IPProto...) dst.Dsts = append(src.Dsts[:0:0], src.Dsts...) dst.Srcs = append(src.Srcs[:0:0], src.Srcs...) return dst @@ -26,6 +28,7 @@ func (src *Match) Clone() *Match { // A compilation failure here means this code must be regenerated, with command: // tailscale.com/cmd/cloner -type Match var _MatchNeedsRegeneration = Match(struct { - Dsts []NetPortRange - Srcs []netaddr.IPPrefix + IPProto []ipproto.Proto + Dsts []NetPortRange + Srcs []netaddr.IPPrefix }{}) diff --git a/wgengine/filter/tailcfg.go b/wgengine/filter/tailcfg.go index 2f20cdb61..1338a75b4 100644 --- a/wgengine/filter/tailcfg.go +++ b/wgengine/filter/tailcfg.go @@ -10,8 +10,16 @@ import ( "inet.af/netaddr" "tailscale.com/tailcfg" + "tailscale.com/types/ipproto" ) +var defaultProtos = []ipproto.Proto{ + ipproto.TCP, + ipproto.UDP, + ipproto.ICMPv4, + ipproto.ICMPv6, +} + // MatchesFromFilterRules converts tailcfg FilterRules into Matches. // If an error is returned, the Matches result is still valid, // containing the rules that were successfully converted. @@ -22,6 +30,17 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) { for _, r := range pf { m := Match{} + if len(r.IPProto) == 0 { + m.IPProto = append([]ipproto.Proto(nil), defaultProtos...) + } else { + m.IPProto = make([]ipproto.Proto, 0, len(r.IPProto)) + for _, n := range r.IPProto { + if n >= 0 && n <= 0xff { + m.IPProto = append(m.IPProto, ipproto.Proto(n)) + } + } + } + for i, s := range r.SrcIPs { var bits *int if len(r.SrcBits) > i { diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 917e55828..7a437f9e8 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -630,7 +630,7 @@ func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (chan delete(c.onEndpointRefreshed, de) } - if stringsEqual(endpoints, c.lastEndpoints) { + if stringSetsEqual(endpoints, c.lastEndpoints) { return false } c.lastEndpoints = endpoints @@ -814,46 +814,6 @@ 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 *netmap.NetworkMap, ip netaddr.IP) (n *tailcfg.Node, ok bool) { - if nm == nil { - return nil, false - } - // Check for exact matches before looking for subnet matches. - for _, p := range nm.Peers { - for _, a := range p.Addresses { - if a.IP == ip { - return p, true - } - } - } - - // TODO(bradfitz): this is O(n peers). Add ART to netaddr? - var best netaddr.IPPrefix - for _, p := range nm.Peers { - for _, cidr := range p.AllowedIPs { - if cidr.Contains(ip) { - if best.IsZero() || cidr.Bits > best.Bits { - n = p - best = cidr - } - } - } - } - return n, n != nil -} - -// PeerForIP returns the node that ip should route to. -func (c *Conn) PeerForIP(ip netaddr.IP) (n *tailcfg.Node, ok bool) { - c.mu.Lock() - defer c.mu.Unlock() - if c.netMap == nil { - return - } - return peerForIP(c.netMap, ip) -} - // LastRecvActivityOfDisco returns the time we last got traffic from // this endpoint (updated every ~10 seconds). func (c *Conn) LastRecvActivityOfDisco(dk tailcfg.DiscoKey) time.Time { @@ -871,21 +831,14 @@ func (c *Conn) LastRecvActivityOfDisco(dk tailcfg.DiscoKey) time.Time { } // Ping handles a "tailscale ping" CLI query. -func (c *Conn) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { +func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { c.mu.Lock() defer c.mu.Unlock() - res := &ipnstate.PingResult{IP: ip.String()} if c.privateKey.IsZero() { res.Err = "local tailscaled stopped" cb(res) return } - peer, ok := peerForIP(c.netMap, ip) - if !ok { - res.Err = "no matching peer" - cb(res) - return - } if len(peer.Addresses) > 0 { res.NodeIP = peer.Addresses[0].IP.String() } @@ -1111,12 +1064,32 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason return eps, already, nil } -func stringsEqual(x, y []string) bool { - if len(x) != len(y) { - return false +// stringSetsEqual reports whether x and y represent the same set of +// strings. The order doesn't matter. +// +// It does not mutate the slices. +func stringSetsEqual(x, y []string) bool { + if len(x) == len(y) { + orderMatches := true + for i := range x { + if x[i] != y[i] { + orderMatches = false + break + } + } + if orderMatches { + return true + } } - for i := range x { - if x[i] != y[i] { + m := map[string]int{} + for _, v := range x { + m[v] |= 1 + } + for _, v := range y { + m[v] |= 2 + } + for _, n := range m { + if n != 3 { return false } } @@ -2034,7 +2007,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) (isDiscoMsg bo return } if de != nil { - c.logf("magicsock: disco: %v<-%v (%v, %v) got call-me-maybe, %d endpoints", + c.logf("[v1] magicsock: disco: %v<-%v (%v, %v) got call-me-maybe, %d endpoints", c.discoShort, de.discoShort, de.publicKey.ShortString(), derpStr(src.String()), len(dm.MyNumber)) @@ -3012,25 +2985,7 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { c.mu.Lock() defer c.mu.Unlock() - ss := &ipnstate.PeerStatus{ - PublicKey: c.privateKey.Public(), - Addrs: c.lastEndpoints, - OS: version.OS(), - } - if c.netMap != nil { - ss.HostName = c.netMap.Hostinfo.Hostname - ss.DNSName = c.netMap.Name - ss.UserID = c.netMap.User - } else { - ss.HostName, _ = os.Hostname() - } - if c.derpMap != nil { - derpRegion, ok := c.derpMap.Regions[c.myDerp] - if ok { - ss.Relay = derpRegion.RegionCode - } - } - + var tailAddr string if c.netMap != nil { for _, addr := range c.netMap.Addresses { if !addr.IsSingleIP() { @@ -3041,11 +2996,30 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { // readability of `tailscale status`, make it the IPv4 // address. if addr.IP.Is4() { - ss.TailAddr = addr.IP.String() + tailAddr = addr.IP.String() } } } - sb.SetSelfStatus(ss) + + sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) { + ss.PublicKey = c.privateKey.Public() + ss.Addrs = c.lastEndpoints + ss.OS = version.OS() + if c.netMap != nil { + ss.HostName = c.netMap.Hostinfo.Hostname + ss.DNSName = c.netMap.Name + ss.UserID = c.netMap.User + } else { + ss.HostName, _ = os.Hostname() + } + if c.derpMap != nil { + derpRegion, ok := c.derpMap.Regions[c.myDerp] + if ok { + ss.Relay = derpRegion.RegionCode + } + } + ss.TailAddr = tailAddr + }) for dk, n := range c.nodeOfDisco { ps := &ipnstate.PeerStatus{InMagicSock: true} @@ -3106,10 +3080,9 @@ type discoEndpoint struct { lastFullPing time.Time // last time we pinged all endpoints derpAddr netaddr.IPPort // fallback/bootstrap path, if non-zero (non-zero for well-behaved clients) - bestAddr netaddr.IPPort // best non-DERP path; zero if none - bestAddrLatency time.Duration - bestAddrAt time.Time // time best address re-confirmed - trustBestAddrUntil time.Time // time when bestAddr expires + bestAddr addrLatency // best non-DERP path; zero if none + bestAddrAt time.Time // time best address re-confirmed + trustBestAddrUntil time.Time // time when bestAddr expires sentPing map[stun.TxID]sentPing endpointState map[netaddr.IPPort]*endpointState isCallMeMaybeEP map[netaddr.IPPort]bool @@ -3214,8 +3187,8 @@ func (st *endpointState) shouldDeleteLocked() bool { func (de *discoEndpoint) deleteEndpointLocked(ep netaddr.IPPort) { delete(de.endpointState, ep) - if de.bestAddr == ep { - de.bestAddr = netaddr.IPPort{} + if de.bestAddr.IPPort == ep { + de.bestAddr = addrLatency{} } } @@ -3283,7 +3256,7 @@ func (de *discoEndpoint) DstToBytes() []byte { return packIPPort(de.fakeWGAddr) // // de.mu must be held. func (de *discoEndpoint) addrForSendLocked(now time.Time) (udpAddr, derpAddr netaddr.IPPort) { - udpAddr = de.bestAddr + udpAddr = de.bestAddr.IPPort if udpAddr.IsZero() || now.After(de.trustBestAddrUntil) { // We had a bestAddr but it expired so send both to it // and DERP. @@ -3336,7 +3309,7 @@ func (de *discoEndpoint) wantFullPingLocked(now time.Time) bool { if now.After(de.trustBestAddrUntil) { return true } - if de.bestAddrLatency <= goodEnoughLatency { + if de.bestAddr.latency <= goodEnoughLatency { return false } if now.Sub(de.lastFullPing) >= upgradeInterval { @@ -3589,7 +3562,7 @@ func (de *discoEndpoint) addCandidateEndpoint(ep netaddr.IPPort) { } // Newly discovered endpoint. Exciting! - de.c.logf("magicsock: disco: adding %v as candidate endpoint for %v (%s)", ep, de.discoShort, de.publicKey.ShortString()) + de.c.logf("[v1] magicsock: disco: adding %v as candidate endpoint for %v (%s)", ep, de.discoShort, de.publicKey.ShortString()) de.endpointState[ep] = &endpointState{ lastGotPing: time.Now(), } @@ -3602,7 +3575,7 @@ func (de *discoEndpoint) addCandidateEndpoint(ep netaddr.IPPort) { } } size2 := len(de.endpointState) - de.c.logf("magicsock: disco: addCandidateEndpoint pruned %v candidate set from %v to %v entries", size, size2) + de.c.logf("[v1] magicsock: disco: addCandidateEndpoint pruned %v candidate set from %v to %v entries", size, size2) } } @@ -3668,20 +3641,50 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort) // Promote this pong response to our current best address if it's lower latency. // TODO(bradfitz): decide how latency vs. preference order affects decision if !isDerp { - if de.bestAddr.IsZero() || latency < de.bestAddrLatency { - if de.bestAddr != sp.to { - de.c.logf("magicsock: disco: node %v %v now using %v", de.publicKey.ShortString(), de.discoShort, sp.to) - de.bestAddr = sp.to - } + thisPong := addrLatency{sp.to, latency} + if betterAddr(thisPong, de.bestAddr) { + de.c.logf("magicsock: disco: node %v %v now using %v", de.publicKey.ShortString(), de.discoShort, sp.to) + de.bestAddr = thisPong } - if de.bestAddr == sp.to { - de.bestAddrLatency = latency + if de.bestAddr.IPPort == thisPong.IPPort { + de.bestAddr.latency = latency de.bestAddrAt = now de.trustBestAddrUntil = now.Add(trustUDPAddrDuration) } } } +// addrLatency is an IPPort with an associated latency. +type addrLatency struct { + netaddr.IPPort + latency time.Duration +} + +// betterAddr reports whether a is a better addr to use than b. +func betterAddr(a, b addrLatency) bool { + if a.IPPort == b.IPPort { + return false + } + if b.IsZero() { + return true + } + if a.IsZero() { + return false + } + if a.IP.Is6() && b.IP.Is4() { + // Prefer IPv6 for being a bit more robust, as long as + // the latencies are roughly equivalent. + if a.latency/10*9 < b.latency { + return true + } + } else if a.IP.Is4() && b.IP.Is6() { + if betterAddr(b, a) { + return false + } + } + return a.latency < b.latency +} + // discoEndpoint.mu must be held. func (st *endpointState) addPongReplyLocked(r pongReply) { if n := len(st.recentPongs); n < pongHistoryCount { @@ -3729,7 +3732,7 @@ func (de *discoEndpoint) handleCallMeMaybe(m *disco.CallMeMaybe) { } } if len(newEPs) > 0 { - de.c.logf("magicsock: disco: call-me-maybe from %v %v added new endpoints: %v", + de.c.logf("[v1] magicsock: disco: call-me-maybe from %v %v added new endpoints: %v", de.publicKey.ShortString(), de.discoShort, logger.ArgWriter(func(w *bufio.Writer) { for i, ep := range newEPs { @@ -3788,8 +3791,7 @@ func (de *discoEndpoint) stopAndReset() { // state isn't a mix of before & after two sessions. de.lastSend = time.Time{} de.lastFullPing = time.Time{} - de.bestAddr = netaddr.IPPort{} - de.bestAddrLatency = 0 + de.bestAddr = addrLatency{} de.bestAddrAt = time.Time{} de.trustBestAddrUntil = time.Time{} for _, es := range de.endpointState { diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 8e64a2696..2e7e29635 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -37,6 +37,7 @@ import ( "tailscale.com/derp/derpmap" "tailscale.com/ipn/ipnstate" "tailscale.com/net/stun/stuntest" + "tailscale.com/net/tstun" "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/tstest/natlab" @@ -47,7 +48,6 @@ import ( "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" @@ -130,7 +130,7 @@ type magicStack struct { epCh chan []string // endpoint updates produced by this peer conn *Conn // the magicsock itself tun *tuntest.ChannelTUN // TUN device to send/receive packets - tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks + tsTun *tstun.Wrapper // wrapped tun that implements filtering and wgengine hooks dev *device.Device // the wireguard-go Device that connects the previous things wgLogger *wglog.Logger // wireguard-go log wrapper } @@ -166,16 +166,16 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der } tun := tuntest.NewChannelTUN() - tsTun := tstun.WrapTUN(logf, tun.TUN()) + tsTun := tstun.Wrap(logf, tun.TUN()) tsTun.SetFilter(filter.NewAllowAllForTest(logf)) wgLogger := wglog.NewLogger(logf) - dev := device.NewDevice(tsTun, &device.DeviceOptions{ - Logger: wgLogger.DeviceLogger, + opts := &device.DeviceOptions{ CreateEndpoint: conn.CreateEndpoint, CreateBind: conn.CreateBind, SkipBindUpdate: true, - }) + } + dev := device.NewDevice(tsTun, wgLogger.DeviceLogger, opts) dev.Up() // Wait for magicsock to connect up to DERP. @@ -522,12 +522,13 @@ func TestDeviceStartStop(t *testing.T) { defer conn.Close() tun := tuntest.NewChannelTUN() - dev := device.NewDevice(tun.TUN(), &device.DeviceOptions{ - Logger: wglog.NewLogger(t.Logf).DeviceLogger, + wgLogger := wglog.NewLogger(t.Logf) + opts := &device.DeviceOptions{ CreateEndpoint: conn.CreateEndpoint, CreateBind: conn.CreateBind, SkipBindUpdate: true, - }) + } + dev := device.NewDevice(tun.TUN(), wgLogger.DeviceLogger, opts) dev.Up() dev.Close() } @@ -1431,7 +1432,7 @@ func TestDerpReceiveFromIPv4(t *testing.T) { t.Fatal(err) } defer sendConn.Close() - nodeKey, _ := addTestEndpoint(conn, sendConn) + nodeKey, _ := addTestEndpoint(t, conn, sendConn) var sends int = 250e3 // takes about a second if testing.Short() { @@ -1509,7 +1510,7 @@ func TestDerpReceiveFromIPv4(t *testing.T) { // 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) { +func addTestEndpoint(tb testing.TB, 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. @@ -1525,7 +1526,10 @@ func addTestEndpoint(conn *Conn, sendConn net.PacketConn) (tailcfg.NodeKey, tail }, }) conn.SetPrivateKey(wgkey.Private{0: 1}) - conn.CreateEndpoint([32]byte(nodeKey), "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345") + _, err := conn.CreateEndpoint([32]byte(nodeKey), "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345") + if err != nil { + tb.Fatal(err) + } conn.addValidDiscoPathForTest(discoKey, netaddr.MustParseIPPort(sendConn.LocalAddr().String())) return nodeKey, discoKey } @@ -1541,7 +1545,7 @@ func setUpReceiveFrom(tb testing.TB) (roundTrip func()) { } tb.Cleanup(func() { sendConn.Close() }) - addTestEndpoint(conn, sendConn) + addTestEndpoint(tb, conn, sendConn) var dstAddr net.Addr = conn.pconn4.LocalAddr() sendBuf := make([]byte, 1<<10) @@ -1793,3 +1797,114 @@ func TestRebindStress(t *testing.T) { t.Fatalf("Got ReceiveIPv4 error: %v (is closed = %v). Log:\n%s", err, errors.Is(err, net.ErrClosed), logBuf.Bytes()) } } + +func TestStringSetsEqual(t *testing.T) { + s := func(nn ...int) (ret []string) { + for _, n := range nn { + ret = append(ret, strconv.Itoa(n)) + } + return + } + tests := []struct { + a, b []string + want bool + }{ + { + want: true, + }, + { + a: s(1, 2, 3), + b: s(1, 2, 3), + want: true, + }, + { + a: s(1, 2), + b: s(2, 1), + want: true, + }, + { + a: s(1, 2), + b: s(2, 1, 1), + want: true, + }, + { + a: s(1, 2, 2), + b: s(2, 1), + want: true, + }, + { + a: s(1, 2, 2), + b: s(2, 1, 1), + want: true, + }, + { + a: s(1, 2, 2, 3), + b: s(2, 1, 1), + want: false, + }, + { + a: s(1, 2, 2), + b: s(2, 1, 1, 3), + want: false, + }, + } + for _, tt := range tests { + if got := stringSetsEqual(tt.a, tt.b); got != tt.want { + t.Errorf("%q vs %q = %v; want %v", tt.a, tt.b, got, tt.want) + } + } + +} + +func TestBetterAddr(t *testing.T) { + const ms = time.Millisecond + al := func(ipps string, d time.Duration) addrLatency { + return addrLatency{netaddr.MustParseIPPort(ipps), d} + } + zero := addrLatency{} + tests := []struct { + a, b addrLatency + want bool + }{ + {a: zero, b: zero, want: false}, + {a: al("10.0.0.2:123", 5*ms), b: zero, want: true}, + {a: zero, b: al("10.0.0.2:123", 5*ms), want: false}, + {a: al("10.0.0.2:123", 5*ms), b: al("1.2.3.4:555", 6*ms), want: true}, + {a: al("10.0.0.2:123", 5*ms), b: al("10.0.0.2:123", 10*ms), want: false}, // same IPPort + + // Prefer IPv6 if roughly equivalent: + { + a: al("[2001::5]:123", 100*ms), + b: al("1.2.3.4:555", 91*ms), + want: true, + }, + { + a: al("1.2.3.4:555", 91*ms), + b: al("[2001::5]:123", 100*ms), + want: false, + }, + // But not if IPv4 is much faster: + { + a: al("[2001::5]:123", 100*ms), + b: al("1.2.3.4:555", 30*ms), + want: false, + }, + { + a: al("1.2.3.4:555", 30*ms), + b: al("[2001::5]:123", 100*ms), + want: true, + }, + } + for _, tt := range tests { + got := betterAddr(tt.a, tt.b) + if got != tt.want { + t.Errorf("betterAddr(%+v, %+v) = %v; want %v", tt.a, tt.b, got, tt.want) + continue + } + gotBack := betterAddr(tt.b, tt.a) + if got && gotBack { + t.Errorf("betterAddr(%+v, %+v) and betterAddr(%+v, %+v) both unexpectedly true", tt.a, tt.b, tt.b, tt.a) + } + } + +} diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 254df7cb6..8ee7087ce 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -10,6 +10,7 @@ package monitor import ( "encoding/json" "errors" + "runtime" "sync" "time" @@ -18,6 +19,13 @@ import ( "tailscale.com/types/logger" ) +// pollWallTimeInterval is how often we check the time to check +// for big jumps in wall (non-monotonic) time as a backup mechanism +// to get notified of a sleeping device waking back up. +// Usually there are also minor network change events on wake that let +// us check the wall time sooner than this. +const pollWallTimeInterval = 15 * time.Second + // message represents a message returned from an osMon. type message interface { // Ignore is whether we should ignore this message. @@ -50,18 +58,20 @@ type Mon struct { logf logger.Logf om osMon // nil means not supported on this platform change chan struct{} - stop chan struct{} + stop chan struct{} // closed on Stop - mu sync.Mutex // guards cbs - cbs map[*callbackHandle]ChangeFunc - ifState *interfaces.State - gwValid bool // whether gw and gwSelfIP are valid (cached)x - gw netaddr.IP - gwSelfIP netaddr.IP - - onceStart sync.Once + mu sync.Mutex // guards all following fields + cbs map[*callbackHandle]ChangeFunc + ifState *interfaces.State + gwValid bool // whether gw and gwSelfIP are valid + gw netaddr.IP // our gateway's IP + gwSelfIP netaddr.IP // our own IP address (that corresponds to gw) started bool + closed bool goroutines sync.WaitGroup + wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick + lastWall time.Time + timeJumped bool // whether we need to send a changed=true after a big time jump } // New instantiates and starts a monitoring instance. @@ -70,10 +80,11 @@ type Mon struct { func New(logf logger.Logf) (*Mon, error) { logf = logger.WithPrefix(logf, "monitor: ") m := &Mon{ - logf: logf, - cbs: map[*callbackHandle]ChangeFunc{}, - change: make(chan struct{}, 1), - stop: make(chan struct{}), + logf: logf, + cbs: map[*callbackHandle]ChangeFunc{}, + change: make(chan struct{}, 1), + stop: make(chan struct{}), + lastWall: wallTime(), } st, err := m.interfaceStateUncached() if err != nil { @@ -101,12 +112,7 @@ func (m *Mon) InterfaceState() *interfaces.State { } func (m *Mon) interfaceStateUncached() (*interfaces.State, error) { - s, err := interfaces.GetState() - if s != nil { - s.RemoveTailscaleInterfaces() - s.RemoveUninterestingInterfacesAndAddresses() - } - return s, err + return interfaces.GetState() } // GatewayAndSelfIP returns the current network's default gateway, and @@ -145,28 +151,54 @@ func (m *Mon) RegisterChangeCallback(callback ChangeFunc) (unregister func()) { // Start starts the monitor. // A monitor can only be started & closed once. func (m *Mon) Start() { - m.onceStart.Do(func() { - if m.om == nil { - return - } - m.started = true - m.goroutines.Add(2) - go m.pump() - go m.debounce() - }) + m.mu.Lock() + defer m.mu.Unlock() + if m.started || m.closed { + return + } + m.started = true + + switch runtime.GOOS { + case "ios", "android": + // For battery reasons, and because these platforms + // don't really sleep in the same way, don't poll + // for the wall time to detect for wake-for-sleep + // walltime jumps. + default: + m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime) + } + + if m.om == nil { + return + } + m.goroutines.Add(2) + go m.pump() + go m.debounce() } // Close closes the monitor. -// It may only be called once. func (m *Mon) Close() error { + m.mu.Lock() + if m.closed { + m.mu.Unlock() + return nil + } + m.closed = true close(m.stop) + + if m.wallTimer != nil { + m.wallTimer.Stop() + } + var err error if m.om != nil { err = m.om.Close() } - // If it was previously started, wait for those goroutines to finish. - m.onceStart.Do(func() {}) - if m.started { + + started := m.started + m.mu.Unlock() + + if started { m.goroutines.Wait() } return err @@ -232,9 +264,17 @@ func (m *Mon) debounce() { m.logf("interfaces.State: %v", err) } else { m.mu.Lock() + + // See if we have a queued or new time jump signal. + m.checkWallTimeAdvanceLocked() + timeJumped := m.timeJumped + if timeJumped { + m.logf("time jumped (probably wake from sleep); synthesizing major change event") + } + oldState := m.ifState - changed := !curState.Equal(oldState) - if changed { + ifChanged := !curState.EqualFiltered(oldState, interfaces.FilterInteresting) + if ifChanged { m.gwValid = false m.ifState = curState @@ -243,6 +283,10 @@ func (m *Mon) debounce() { jsonSummary(oldState), jsonSummary(curState)) } } + changed := ifChanged || timeJumped + if changed { + m.timeJumped = false + } for _, cb := range m.cbs { go cb(changed, m.ifState) } @@ -264,3 +308,33 @@ func jsonSummary(x interface{}) interface{} { } return j } + +func wallTime() time.Time { + // From time package's docs: "The canonical way to strip a + // monotonic clock reading is to use t = t.Round(0)." + return time.Now().Round(0) +} + +func (m *Mon) pollWallTime() { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return + } + m.checkWallTimeAdvanceLocked() + if m.timeJumped { + m.InjectEvent() + } + m.wallTimer.Reset(pollWallTimeInterval) +} + +// checkWallTimeAdvanceLocked updates m.timeJumped, if wall time jumped +// more than 150% of pollWallTimeInterval, indicating we probably just +// came out of sleep. +func (m *Mon) checkWallTimeAdvanceLocked() { + now := wallTime() + if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 { + m.timeJumped = true + } + m.lastWall = now +} diff --git a/wgengine/monitor/monitor_polling.go b/wgengine/monitor/monitor_polling.go index 079c956bb..cdc995ca4 100644 --- a/wgengine/monitor/monitor_polling.go +++ b/wgengine/monitor/monitor_polling.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "tailscale.com/net/interfaces" "tailscale.com/types/logger" ) @@ -53,7 +54,7 @@ func (pm *pollingMon) Receive() (message, error) { defer ticker.Stop() base := pm.m.InterfaceState() for { - if cur, err := pm.m.interfaceStateUncached(); err == nil && !cur.Equal(base) { + if cur, err := pm.m.interfaceStateUncached(); err == nil && !cur.EqualFiltered(base, interfaces.FilterInteresting) { return unspecifiedMessage{}, nil } select { diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index ad647fa6f..92881decd 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -32,13 +32,13 @@ import ( "inet.af/netstack/waiter" "tailscale.com/net/packet" "tailscale.com/net/tsaddr" + "tailscale.com/net/tstun" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/util/dnsname" "tailscale.com/wgengine" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" - "tailscale.com/wgengine/tstun" ) const debugNetstack = false @@ -49,7 +49,7 @@ const debugNetstack = false type Impl struct { ipstack *stack.Stack linkEP *channel.Endpoint - tundev *tstun.TUN + tundev *tstun.Wrapper e wgengine.Engine mc *magicsock.Conn logf logger.Logf @@ -67,7 +67,7 @@ const nicID = 1 const mtu = 1500 // Create creates and populates a new Impl. -func Create(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) (*Impl, error) { +func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn) (*Impl, error) { if mc == nil { return nil, errors.New("nil magicsock.Conn") } @@ -300,7 +300,7 @@ func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil } - // No Magic DNS name so try real DNS. + // No MagicDNS name so try real DNS. var r net.Resolver ips, err := r.LookupIP(ctx, "ip", host) if err != nil { @@ -363,7 +363,7 @@ func (ns *Impl) injectOutbound() { } } -func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.TUN) filter.Response { +func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response { var pn tcpip.NetworkProtocolNumber switch p.IPVersion { case 4: diff --git a/wgengine/pendopen.go b/wgengine/pendopen.go index be1fa1468..2951a0c7e 100644 --- a/wgengine/pendopen.go +++ b/wgengine/pendopen.go @@ -14,8 +14,9 @@ import ( "tailscale.com/net/flowtrack" "tailscale.com/net/packet" "tailscale.com/net/tsaddr" + "tailscale.com/net/tstun" + "tailscale.com/types/ipproto" "tailscale.com/wgengine/filter" - "tailscale.com/wgengine/tstun" ) const tcpTimeoutBeforeDebug = 5 * time.Second @@ -65,10 +66,10 @@ func (e *userspaceEngine) noteFlowProblemFromPeer(f flowtrack.Tuple, problem pac of.problem = problem } -func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) { +func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.Wrapper) (res filter.Response) { res = filter.Accept // always - if pp.IPProto == packet.TSMP { + if pp.IPProto == ipproto.TSMP { res = filter.DropSilently rh, ok := pp.AsTailscaleRejectedHeader() if !ok { @@ -83,14 +84,14 @@ func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) } if pp.IPVersion == 0 || - pp.IPProto != packet.TCP || + pp.IPProto != ipproto.TCP || pp.TCPFlags&(packet.TCPSyn|packet.TCPRst) == 0 { return } // Either a SYN or a RST came back. Remove it in either case. - f := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed + f := flowtrack.Tuple{Proto: pp.IPProto, Dst: pp.Src, Src: pp.Dst} // src/dst reversed removed := e.removeFlow(f) if removed && pp.TCPFlags&packet.TCPRst != 0 { e.logf("open-conn-track: flow TCP %v got RST by peer", f) @@ -98,23 +99,23 @@ func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) return } -func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) { +func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wrapper) (res filter.Response) { res = filter.Accept // always if pp.IPVersion == 0 || - pp.IPProto != packet.TCP || + pp.IPProto != ipproto.TCP || pp.TCPFlags&packet.TCPSyn == 0 { return } - flow := flowtrack.Tuple{Src: pp.Src, Dst: pp.Dst} + flow := flowtrack.Tuple{Proto: pp.IPProto, Src: pp.Src, Dst: pp.Dst} // iOS likes to probe Apple IPs on all interfaces to check for connectivity. // Don't start timers tracking those. They won't succeed anyway. Avoids log spam // like: // open-conn-track: timeout opening (100.115.73.60:52501 => 17.125.252.5:443); no associated peer node if runtime.GOOS == "ios" && flow.Dst.Port == 443 && !tsaddr.IsTailscaleIP(flow.Dst.IP) { - if _, ok := e.magicConn.PeerForIP(flow.Dst.IP); !ok { + if _, err := e.peerForIP(flow.Dst.IP); err != nil { return } } @@ -154,8 +155,12 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { } // Diagnose why it might've timed out. - n, ok := e.magicConn.PeerForIP(flow.Dst.IP) - if !ok { + n, err := e.peerForIP(flow.Dst.IP) + if err != nil { + e.logf("open-conn-track: timeout opening %v; peerForIP: %v", flow, err) + return + } + if n == nil { e.logf("open-conn-track: timeout opening %v; no associated peer node", flow) return } diff --git a/wgengine/router/router.go b/wgengine/router/router.go index 9c3f1003f..2e53363cf 100644 --- a/wgengine/router/router.go +++ b/wgengine/router/router.go @@ -7,12 +7,11 @@ package router import ( - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" + "tailscale.com/net/dns" "tailscale.com/types/logger" "tailscale.com/types/preftype" - "tailscale.com/wgengine/router/dns" ) // Router is responsible for managing the system network stack. @@ -33,9 +32,9 @@ type Router interface { // New returns a new Router for the current platform, using the // provided tun device. -func New(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) { +func New(logf logger.Logf, tundev tun.Device) (Router, error) { logf = logger.WithPrefix(logf, "router: ") - return newUserspaceRouter(logf, wgdev, tundev) + return newUserspaceRouter(logf, tundev) } // Cleanup restores the system network configuration to its original state diff --git a/wgengine/router/router_darwin.go b/wgengine/router/router_darwin.go index 26b689355..58ba8e6d3 100644 --- a/wgengine/router/router_darwin.go +++ b/wgengine/router/router_darwin.go @@ -5,13 +5,12 @@ package router import ( - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "tailscale.com/types/logger" ) -func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) { - return newUserspaceBSDRouter(logf, wgdev, tundev) +func newUserspaceRouter(logf logger.Logf, tundev tun.Device) (Router, error) { + return newUserspaceBSDRouter(logf, tundev) } func cleanup(logger.Logf, string) { diff --git a/wgengine/router/router_default.go b/wgengine/router/router_default.go index 4d7365e04..7f05da42f 100644 --- a/wgengine/router/router_default.go +++ b/wgengine/router/router_default.go @@ -7,13 +7,12 @@ package router import ( - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "tailscale.com/types/logger" ) -func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tunDev tun.Device, netChanged func()) Router { - return NewFakeRouter(logf, tunname, dev, tunDev, netChanged) +func newUserspaceRouter(logf logger.Logf, tunname string, tunDev tun.Device, netChanged func()) Router { + return NewFakeRouter(logf, tunname, tunDev, netChanged) } func cleanup(logf logger.Logf, interfaceName string) { diff --git a/wgengine/router/router_fake.go b/wgengine/router/router_fake.go index 0d14e5000..add4b576b 100644 --- a/wgengine/router/router_fake.go +++ b/wgengine/router/router_fake.go @@ -5,15 +5,13 @@ package router import ( - "github.com/tailscale/wireguard-go/device" - "github.com/tailscale/wireguard-go/tun" "tailscale.com/types/logger" ) -// NewFakeRouter returns a Router that does nothing when called and -// always returns nil errors. -func NewFake(logf logger.Logf, _ *device.Device, _ tun.Device) (Router, error) { - return fakeRouter{logf: logf}, nil +// NewFake returns a Router that does nothing when called and always +// returns nil errors. +func NewFake(logf logger.Logf) Router { + return fakeRouter{logf: logf} } type fakeRouter struct { diff --git a/wgengine/router/router_freebsd.go b/wgengine/router/router_freebsd.go index e56e3f82d..6e9380299 100644 --- a/wgengine/router/router_freebsd.go +++ b/wgengine/router/router_freebsd.go @@ -5,7 +5,6 @@ package router import ( - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "tailscale.com/types/logger" ) @@ -15,8 +14,8 @@ import ( // Work is currently underway for an in-kernel FreeBSD implementation of wireguard // https://svnweb.freebsd.org/base?view=revision&revision=357986 -func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) { - return newUserspaceBSDRouter(logf, nil, tundev) +func newUserspaceRouter(logf logger.Logf, tundev tun.Device) (Router, error) { + return newUserspaceBSDRouter(logf, tundev) } func cleanup(logf logger.Logf, interfaceName string) { diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index b700efccc..311681ab0 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -16,14 +16,13 @@ import ( "github.com/coreos/go-iptables/iptables" "github.com/go-multierror/multierror" - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" + "tailscale.com/net/dns" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" "tailscale.com/types/preftype" "tailscale.com/version/distro" - "tailscale.com/wgengine/router/dns" ) const ( @@ -110,7 +109,7 @@ type linuxRouter struct { cmd commandRunner } -func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (Router, error) { +func newUserspaceRouter(logf logger.Logf, tunDev tun.Device) (Router, error) { tunname, err := tunDev.Name() if err != nil { return nil, err diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index 7bacb2e2f..45109d35c 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -627,7 +627,7 @@ func TestDelRouteIdempotent(t *testing.T) { } } - r, err := newUserspaceRouter(logf, nil, tun) + r, err := newUserspaceRouter(logf, tun) if err != nil { t.Fatal(err) } diff --git a/wgengine/router/router_openbsd.go b/wgengine/router/router_openbsd.go index 8c7269658..a6dbf9282 100644 --- a/wgengine/router/router_openbsd.go +++ b/wgengine/router/router_openbsd.go @@ -10,11 +10,10 @@ import ( "log" "os/exec" - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" + "tailscale.com/net/dns" "tailscale.com/types/logger" - "tailscale.com/wgengine/router/dns" ) // For now this router only supports the WireGuard userspace implementation. @@ -31,7 +30,7 @@ type openbsdRouter struct { dns *dns.Manager } -func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) { +func newUserspaceRouter(logf logger.Logf, tundev tun.Device) (Router, error) { tunname, err := tundev.Name() if err != nil { return nil, err diff --git a/wgengine/router/router_userspace_bsd.go b/wgengine/router/router_userspace_bsd.go index 71ccd1706..79a81de03 100644 --- a/wgengine/router/router_userspace_bsd.go +++ b/wgengine/router/router_userspace_bsd.go @@ -12,12 +12,11 @@ import ( "os/exec" "runtime" - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "inet.af/netaddr" + "tailscale.com/net/dns" "tailscale.com/types/logger" "tailscale.com/version" - "tailscale.com/wgengine/router/dns" ) type userspaceBSDRouter struct { @@ -29,7 +28,7 @@ type userspaceBSDRouter struct { dns *dns.Manager } -func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) { +func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device) (Router, error) { tunname, err := tundev.Name() if err != nil { return nil, err diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go index 89c686b95..2efdce7ed 100644 --- a/wgengine/router/router_windows.go +++ b/wgengine/router/router_windows.go @@ -16,21 +16,19 @@ import ( "syscall" "time" - "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "inet.af/netaddr" "tailscale.com/logtail/backoff" + "tailscale.com/net/dns" "tailscale.com/types/logger" - "tailscale.com/wgengine/router/dns" ) type winRouter struct { logf func(fmt string, args ...interface{}) tunname string nativeTun *tun.NativeTun - wgdev *device.Device routeChangeCallback *winipcfg.RouteChangeCallback dns *dns.Manager firewall *firewallTweaker @@ -45,7 +43,7 @@ type winRouter struct { firewallSubproc *exec.Cmd } -func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) { +func newUserspaceRouter(logf logger.Logf, tundev tun.Device) (Router, error) { tunname, err := tundev.Name() if err != nil { return nil, err @@ -65,7 +63,6 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic return &winRouter{ logf: logf, - wgdev: wgdev, tunname: tunname, nativeTun: nativeTun, dns: dns.NewManager(mconfig), @@ -112,11 +109,8 @@ func (r *winRouter) Set(cfg *Config) error { } // Flush DNS on router config change to clear cached DNS entries (solves #1430) - out, err := exec.Command("ipconfig", "/flushdns").CombinedOutput() - if err != nil { - r.logf("flushdns error: %v; output: %s", err, out) - } else { - r.logf("flushdns successful") + if err := dns.Flush(); err != nil { + r.logf("flushdns error: %v", err) } return nil diff --git a/wgengine/userspace.go b/wgengine/userspace.go index fccca8c8b..9202e4c7a 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -8,12 +8,12 @@ import ( "bufio" "bytes" "context" + crand "crypto/rand" "errors" "fmt" "io" "net" "os" - "os/exec" "runtime" "strconv" "strings" @@ -29,38 +29,28 @@ import ( "tailscale.com/health" "tailscale.com/internal/deepprint" "tailscale.com/ipn/ipnstate" + "tailscale.com/net/dns" "tailscale.com/net/flowtrack" "tailscale.com/net/interfaces" "tailscale.com/net/packet" "tailscale.com/net/tsaddr" "tailscale.com/net/tshttpproxy" + "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/types/ipproto" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/types/wgkey" "tailscale.com/version" - "tailscale.com/version/distro" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/tsdns" - "tailscale.com/wgengine/tstun" "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wglog" ) -// minimalMTU is the MTU we set on tailscale's TUN -// interface. wireguard-go defaults to 1420 bytes, which only works if -// the "outer" MTU is 1500 bytes. This breaks on DSL connections -// (typically 1492 MTU) and on GCE (1460 MTU?!). -// -// 1280 is the smallest MTU allowed for IPv6, which is a sensible -// "probably works everywhere" setting until we develop proper PMTU -// discovery. -const minimalMTU = 1280 - const magicDNSPort = 53 var magicDNSIP = netaddr.IPv4(100, 100, 100, 100) @@ -90,10 +80,10 @@ type userspaceEngine struct { reqCh chan struct{} waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool timeNow func() time.Time - tundev *tstun.TUN + tundev *tstun.Wrapper wgdev *device.Device router router.Router - resolver *tsdns.Resolver + resolver *dns.Resolver magicConn *magicsock.Conn linkMon *monitor.Mon linkMonOwned bool // whether we created linkMon (and thus need to close it) @@ -101,10 +91,10 @@ type userspaceEngine struct { testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called - // localAddrs is the set of IP addresses assigned to the local + // isLocalAddr reports the whether an IP is assigned to the local // tunnel interface. It's used to reflect local packets // incorrectly sent to us. - localAddrs atomic.Value // of map[netaddr.IP]bool + isLocalAddr atomic.Value // of func(netaddr.IP)bool wgLock sync.Mutex // serializes all wgdev operations; see lock order comment below lastCfgFull wgcfg.Config @@ -117,8 +107,9 @@ type userspaceEngine struct { destIPActivityFuncs map[netaddr.IP]func() statusBufioReader *bufio.Reader // reusable for UAPI - mu sync.Mutex // guards following; see lock order comment below - closing bool // Close was called (even if we're still closing) + mu sync.Mutex // guards following; see lock order comment below + netMap *netmap.NetworkMap // or nil + closing bool // Close was called (even if we're still closing) statusCallback StatusCallback peerSequence []wgkey.Key endpoints []string @@ -126,36 +117,30 @@ type userspaceEngine struct { pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go networkMapCallbacks map[*someHandle]NetworkMapCallback tsIPByIPPort map[netaddr.IPPort]netaddr.IP // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups + pongCallback map[[8]byte]func() // for TSMP pong responses // Lock ordering: magicsock.Conn.mu, wgLock, then mu. } // InternalsGetter is implemented by Engines that can export their internals. type InternalsGetter interface { - GetInternals() (*tstun.TUN, *magicsock.Conn) + GetInternals() (*tstun.Wrapper, *magicsock.Conn) } -func (e *userspaceEngine) GetInternals() (*tstun.TUN, *magicsock.Conn) { +func (e *userspaceEngine) GetInternals() (*tstun.Wrapper, *magicsock.Conn) { return e.tundev, e.magicConn } -// RouterGen is the signature for a function that creates a -// router.Router. -type RouterGen func(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (router.Router, error) - // Config is the engine configuration. type Config struct { - // TUN is the TUN device used by the engine. - // Exactly one of either TUN or TUNName must be specified. - TUN tun.Device + // Tun is the device used by the Engine to exchange packets with + // the OS. + // If nil, a fake Device that does nothing is used. + Tun tun.Device - // TUNName is the TUN device to create. - // Exactly one of either TUN or TUNName must be specified. - TUNName string - - // RouterGen is the function used to instantiate the router. - // If nil, wgengine/router.New is used. - RouterGen RouterGen + // Router interfaces the Engine to the OS network stack. + // If nil, a fake Router that does nothing is used. + Router router.Router // LinkMonitor optionally provides an existing link monitor to re-use. // If nil, a new link monitor is created. @@ -165,59 +150,36 @@ type Config struct { // If zero, a port is automatically selected. ListenPort uint16 - // Fake determines whether this engine should automatically - // reply to ICMP pings. - Fake bool + // RespondToPing determines whether this engine should internally + // reply to ICMP pings, without involving the OS. + // Used in "fake" mode for development. + RespondToPing bool } func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) { logf("Starting userspace wireguard engine (with fake TUN device)") return NewUserspaceEngine(logf, Config{ - TUN: tstun.NewFakeTUN(), - RouterGen: router.NewFake, - ListenPort: listenPort, - Fake: true, + ListenPort: listenPort, + RespondToPing: true, }) } // NewUserspaceEngine creates the named tun device and returns a // Tailscale Engine running on it. -func NewUserspaceEngine(logf logger.Logf, conf Config) (Engine, error) { - if conf.TUN != nil && conf.TUNName != "" { - return nil, errors.New("TUN and TUNName are mutually exclusive") - } - if conf.TUN == nil && conf.TUNName == "" { - return nil, errors.New("either TUN or TUNName are required") - } - tunDev := conf.TUN - var err error - if tunName := conf.TUNName; tunName != "" { - logf("Starting userspace wireguard engine with tun device %q", tunName) - tunDev, err = tun.CreateTUN(tunName, minimalMTU) - if err != nil { - diagnoseTUNFailure(tunName, logf) - logf("CreateTUN: %v", err) - return nil, err - } - logf("CreateTUN ok.") - - if err := waitInterfaceUp(tunDev, 90*time.Second, logf); err != nil { - return nil, err - } - } - - if conf.RouterGen == nil { - conf.RouterGen = router.New - } - - return newUserspaceEngine(logf, tunDev, conf) -} - -func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ Engine, reterr error) { +func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) { var closePool closeOnErrorPool defer closePool.closeAllIfError(&reterr) - tsTUNDev := tstun.WrapTUN(logf, rawTUNDev) + if conf.Tun == nil { + logf("[v1] using fake (no-op) tun device") + conf.Tun = tstun.NewFake() + } + if conf.Router == nil { + logf("[v1] using fake (no-op) OS network configurator") + conf.Router = router.NewFake(logf) + } + + tsTUNDev := tstun.Wrap(logf, conf.Tun) closePool.add(tsTUNDev) e := &userspaceEngine{ @@ -226,9 +188,10 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ reqCh: make(chan struct{}, 1), waitCh: make(chan struct{}), tundev: tsTUNDev, + router: conf.Router, pingers: make(map[wgkey.Key]*pinger), } - e.localAddrs.Store(map[netaddr.IP]bool{}) + e.isLocalAddr.Store(genLocalAddrFunc(nil)) if conf.LinkMonitor != nil { e.linkMon = conf.LinkMonitor @@ -242,7 +205,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ e.linkMonOwned = true } - e.resolver = tsdns.NewResolver(tsdns.ResolverConfig{ + e.resolver = dns.NewResolver(dns.ResolverConfig{ Logf: logf, Forward: true, LinkMonitor: e.linkMon, @@ -281,8 +244,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ closePool.add(e.magicConn) e.magicConn.SetNetworkUp(e.linkMon.InterfaceState().AnyInterfaceUp()) - // Respond to all pings only in fake mode. - if conf.Fake { + if conf.RespondToPing { e.tundev.PostFilterIn = echoRespondToAll } e.tundev.PreFilterOut = e.handleLocalPackets @@ -300,7 +262,6 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ e.wgLogger = wglog.NewLogger(logf) opts := &device.DeviceOptions{ - Logger: e.wgLogger.DeviceLogger, HandshakeDone: func(peerKey device.NoisePublicKey, peer *device.Peer, deviceAllowedIPs *device.AllowedIPs) { // Send an unsolicited status event every time a // handshake completes. This makes sure our UI can @@ -349,20 +310,21 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ SkipBindUpdate: true, } + e.tundev.OnTSMPPongReceived = func(data [8]byte) { + e.mu.Lock() + defer e.mu.Unlock() + cb := e.pongCallback[data] + e.logf("wgengine: got TSMP pong %02x; cb=%v", data, cb != nil) + if cb != nil { + go cb() + } + } + // wgdev takes ownership of tundev, will close it when closed. e.logf("Creating wireguard device...") - e.wgdev = device.NewDevice(e.tundev, opts) + e.wgdev = device.NewDevice(e.tundev, e.wgLogger.DeviceLogger, opts) closePool.addFunc(e.wgdev.Close) - // Pass the underlying tun.(*NativeDevice) to the router: - // routers do not Read or Write, but do access native interfaces. - e.logf("Creating router...") - e.router, err = conf.RouterGen(logf, e.wgdev, e.tundev.Unwrap()) - if err != nil { - return nil, err - } - closePool.add(e.router) - go func() { up := false for event := range e.tundev.Events() { @@ -411,7 +373,7 @@ func newUserspaceEngine(logf logger.Logf, rawTUNDev tun.Device, conf Config) (_ } // echoRespondToAll is an inbound post-filter responding to all echo requests. -func echoRespondToAll(p *packet.Parsed, t *tstun.TUN) filter.Response { +func echoRespondToAll(p *packet.Parsed, t *tstun.Wrapper) filter.Response { if p.IsEchoRequest() { header := p.ICMP4Header() header.ToResponse() @@ -432,44 +394,40 @@ func echoRespondToAll(p *packet.Parsed, t *tstun.TUN) filter.Response { // stack, and intercepts any packets that should be handled by // tailscaled directly. Other packets are allowed to proceed into the // main ACL filter. -func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.TUN) filter.Response { +func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Response { if verdict := e.handleDNS(p, t); verdict == filter.Drop { // local DNS handled the packet. return filter.Drop } - if (runtime.GOOS == "darwin" || runtime.GOOS == "ios") && e.isLocalAddr(p.Dst.IP) { - // macOS NetworkExtension directs packets destined to the - // tunnel's local IP address into the tunnel, instead of - // looping back within the kernel network stack. We have to - // notice that an outbound packet is actually destined for - // ourselves, and loop it back into macOS. - t.InjectInboundCopy(p.Buffer()) - return filter.Drop + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + isLocalAddr, ok := e.isLocalAddr.Load().(func(netaddr.IP) bool) + if !ok { + e.logf("[unexpected] e.isLocalAddr was nil, can't check for loopback packet") + } else if isLocalAddr(p.Dst.IP) { + // macOS NetworkExtension directs packets destined to the + // tunnel's local IP address into the tunnel, instead of + // looping back within the kernel network stack. We have to + // notice that an outbound packet is actually destined for + // ourselves, and loop it back into macOS. + t.InjectInboundCopy(p.Buffer()) + return filter.Drop + } } return filter.Accept } -func (e *userspaceEngine) isLocalAddr(ip netaddr.IP) bool { - localAddrs, ok := e.localAddrs.Load().(map[netaddr.IP]bool) - if !ok { - e.logf("[unexpected] e.localAddrs was nil, can't check for loopback packet") - return false - } - return localAddrs[ip] -} - // handleDNS is an outbound pre-filter resolving Tailscale domains. -func (e *userspaceEngine) handleDNS(p *packet.Parsed, t *tstun.TUN) filter.Response { - if p.Dst.IP == magicDNSIP && p.Dst.Port == magicDNSPort && p.IPProto == packet.UDP { - request := tsdns.Packet{ +func (e *userspaceEngine) handleDNS(p *packet.Parsed, t *tstun.Wrapper) filter.Response { + if p.Dst.IP == magicDNSIP && p.Dst.Port == magicDNSPort && p.IPProto == ipproto.UDP { + request := dns.Packet{ Payload: append([]byte(nil), p.Payload()...), Addr: netaddr.IPPort{IP: p.Src.IP, Port: p.Src.Port}, } err := e.resolver.EnqueueRequest(request) if err != nil { - e.logf("tsdns: enqueue: %v", err) + e.logf("dns: enqueue: %v", err) } return filter.Drop } @@ -480,11 +438,11 @@ func (e *userspaceEngine) handleDNS(p *packet.Parsed, t *tstun.TUN) filter.Respo func (e *userspaceEngine) pollResolver() { for { resp, err := e.resolver.NextResponse() - if err == tsdns.ErrClosed { + if err == dns.ErrClosed { return } if err != nil { - e.logf("tsdns: error: %v", err) + e.logf("dns: error: %v", err) continue } @@ -498,7 +456,7 @@ func (e *userspaceEngine) pollResolver() { } hlen := h.Len() - // TODO(dmytro): avoid this allocation without importing tstun quirks into tsdns. + // TODO(dmytro): avoid this allocation without importing tstun quirks into dns. const offset = tstun.PacketStartOffset buf := make([]byte, offset+hlen+len(resp.Payload)) copy(buf[offset+hlen:], resp.Payload) @@ -925,16 +883,34 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey e.tundev.SetDestIPActivityFuncs(e.destIPActivityFuncs) } +// genLocalAddrFunc returns a func that reports whether an IP is in addrs. +// addrs is assumed to be all /32 or /128 entries. +func genLocalAddrFunc(addrs []netaddr.IPPrefix) func(netaddr.IP) bool { + // Specialize the three common cases: no address, just IPv4 + // (or just IPv6), and both IPv4 and IPv6. + if len(addrs) == 0 { + return func(netaddr.IP) bool { return false } + } + if len(addrs) == 1 { + return func(t netaddr.IP) bool { return t == addrs[0].IP } + } + if len(addrs) == 2 { + return func(t netaddr.IP) bool { return t == addrs[0].IP || t == addrs[1].IP } + } + // Otherwise, the general implementation: a map lookup. + m := map[netaddr.IP]bool{} + for _, a := range addrs { + m[a.IP] = true + } + return func(t netaddr.IP) bool { return m[t] } +} + func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config) error { if routerCfg == nil { panic("routerCfg must not be nil") } - localAddrs := map[netaddr.IP]bool{} - for _, addr := range routerCfg.LocalAddrs { - localAddrs[addr.IP] = true - } - e.localAddrs.Store(localAddrs) + e.isLocalAddr.Store(genLocalAddrFunc(routerCfg.LocalAddrs)) e.wgLock.Lock() defer e.wgLock.Unlock() @@ -1034,7 +1010,7 @@ func (e *userspaceEngine) SetFilter(filt *filter.Filter) { e.tundev.SetFilter(filt) } -func (e *userspaceEngine) SetDNSMap(dm *tsdns.Map) { +func (e *userspaceEngine) SetDNSMap(dm *dns.Map) { e.resolver.SetMap(dm) } @@ -1270,6 +1246,7 @@ func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) { e.logf("[v1] LinkChange: minor") } + health.SetAnyInterfaceUp(up) e.magicConn.SetNetworkUp(up) why := "link-change-minor" @@ -1306,6 +1283,7 @@ func (e *userspaceEngine) SetDERPMap(dm *tailcfg.DERPMap) { func (e *userspaceEngine) SetNetworkMap(nm *netmap.NetworkMap) { e.magicConn.SetNetworkMap(nm) e.mu.Lock() + e.netMap = nm callbacks := make([]NetworkMapCallback, 0, 4) for _, fn := range e.networkMapCallbacks { callbacks = append(callbacks, fn) @@ -1338,8 +1316,107 @@ func (e *userspaceEngine) UpdateStatus(sb *ipnstate.StatusBuilder) { e.magicConn.UpdateStatus(sb) } -func (e *userspaceEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { - e.magicConn.Ping(ip, cb) +func (e *userspaceEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) { + res := &ipnstate.PingResult{IP: ip.String()} + peer, err := e.peerForIP(ip) + if err != nil { + e.logf("ping(%v): %v", ip, err) + res.Err = err.Error() + cb(res) + return + } + if peer == nil { + e.logf("ping(%v): no matching peer", ip) + res.Err = "no matching peer" + cb(res) + return + } + pingType := "disco" + if useTSMP { + pingType = "TSMP" + } + e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key.ShortString(), peer.ComputedName) + if useTSMP { + e.sendTSMPPing(ip, peer, res, cb) + } else { + e.magicConn.Ping(peer, res, cb) + } +} + +func (e *userspaceEngine) mySelfIPMatchingFamily(dst netaddr.IP) (src netaddr.IP, err error) { + e.mu.Lock() + defer e.mu.Unlock() + if e.netMap == nil { + return netaddr.IP{}, errors.New("no netmap") + } + for _, a := range e.netMap.Addresses { + if a.IsSingleIP() && a.IP.BitLen() == dst.BitLen() { + return a.IP, nil + } + } + if len(e.netMap.Addresses) == 0 { + return netaddr.IP{}, errors.New("no self address in netmap") + } + return netaddr.IP{}, errors.New("no self address in netmap matching address family") +} + +func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { + srcIP, err := e.mySelfIPMatchingFamily(ip) + if err != nil { + res.Err = err.Error() + cb(res) + return + } + var iph packet.Header + if srcIP.Is4() { + iph = packet.IP4Header{ + IPProto: ipproto.TSMP, + Src: srcIP, + Dst: ip, + } + } else { + iph = packet.IP6Header{ + IPProto: ipproto.TSMP, + Src: srcIP, + Dst: ip, + } + } + + var data [8]byte + crand.Read(data[:]) + + expireTimer := time.AfterFunc(10*time.Second, func() { + e.setTSMPPongCallback(data, nil) + }) + t0 := time.Now() + e.setTSMPPongCallback(data, func() { + expireTimer.Stop() + d := time.Since(t0) + res.LatencySeconds = d.Seconds() + res.NodeIP = ip.String() + res.NodeName = peer.ComputedName + cb(res) + }) + + var tsmpPayload [9]byte + tsmpPayload[0] = byte(packet.TSMPTypePing) + copy(tsmpPayload[1:], data[:]) + + tsmpPing := packet.Generate(iph, tsmpPayload[:]) + e.tundev.InjectOutbound(tsmpPing) +} + +func (e *userspaceEngine) setTSMPPongCallback(data [8]byte, cb func()) { + e.mu.Lock() + defer e.mu.Unlock() + if e.pongCallback == nil { + e.pongCallback = map[[8]byte]func(){} + } + if cb == nil { + delete(e.pongCallback, data) + } else { + e.pongCallback[data] = cb + } } func (e *userspaceEngine) RegisterIPPortIdentity(ipport netaddr.IPPort, tsIP netaddr.IP) { @@ -1367,92 +1444,77 @@ func (e *userspaceEngine) WhoIsIPPort(ipport netaddr.IPPort) (tsIP netaddr.IP, o return tsIP, ok } -// diagnoseTUNFailure is called if tun.CreateTUN fails, to poke around -// 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(tunName string, logf logger.Logf) { - switch runtime.GOOS { - case "linux": - diagnoseLinuxTUNFailure(tunName, logf) - case "darwin": - diagnoseDarwinTUNFailure(tunName, logf) - default: - logf("no TUN failure diagnostics for OS %q", runtime.GOOS) +// peerForIP returns the Node in the wireguard config +// that's responsible for handling the given IP address. +// +// If none is found in the wireguard config but one is found in +// the netmap, it's described in an error. +// +// If none is found in either place, (nil, nil) is returned. +// +// peerForIP acquires both e.mu and e.wgLock, but neither at the same +// time. +func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error) { + e.mu.Lock() + nm := e.netMap + e.mu.Unlock() + if nm == nil { + return nil, errors.New("no network map") } -} -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 { - logf("no TUN, and failed to look up kernel version: %v", err) - return - } - logf("Linux kernel version: %s", kernel) - - modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput() - if err == nil { - logf("'modprobe tun' successful") - // Either tun is currently loaded, or it's statically - // compiled into the kernel (which modprobe checks - // with /lib/modules/$(uname -r)/modules.builtin) - // - // So if there's a problem at this point, it's - // probably because /dev/net/tun doesn't exist. - const dev = "/dev/net/tun" - if fi, err := os.Stat(dev); err != nil { - logf("tun module loaded in kernel, but %s does not exist", dev) - } else { - logf("%s: %v", dev, fi.Mode()) + // Check for exact matches before looking for subnet matches. + var bestInNMPrefix netaddr.IPPrefix + var bestInNM *tailcfg.Node + for _, p := range nm.Peers { + for _, a := range p.Addresses { + if a.IP == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { + return p, nil + } } - - // We failed to find why it failed. Just let our - // caller report the error it got from wireguard-go. - return - } - logf("is CONFIG_TUN enabled in your kernel? `modprobe tun` failed with: %s", modprobeOut) - - switch distro.Get() { - case distro.Debian: - dpkgOut, err := exec.Command("dpkg", "-S", "kernel/drivers/net/tun.ko").CombinedOutput() - if len(bytes.TrimSpace(dpkgOut)) == 0 || err != nil { - logf("tun module not loaded nor found on disk") - return - } - if !bytes.Contains(dpkgOut, kernel) { - logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", dpkgOut) - } - case distro.Arch: - findOut, err := exec.Command("find", "/lib/modules/", "-path", "*/net/tun.ko*").CombinedOutput() - if len(bytes.TrimSpace(findOut)) == 0 || err != nil { - logf("tun module not loaded nor found on disk") - return - } - if !bytes.Contains(findOut, kernel) { - logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut) - } - case distro.OpenWrt: - out, err := exec.Command("opkg", "list-installed").CombinedOutput() - if err != nil { - logf("error querying OpenWrt installed packages: %s", out) - return - } - for _, pkg := range []string{"kmod-tun", "ca-bundle"} { - if !bytes.Contains(out, []byte(pkg+" - ")) { - logf("Missing required package %s; run: opkg install %s", pkg, pkg) + for _, cidr := range p.AllowedIPs { + if !cidr.Contains(ip) { + continue + } + if bestInNMPrefix.IsZero() || cidr.Bits > bestInNMPrefix.Bits { + bestInNMPrefix = cidr + bestInNM = p } } } + + e.wgLock.Lock() + defer e.wgLock.Unlock() + + // TODO(bradfitz): this is O(n peers). Add ART to netaddr? + var best netaddr.IPPrefix + var bestKey tailcfg.NodeKey + for _, p := range e.lastCfgFull.Peers { + for _, cidr := range p.AllowedIPs { + if !cidr.Contains(ip) { + continue + } + if best.IsZero() || cidr.Bits > best.Bits { + best = cidr + bestKey = tailcfg.NodeKey(p.PublicKey) + } + } + } + // And another pass. Probably better than allocating a map per peerForIP + // call. But TODO(bradfitz): add a lookup map to netmap.NetworkMap. + if !bestKey.IsZero() { + for _, p := range nm.Peers { + if p.Key == bestKey { + return p, nil + } + } + } + if bestInNM == nil { + return nil, nil + } + if bestInNMPrefix.Bits == 0 { + return nil, errors.New("exit node found but not enabled") + } + return nil, fmt.Errorf("node %q found, but not using its %v route", bestInNM.ComputedNameWithHost, bestInNMPrefix) } type closeOnErrorPool []func() diff --git a/wgengine/userspace_test.go b/wgengine/userspace_test.go index e9b83389a..ac081d83e 100644 --- a/wgengine/userspace_test.go +++ b/wgengine/userspace_test.go @@ -13,10 +13,10 @@ import ( "go4.org/mem" "inet.af/netaddr" + "tailscale.com/net/tstun" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/tstun" "tailscale.com/wgengine/wgcfg" ) @@ -39,7 +39,7 @@ func TestNoteReceiveActivity(t *testing.T) { logf: func(format string, a ...interface{}) { fmt.Fprintf(&logBuf, format, a...) }, - tundev: new(tstun.TUN), + tundev: new(tstun.Wrapper), testMaybeReconfigHook: func() { confc <- true }, trimmedDisco: map[tailcfg.DiscoKey]bool{}, } @@ -139,3 +139,50 @@ func dkFromHex(hex string) tailcfg.DiscoKey { } return tailcfg.DiscoKey(k) } + +// an experiment to see if genLocalAddrFunc was worth it. As of Go +// 1.16, it still very much is. (30-40x faster) +func BenchmarkGenLocalAddrFunc(b *testing.B) { + la1 := netaddr.MustParseIP("1.2.3.4") + la2 := netaddr.MustParseIP("::4") + lanot := netaddr.MustParseIP("5.5.5.5") + var x bool + b.Run("map1", func(b *testing.B) { + m := map[netaddr.IP]bool{ + la1: true, + } + for i := 0; i < b.N; i++ { + x = m[la1] + x = m[lanot] + } + }) + b.Run("map2", func(b *testing.B) { + m := map[netaddr.IP]bool{ + la1: true, + la2: true, + } + for i := 0; i < b.N; i++ { + x = m[la1] + x = m[lanot] + } + }) + b.Run("or1", func(b *testing.B) { + f := func(t netaddr.IP) bool { + return t == la1 + } + for i := 0; i < b.N; i++ { + x = f(la1) + x = f(lanot) + } + }) + b.Run("or2", func(b *testing.B) { + f := func(t netaddr.IP) bool { + return t == la1 || t == la2 + } + for i := 0; i < b.N; i++ { + x = f(la1) + x = f(lanot) + } + }) + b.Logf("x = %v", x) +} diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index f4f7d3085..f3248d6fc 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -14,12 +14,12 @@ import ( "inet.af/netaddr" "tailscale.com/ipn/ipnstate" + "tailscale.com/net/dns" "tailscale.com/tailcfg" "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" ) @@ -84,7 +84,7 @@ func (e *watchdogEngine) GetFilter() *filter.Filter { func (e *watchdogEngine) SetFilter(filt *filter.Filter) { e.watchdog("SetFilter", func() { e.wrap.SetFilter(filt) }) } -func (e *watchdogEngine) SetDNSMap(dm *tsdns.Map) { +func (e *watchdogEngine) SetDNSMap(dm *dns.Map) { e.watchdog("SetDNSMap", func() { e.wrap.SetDNSMap(dm) }) } func (e *watchdogEngine) SetStatusCallback(cb StatusCallback) { @@ -117,8 +117,8 @@ func (e *watchdogEngine) DiscoPublicKey() (k tailcfg.DiscoKey) { e.watchdog("DiscoPublicKey", func() { k = e.wrap.DiscoPublicKey() }) return k } -func (e *watchdogEngine) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) { - e.watchdog("Ping", func() { e.wrap.Ping(ip, cb) }) +func (e *watchdogEngine) Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) { + e.watchdog("Ping", func() { e.wrap.Ping(ip, useTSMP, cb) }) } func (e *watchdogEngine) RegisterIPPortIdentity(ipp netaddr.IPPort, tsIP netaddr.IP) { e.watchdog("RegisterIPPortIdentity", func() { e.wrap.RegisterIPPortIdentity(ipp, tsIP) }) diff --git a/wgengine/watchdog_test.go b/wgengine/watchdog_test.go index 7487d4827..0a699f848 100644 --- a/wgengine/watchdog_test.go +++ b/wgengine/watchdog_test.go @@ -7,6 +7,7 @@ package wgengine import ( "bytes" "fmt" + "runtime" "strings" "testing" "time" @@ -15,6 +16,13 @@ import ( func TestWatchdog(t *testing.T) { t.Parallel() + var maxWaitMultiple time.Duration = 1 + if runtime.GOOS == "darwin" { + // Work around slow close syscalls on Big Sur with content filter Network Extensions installed. + // See https://github.com/tailscale/tailscale/issues/1598. + maxWaitMultiple = 15 + } + t.Run("default watchdog does not fire", func(t *testing.T) { t.Parallel() e, err := NewFakeUserspaceEngine(t.Logf, 0) @@ -23,7 +31,7 @@ func TestWatchdog(t *testing.T) { } e = NewWatchdog(e) - e.(*watchdogEngine).maxWait = 150 * time.Millisecond + e.(*watchdogEngine).maxWait = maxWaitMultiple * 150 * time.Millisecond e.(*watchdogEngine).logf = t.Logf e.(*watchdogEngine).fatalf = t.Fatalf @@ -42,7 +50,7 @@ func TestWatchdog(t *testing.T) { usEngine := e.(*userspaceEngine) e = NewWatchdog(e) wdEngine := e.(*watchdogEngine) - wdEngine.maxWait = 100 * time.Millisecond + wdEngine.maxWait = maxWaitMultiple * 100 * time.Millisecond logBuf := new(bytes.Buffer) fatalCalled := make(chan struct{}) diff --git a/wgengine/wgcfg/device_test.go b/wgengine/wgcfg/device_test.go index d48da7c52..6bab065a5 100644 --- a/wgengine/wgcfg/device_test.go +++ b/wgengine/wgcfg/device_test.go @@ -55,12 +55,8 @@ func TestDeviceConfig(t *testing.T) { }}, } - device1 := device.NewDevice(newNilTun(), &device.DeviceOptions{ - Logger: device.NewLogger(device.LogLevelError, "device1"), - }) - device2 := device.NewDevice(newNilTun(), &device.DeviceOptions{ - Logger: device.NewLogger(device.LogLevelError, "device2"), - }) + device1 := device.NewDevice(newNilTun(), device.NewLogger(device.LogLevelError, "device1")) + device2 := device.NewDevice(newNilTun(), device.NewLogger(device.LogLevelError, "device2")) defer device1.Close() defer device2.Close() diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index c8e7963db..5946e1e77 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -9,12 +9,12 @@ import ( "inet.af/netaddr" "tailscale.com/ipn/ipnstate" + "tailscale.com/net/dns" "tailscale.com/tailcfg" "tailscale.com/types/netmap" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/monitor" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/tsdns" "tailscale.com/wgengine/wgcfg" ) @@ -66,7 +66,7 @@ type Engine interface { SetFilter(*filter.Filter) // SetDNSMap updates the DNS map. - SetDNSMap(*tsdns.Map) + SetDNSMap(*dns.Map) // SetStatusCallback sets the function to call when the // WireGuard status changes. @@ -136,7 +136,7 @@ type Engine interface { // Ping is a request to start a discovery ping with the peer handling // the given IP and then call cb with its ping latency & method. - Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) + Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult)) // RegisterIPPortIdentity registers a given node (identified by its // Tailscale IP) as temporarily having the given IP:port for whois lookups.