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