diff --git a/VERSION.txt b/VERSION.txt
index f0bb29e76..bc80560fa 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-1.3.0
+1.5.0
diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go
new file mode 100644
index 000000000..bd4145144
--- /dev/null
+++ b/cmd/hello/hello.go
@@ -0,0 +1,134 @@
+// 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.
+
+// The hello binary runs hello.ipn.dev.
+package main // import "tailscale.com/cmd/hello"
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "tailscale.com/safesocket"
+ "tailscale.com/tailcfg"
+)
+
+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")
+)
+
+func main() {
+ flag.Parse()
+
+ http.HandleFunc("/", root)
+ log.Printf("Starting hello server.")
+
+ errc := make(chan error, 1)
+ if *httpAddr != "" {
+ log.Printf("running HTTP server on %s", *httpAddr)
+ go func() {
+ errc <- http.ListenAndServe(*httpAddr, nil)
+ }()
+ }
+ if *httpsAddr != "" {
+ log.Printf("running HTTPS server on %s", *httpsAddr)
+ go func() {
+ errc <- http.ListenAndServeTLS(*httpsAddr,
+ "/etc/hello/hello.ipn.dev.crt",
+ "/etc/hello/hello.ipn.dev.key",
+ nil,
+ )
+ }()
+ }
+ log.Fatal(<-errc)
+}
+
+func slurpHTML() string {
+ slurp, err := ioutil.ReadFile("hello.tmpl.html")
+ if err != nil {
+ log.Fatal(err)
+ }
+ return string(slurp)
+}
+
+var tmpl = template.Must(template.New("home").Parse(slurpHTML()))
+
+type tmplData struct {
+ DisplayName string // "Foo Barberson"
+ LoginName string // "foo@bar.com"
+ MachineName string // "imac5k"
+ IP string // "100.2.3.4"
+}
+
+func root(w http.ResponseWriter, r *http.Request) {
+ if r.TLS == nil && *httpsAddr != "" {
+ host := r.Host
+ if strings.Contains(r.Host, "100.101.102.103") {
+ host = "hello.ipn.dev"
+ }
+ http.Redirect(w, r, "https://"+host, http.StatusFound)
+ return
+ }
+ if r.RequestURI != "/" {
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+ }
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ http.Error(w, "no remote addr", 500)
+ return
+ }
+ who, err := whoIs(ip)
+ 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)
+ return
+ }
+ 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,
+ })
+}
+
+// tsSockClient does HTTP requests to the local Tailscale daemon.
+// The hostname in the HTTP request is ignored.
+var tsSockClient = &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ 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))
+ if err != nil {
+ return nil, err
+ }
+ defer res.Body.Close()
+ slurp, _ := ioutil.ReadAll(res.Body)
+ if res.StatusCode != 200 {
+ return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
+ }
+ r := new(tailcfg.WhoIsResponse)
+ if err := json.Unmarshal(slurp, r); err != nil {
+ if max := 200; len(slurp) > max {
+ slurp = slurp[:max]
+ }
+ return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
+ }
+ return r, nil
+}
diff --git a/cmd/hello/hello.tmpl.html b/cmd/hello/hello.tmpl.html
new file mode 100644
index 000000000..bf43b65ac
--- /dev/null
+++ b/cmd/hello/hello.tmpl.html
@@ -0,0 +1,17 @@
+
+
+ Hello from Tailscale
+
+
+ Hello!
+
+ Hello {{.DisplayName}} ({{.LoginName}}) from {{.MachineName}} ({{.IP}}).
+
+
+ Your Tailscale is working!
+
+
+ Welcome to Tailscale.
+
+
+
diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go
index 022eb1328..f4f8c78ad 100644
--- a/cmd/tailscale/cli/status.go
+++ b/cmd/tailscale/cli/status.go
@@ -65,7 +65,17 @@ func runStatus(ctx context.Context, args []string) error {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
- ch <- n.Status
+ select {
+ case ch <- n.Status:
+ default:
+ // A status update from somebody else's request.
+ // Ignoring this matters mostly for "tailscale status -web"
+ // mode, otherwise the channel send would block forever
+ // and pump would stop reading from tailscaled, which
+ // previously caused tailscaled to block (while holding
+ // a mutex), backing up unrelated clients.
+ // See https://github.com/tailscale/tailscale/issues/1234
+ }
}
})
go pump(ctx, bc, c)
diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go
index d95ad30be..e60801f9b 100644
--- a/cmd/tailscale/cli/up.go
+++ b/cmd/tailscale/cli/up.go
@@ -228,7 +228,16 @@ func runUp(ctx context.Context, args []string) error {
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
- fatalf("backend error: %v\n", *n.ErrMessage)
+ msg := *n.ErrMessage
+ if msg == ipn.ErrMsgPermissionDenied {
+ switch runtime.GOOS {
+ case "windows":
+ msg += " (Tailscale service in use by other user?)"
+ default:
+ msg += " (try 'sudo tailscale up [...]')"
+ }
+ }
+ fatalf("backend error: %v\n", msg)
}
if s := n.State; s != nil {
switch *s {
diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt
index 3e76e25c5..559f214db 100644
--- a/cmd/tailscale/depaware.txt
+++ b/cmd/tailscale/depaware.txt
@@ -27,7 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
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
@@ -89,6 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index 19bcc78d6..77ef9073c 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -31,7 +31,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
@@ -130,6 +129,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
diff --git a/cmd/tailscaled/tailscaled.service b/cmd/tailscaled/tailscaled.service
index 878e86341..71dc89f1b 100644
--- a/cmd/tailscaled/tailscaled.service
+++ b/cmd/tailscaled/tailscaled.service
@@ -20,22 +20,5 @@ CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
-DeviceAllow=/dev/net/tun
-DeviceAllow=/dev/null
-DeviceAllow=/dev/random
-DeviceAllow=/dev/urandom
-DevicePolicy=strict
-LockPersonality=true
-MemoryDenyWriteExecute=true
-PrivateTmp=true
-ProtectClock=true
-ProtectControlGroups=true
-ProtectHome=true
-ProtectKernelTunables=true
-ProtectSystem=strict
-ReadWritePaths=/etc/
-RestrictSUIDSGID=true
-SystemCallArchitectures=native
-
[Install]
WantedBy=multi-user.target
diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index b3a9f61ac..e1aa7958b 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -770,12 +770,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
c.mu.Unlock()
nm := &NetworkMap{
+ SelfNode: resp.Node,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
Expiry: resp.Node.KeyExpiry,
Name: resp.Node.Name,
- DisplayName: resp.Node.DisplayName,
Addresses: resp.Node.Addresses,
Peers: resp.Peers,
LocalPort: localPort,
@@ -799,10 +799,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
}
addUserProfile(nm.User)
+ magicDNSSuffix := nm.MagicDNSSuffix()
+ nm.SelfNode.InitDisplayNames(magicDNSSuffix)
for _, peer := range resp.Peers {
- if peer.DisplayName == "" {
- peer.DisplayName = peer.DefaultDisplayName()
- }
+ peer.InitDisplayNames(magicDNSSuffix)
if !peer.Sharer.IsZero() {
if c.keepSharerAndUserSplit {
addUserProfile(peer.Sharer)
@@ -812,9 +812,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
addUserProfile(peer.User)
}
- if resp.Node.DisplayName == "" {
- nm.DisplayName = resp.Node.DefaultDisplayName()
- }
if resp.Node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
diff --git a/control/controlclient/netmap.go b/control/controlclient/netmap.go
index fe8553b66..40041a491 100644
--- a/control/controlclient/netmap.go
+++ b/control/controlclient/netmap.go
@@ -13,24 +13,23 @@ import (
"strings"
"time"
- "github.com/tailscale/wireguard-go/wgcfg"
"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 {
// Core networking
+ SelfNode *tailcfg.Node
NodeKey tailcfg.NodeKey
PrivateKey wgkey.Private
Expiry time.Time
// Name is the DNS name assigned to this node.
- Name string
- // DisplayName is the title to show for the node in client UIs.
- DisplayName string
+ Name string
Addresses []netaddr.IPPrefix
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
diff --git a/go.mod b/go.mod
index c306a8d67..4f976363f 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ 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-20210120212909-7ad8a0443bd3
+ github.com/tailscale/wireguard-go v0.0.0-20210201213041-c9817e648365
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
diff --git a/go.sum b/go.sum
index 6505d1fd5..d5becc8b9 100644
--- a/go.sum
+++ b/go.sum
@@ -296,6 +296,10 @@ github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4U
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXul2ysBGZVVM0fKISMgZ9BZRXuOYAyn8MxAbY=
github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
+github.com/tailscale/wireguard-go v0.0.0-20210129202040-ddaf8316eff8 h1:7OWHhbjWEuEjt+VlgOXLC4+iPkAvwTMU4zASxa+mKbw=
+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/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=
diff --git a/internal/deepprint/deepprint_test.go b/internal/deepprint/deepprint_test.go
index e2ae45ba7..e5b2b0924 100644
--- a/internal/deepprint/deepprint_test.go
+++ b/internal/deepprint/deepprint_test.go
@@ -8,10 +8,10 @@ import (
"bytes"
"testing"
- "github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
+ "tailscale.com/wgengine/wgcfg"
)
func TestDeepPrint(t *testing.T) {
diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go
index 7518c6e2a..dd8a39ab6 100644
--- a/ipn/ipnserver/server.go
+++ b/ipn/ipnserver/server.go
@@ -7,6 +7,7 @@ package ipnserver
import (
"bufio"
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -32,6 +33,7 @@ import (
"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"
@@ -113,10 +115,11 @@ type server struct {
// connIdentity represents the owner of a localhost TCP connection.
type connIdentity struct {
- Unknown bool
- Pid int
- UserID string
- User *user.User
+ Unknown bool
+ Pid int
+ UserID string
+ User *user.User
+ IsUnixSock bool
}
// getConnIdentity returns the localhost TCP connection's identity information
@@ -125,7 +128,9 @@ type connIdentity struct {
// to be able to map it and couldn't.
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
- return connIdentity{Unknown: true}, nil
+ ci = connIdentity{Unknown: true}
+ _, ci.IsUnixSock = c.(*net.UnixConn)
+ return ci, nil
}
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
if err != nil {
@@ -620,6 +625,7 @@ 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})
}
server.b = b
@@ -860,6 +866,10 @@ 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)
+ return
+ }
if ci.Unknown {
io.WriteString(w, "TailscaleTailscale
This is the local Tailscale daemon.")
return
@@ -883,3 +893,40 @@ 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/local.go b/ipn/local.go
index 580c49abc..0877ad9f7 100644
--- a/ipn/local.go
+++ b/ipn/local.go
@@ -15,7 +15,6 @@ import (
"sync"
"time"
- "github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
@@ -37,6 +36,7 @@ import (
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/tsdns"
+ "tailscale.com/wgengine/wgcfg"
)
var controlDebugFlags = getControlDebugFlags()
@@ -90,6 +90,7 @@ type LocalBackend struct {
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *controlclient.NetworkMap
+ nodeByAddr map[netaddr.IP]*tailcfg.Node
activeLogin string // last logged LoginName from netMap
engineStatus EngineStatus
endpoints []string
@@ -234,7 +235,22 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
})
}
}
+}
+// WhoIs reports the node and user who owns the node with the given IP.
+// If ok == true, n and u are valid.
+func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ n, ok = b.nodeByAddr[ip]
+ if !ok {
+ return nil, u, false
+ }
+ u, ok = b.netMap.UserProfiles[n.User]
+ if !ok {
+ return nil, u, false
+ }
+ return n, u, true
}
// SetDecompressor sets a decompression function, which must be a zstd
@@ -1507,6 +1523,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
b.logf("active login: %v", login)
b.activeLogin = login
}
+
+ if nm == nil {
+ b.nodeByAddr = nil
+ return
+ }
+
+ // Update the nodeByAddr index.
+ if b.nodeByAddr == nil {
+ b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
+ }
+ // First pass, mark everything unwanted.
+ for k := range b.nodeByAddr {
+ b.nodeByAddr[k] = nil
+ }
+ addNode := func(n *tailcfg.Node) {
+ for _, ipp := range n.Addresses {
+ if ipp.IsSingleIP() {
+ b.nodeByAddr[ipp.IP] = n
+ }
+ }
+ }
+ if nm.SelfNode != nil {
+ addNode(nm.SelfNode)
+ }
+ for _, p := range nm.Peers {
+ addNode(p)
+ }
+ // Third pass, actually delete the unwanted items.
+ for k, v := range b.nodeByAddr {
+ if v == nil {
+ delete(b.nodeByAddr, k)
+ }
+ }
}
// TestOnlyPublicKeys returns the current machine and node public
diff --git a/ipn/message.go b/ipn/message.go
index a9106dac7..9b7783f98 100644
--- a/ipn/message.go
+++ b/ipn/message.go
@@ -146,6 +146,10 @@ func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error
return bs.GotCommand(ctx, cmd)
}
+// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
+// operation was done from a user/context that didn't have permission.
+const ErrMsgPermissionDenied = "permission denied"
+
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
@@ -178,7 +182,7 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
}
if IsReadonlyContext(ctx) {
- msg := "permission denied"
+ msg := ErrMsgPermissionDenied
bs.send(Notify{ErrMessage: &msg})
return nil
}
diff --git a/net/interfaces/interfaces_darwin_cgo.go b/net/interfaces/interfaces_darwin_cgo.go
index ad9e982c9..df0cd1532 100644
--- a/net/interfaces/interfaces_darwin_cgo.go
+++ b/net/interfaces/interfaces_darwin_cgo.go
@@ -15,7 +15,7 @@ package interfaces
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
// Otherwise, it returns 0.
-int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
+uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
{
// sockaddrs are after the message header
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
@@ -38,7 +38,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
return 0; // gateway not IPv4
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
- int ip;
+ uint32_t ip;
ip = gateway_si->sin_addr.s_addr;
unsigned char a, b;
@@ -62,7 +62,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
// If no private gateway IP address was found, it returns 0.
// On an error, it returns an error code in (0, 255].
// Any private gateway IP address is > 255.
-int privateGatewayIP()
+uint32_t privateGatewayIP()
{
size_t needed;
int mib[6];
@@ -90,7 +90,7 @@ int privateGatewayIP()
struct rt_msghdr2 *rtm;
for (next = buf; next < lim; next += rtm->rtm_msglen) {
rtm = (struct rt_msghdr2 *)next;
- int ip;
+ uint32_t ip;
ip = privateGatewayIPFromRoute(rtm);
if (ip) {
free(buf);
diff --git a/safesocket/basic_test.go b/safesocket/basic_test.go
index 23727fc4f..863367111 100644
--- a/safesocket/basic_test.go
+++ b/safesocket/basic_test.go
@@ -32,7 +32,7 @@ func TestBasics(t *testing.T) {
errs <- err
return
}
- fmt.Printf("server read %d bytes.\n", n)
+ t.Logf("server read %d bytes.", n)
if string(b[:n]) != "world" {
errs <- fmt.Errorf("got %#v, expected %#v\n", string(b[:n]), "world")
return
diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go
index ba23a1d65..19e183463 100644
--- a/safesocket/safesocket.go
+++ b/safesocket/safesocket.go
@@ -7,7 +7,9 @@
package safesocket
import (
+ "errors"
"net"
+ "runtime"
)
type closeable interface {
@@ -27,6 +29,11 @@ func ConnCloseWrite(c net.Conn) error {
return c.(closeable).CloseWrite()
}
+// ConnectDefault connects to the local Tailscale daemon.
+func ConnectDefault() (net.Conn, error) {
+ return Connect("/var/run/tailscale/tailscaled.sock", 41112)
+}
+
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
func Connect(path string, port uint16) (net.Conn, error) {
return connect(path, port)
@@ -38,3 +45,21 @@ func Connect(path string, port uint16) (net.Conn, error) {
func Listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
return listen(path, port)
}
+
+var (
+ ErrTokenNotFound = errors.New("no token found")
+ ErrNoTokenOnOS = errors.New("no token on " + runtime.GOOS)
+)
+
+var localTCPPortAndToken func() (port int, token string, err error)
+
+// LocalTCPPortAndToken returns the port number and auth token to connect to
+// the local Tailscale daemon. It's currently only applicable on macOS
+// when tailscaled is being run in the Mac Sandbox from the App Store version
+// of Tailscale.
+func LocalTCPPortAndToken() (port int, token string, err error) {
+ if localTCPPortAndToken == nil {
+ return 0, "", ErrNoTokenOnOS
+ }
+ return localTCPPortAndToken()
+}
diff --git a/safesocket/safesocket_darwin.go b/safesocket/safesocket_darwin.go
new file mode 100644
index 000000000..c0d43c41a
--- /dev/null
+++ b/safesocket/safesocket_darwin.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.
+
+package safesocket
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "strconv"
+ "strings"
+)
+
+func init() {
+ localTCPPortAndToken = localTCPPortAndTokenDarwin
+}
+
+func localTCPPortAndTokenDarwin() (port int, token string, err error) {
+ out, err := exec.Command("lsof",
+ "-n", // numeric sockets; don't do DNS lookups, etc
+ "-a", // logical AND remaining options
+ fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
+ "-c", "IPNExtension", // starting with IPNExtension
+ "-F", // machine-readable output
+ ).Output()
+ if err != nil {
+ return 0, "", fmt.Errorf("failed to run lsof looking for IPNExtension: %w", err)
+ }
+ bs := bufio.NewScanner(bytes.NewReader(out))
+ subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
+ for bs.Scan() {
+ line := bs.Bytes()
+ i := bytes.Index(line, subStr)
+ if i == -1 {
+ continue
+ }
+ f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
+ if len(f) != 2 {
+ continue
+ }
+ portStr, token := f[0], f[1]
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return 0, "", fmt.Errorf("invalid port %q found in lsof", portStr)
+ }
+ return port, token, nil
+ }
+ return 0, "", ErrTokenNotFound
+}
diff --git a/safesocket/safesocket_test.go b/safesocket/safesocket_test.go
new file mode 100644
index 000000000..4b39c11cd
--- /dev/null
+++ b/safesocket/safesocket_test.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 safesocket
+
+import "testing"
+
+func TestLocalTCPPortAndToken(t *testing.T) {
+ // Just test that it compiles for now (is available on all platforms).
+ port, token, err := LocalTCPPortAndToken()
+ t.Logf("got %v, %s, %v", port, token, err)
+}
diff --git a/safesocket/unixsocket.go b/safesocket/unixsocket.go
index 31322dd3c..fd2a58852 100644
--- a/safesocket/unixsocket.go
+++ b/safesocket/unixsocket.go
@@ -7,17 +7,15 @@
package safesocket
import (
- "bufio"
- "bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
- "os/exec"
"path/filepath"
"runtime"
+ "strconv"
"strings"
)
@@ -166,42 +164,24 @@ func connectMacOSAppSandbox() (net.Conn, error) {
}
f := strings.SplitN(best.Name(), "-", 3)
portStr, token := f[1], f[2]
- return connectMacTCP(portStr, token)
+ port, err := strconv.Atoi(portStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid port %q", portStr)
+ }
+ return connectMacTCP(port, token)
}
// Otherwise, assume we're running the cmd/tailscale binary from outside the
// App Sandbox.
-
- out, err := exec.Command("lsof",
- "-n", // numeric sockets; don't do DNS lookups, etc
- "-a", // logical AND remaining options
- fmt.Sprintf("-u%d", os.Getuid()), // process of same user only
- "-c", "IPNExtension", // starting with IPNExtension
- "-F", // machine-readable output
- ).Output()
+ port, token, err := LocalTCPPortAndToken()
if err != nil {
return nil, err
}
- bs := bufio.NewScanner(bytes.NewReader(out))
- subStr := []byte(".tailscale.ipn.macos/sameuserproof-")
- for bs.Scan() {
- line := bs.Bytes()
- i := bytes.Index(line, subStr)
- if i == -1 {
- continue
- }
- f := strings.SplitN(string(line[i+len(subStr):]), "-", 2)
- if len(f) != 2 {
- continue
- }
- portStr, token := f[0], f[1]
- return connectMacTCP(portStr, token)
- }
- return nil, fmt.Errorf("failed to find Tailscale's IPNExtension process")
+ return connectMacTCP(port, token)
}
-func connectMacTCP(portStr, token string) (net.Conn, error) {
- c, err := net.Dial("tcp", "localhost:"+portStr)
+func connectMacTCP(port int, token string) (net.Conn, error) {
+ c, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port))
if err != nil {
return nil, fmt.Errorf("error dialing IPNExtension: %w", err)
}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index 098c95d56..ba215000e 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -161,12 +161,6 @@ type Node struct {
StableID StableNodeID
Name string // DNS
- // DisplayName is the title to show for the node in client
- // UIs. This field is assigned by default in controlclient,
- // but can be overriden by providing this field non-empty
- // in a MapResponse.
- DisplayName string `json:",omitempty"`
-
// User is the user who created the node. If ACL tags are in
// use for the node then it doesn't reflect the ACL identity
// that the node is running as.
@@ -190,21 +184,98 @@ type Node struct {
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
+
+ // The following three computed fields hold the various names that can
+ // be used for this node in UIs. They are populated from controlclient
+ // (not from control) by calling node.InitDisplayNames. These can be
+ // used directly or accessed via node.DisplayName or node.DisplayNames.
+
+ ComputedName string `json:",omitempty"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
+ computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
+ ComputedNameWithHost string `json:",omitempty"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
}
-// DefaultDisplayName returns a value suitable
-// for using as the default value for n.DisplayName.
-func (n *Node) DefaultDisplayName() string {
- if n.Name != "" {
- // Use the Magic DNS prefix as the default display name.
- return dnsname.ToBaseName(n.Name)
+// DisplayName returns the user-facing name for a node which should
+// be shown in client UIs.
+//
+// Parameter forOwner specifies whether the name is requested by
+// the owner of the node. When forOwner is false, the hostname is
+// never included in the return value.
+//
+// Return value is either either "Name" or "Name (Hostname)", where
+// Name is the node's MagicDNS base name (for normal non-shared-in
+// nodes), FQDN (without trailing dot, for shared-in nodes), or
+// Hostname (if no MagicDNS). Hostname is only included in the
+// return value if it varies from Name and forOwner is provided true.
+//
+// DisplayName is only valid if InitDisplayNames has been called.
+func (n *Node) DisplayName(forOwner bool) string {
+ if forOwner {
+ return n.ComputedNameWithHost
}
- if n.Hostinfo.Hostname != "" {
- // When no Magic DNS name is present, use the hostname.
- return n.Hostinfo.Hostname
+ return n.ComputedName
+}
+
+// DisplayName returns the decomposed user-facing name for a node.
+//
+// Parameter forOwner specifies whether the name is requested by
+// the owner of the node. When forOwner is false, hostIfDifferent
+// is always returned empty.
+//
+// Return value name is the node's primary name, populated with the
+// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
+// (without trailing dot, for shared-in nodes), or Hostname (if no
+// MagicDNS).
+//
+// Return value hostIfDifferent, when non-empty, is the node's
+// hostname. hostIfDifferent is only populated when the hostname
+// varies from name and forOwner is provided as true.
+//
+// DisplayNames is only valid if InitDisplayNames has been called.
+func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
+ if forOwner {
+ return n.ComputedName, n.computedHostIfDifferent
}
- // When we've exhausted all other name options, use the node's ID.
- return n.ID.String()
+ return n.ComputedName, ""
+}
+
+// InitDisplayNames computes and populates n's display name
+// fields: n.ComputedName, n.computedHostIfDifferent, and
+// n.ComputedNameWithHost.
+func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
+ dnsName := n.Name
+ if dnsName != "" {
+ dnsName = strings.TrimRight(dnsName, ".")
+ if i := strings.Index(dnsName, "."); i != -1 && dnsname.HasSuffix(dnsName, networkMagicDNSSuffix) {
+ dnsName = dnsName[:i]
+ }
+ }
+
+ name := dnsName
+ hostIfDifferent := n.Hostinfo.Hostname
+
+ if strings.EqualFold(name, hostIfDifferent) {
+ hostIfDifferent = ""
+ }
+ if name == "" {
+ if hostIfDifferent != "" {
+ name = hostIfDifferent
+ hostIfDifferent = ""
+ } else {
+ name = n.Key.String()
+ }
+ }
+
+ var nameWithHost string
+ if hostIfDifferent != "" {
+ nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
+ } else {
+ nameWithHost = name
+ }
+
+ n.ComputedName = name
+ n.computedHostIfDifferent = hostIfDifferent
+ n.ComputedNameWithHost = nameWithHost
}
type MachineStatus int
@@ -818,7 +889,6 @@ func (n *Node) Equal(n2 *Node) bool {
n.ID == n2.ID &&
n.StableID == n2.StableID &&
n.Name == n2.Name &&
- n.DisplayName == n2.DisplayName &&
n.User == n2.User &&
n.Sharer == n2.Sharer &&
n.Key == n2.Key &&
@@ -832,7 +902,10 @@ func (n *Node) Equal(n2 *Node) bool {
n.Hostinfo.Equal(&n2.Hostinfo) &&
n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) &&
- n.MachineAuthorized == n2.MachineAuthorized
+ n.MachineAuthorized == n2.MachineAuthorized &&
+ n.ComputedName == n2.ComputedName &&
+ n.computedHostIfDifferent == n2.computedHostIfDifferent &&
+ n.ComputedNameWithHost == n2.ComputedNameWithHost
}
func eqStrings(a, b []string) bool {
@@ -862,3 +935,9 @@ func eqCIDRs(a, b []netaddr.IPPrefix) bool {
func eqTimePtr(a, b *time.Time) bool {
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}
+
+// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
+type WhoIsResponse struct {
+ Node *Node
+ UserProfile *UserProfile
+}
diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go
index 3101048b4..87bbb2484 100644
--- a/tailcfg/tailcfg_clone.go
+++ b/tailcfg/tailcfg_clone.go
@@ -61,25 +61,27 @@ func (src *Node) Clone() *Node {
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _NodeNeedsRegeneration = Node(struct {
- ID NodeID
- StableID StableNodeID
- Name string
- DisplayName string
- User UserID
- Sharer UserID
- Key NodeKey
- KeyExpiry time.Time
- Machine MachineKey
- DiscoKey DiscoKey
- Addresses []netaddr.IPPrefix
- AllowedIPs []netaddr.IPPrefix
- Endpoints []string
- DERP string
- Hostinfo Hostinfo
- Created time.Time
- LastSeen *time.Time
- KeepAlive bool
- MachineAuthorized bool
+ ID NodeID
+ StableID StableNodeID
+ Name string
+ User UserID
+ Sharer UserID
+ Key NodeKey
+ KeyExpiry time.Time
+ Machine MachineKey
+ DiscoKey DiscoKey
+ Addresses []netaddr.IPPrefix
+ AllowedIPs []netaddr.IPPrefix
+ Endpoints []string
+ DERP string
+ Hostinfo Hostinfo
+ Created time.Time
+ LastSeen *time.Time
+ KeepAlive bool
+ MachineAuthorized bool
+ ComputedName string
+ computedHostIfDifferent string
+ ComputedNameWithHost string
}{})
// Clone makes a deep copy of Hostinfo.
diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go
index 794f7d4a1..a6c843db5 100644
--- a/tailcfg/tailcfg_test.go
+++ b/tailcfg/tailcfg_test.go
@@ -189,10 +189,11 @@ func TestHostinfoEqual(t *testing.T) {
func TestNodeEqual(t *testing.T) {
nodeHandles := []string{
- "ID", "StableID", "Name", "DisplayName", "User", "Sharer",
+ "ID", "StableID", "Name", "User", "Sharer",
"Key", "KeyExpiry", "Machine", "DiscoKey",
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
+ "ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
}
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go
index a7cb50725..a7299b607 100644
--- a/tsweb/tsweb.go
+++ b/tsweb/tsweb.go
@@ -42,6 +42,7 @@ func NewMux(debugHandler http.Handler) *http.ServeMux {
func registerCommonDebug(mux *http.ServeMux) {
expvar.Publish("counter_uptime_sec", expvar.Func(func() interface{} { return int64(Uptime().Seconds()) }))
+ expvar.Publish("gauge_goroutines", expvar.Func(func() interface{} { return runtime.NumGoroutine() }))
mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
mux.Handle("/debug/varz", Protected(http.HandlerFunc(VarzHandler)))
diff --git a/types/logger/logger.go b/types/logger/logger.go
index 9026e4fae..dc79e8772 100644
--- a/types/logger/logger.go
+++ b/types/logger/logger.go
@@ -64,9 +64,9 @@ type limitData struct {
var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
-// rateFreePrefix are format string prefixes that are exempt from rate limiting.
+// rateFree are format string substrings that are exempt from rate limiting.
// Things should not be added to this unless they're already limited otherwise.
-var rateFreePrefix = []string{
+var rateFree = []string{
"magicsock: disco: ",
"magicsock: CreateEndpoint:",
}
@@ -93,8 +93,8 @@ func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
)
judge := func(format string) verdict {
- for _, pfx := range rateFreePrefix {
- if strings.HasPrefix(format, pfx) {
+ for _, sub := range rateFree {
+ if strings.Contains(format, sub) {
return allow
}
}
diff --git a/util/dnsname/dnsname.go b/util/dnsname/dnsname.go
index 633471e2f..1488272a4 100644
--- a/util/dnsname/dnsname.go
+++ b/util/dnsname/dnsname.go
@@ -17,11 +17,3 @@ func HasSuffix(name, suffix string) bool {
nameBase := strings.TrimSuffix(name, suffix)
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
}
-
-// ToBaseName removes the domain ending from a DNS name of a node.
-func ToBaseName(name string) string {
- if i := strings.Index(name, "."); i != -1 {
- return name[:i]
- }
- return name
-}
diff --git a/util/dnsname/dnsname_test.go b/util/dnsname/dnsname_test.go
index a8c97ed8b..da4e51384 100644
--- a/util/dnsname/dnsname_test.go
+++ b/util/dnsname/dnsname_test.go
@@ -26,21 +26,3 @@ func TestHasSuffix(t *testing.T) {
}
}
}
-
-func TestToBaseName(t *testing.T) {
- tests := []struct {
- name string
- want string
- }{
- {"foo", "foo"},
- {"foo.com", "foo"},
- {"foo.example.com.beta.tailscale.net", "foo"},
- {"computer-a.test.gmail.com.beta.tailscale.net", "computer-a"},
- }
- for _, tt := range tests {
- got := ToBaseName(tt.name)
- if got != tt.want {
- t.Errorf("ToBaseName(%q) = %q; want %q", tt.name, got, tt.want)
- }
- }
-}
diff --git a/wgengine/magicsock/legacy.go b/wgengine/magicsock/legacy.go
index 8fe34e5d8..7620cc1ce 100644
--- a/wgengine/magicsock/legacy.go
+++ b/wgengine/magicsock/legacy.go
@@ -19,7 +19,6 @@ import (
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/tai64n"
- "github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
@@ -28,6 +27,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/wgkey"
+ "tailscale.com/wgengine/wgcfg"
)
var (
@@ -438,7 +438,17 @@ func (a *addrSet) DstToBytes() []byte {
return packIPPort(a.dst())
}
func (a *addrSet) DstToString() string {
- return a.Addrs()
+ var addrs []string
+ for _, addr := range a.addrs {
+ addrs = append(addrs, addr.String())
+ }
+
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.roamAddr != nil {
+ addrs = append(addrs, a.roamAddr.String())
+ }
+ return strings.Join(addrs, ",")
}
func (a *addrSet) DstIP() net.IP {
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
@@ -577,20 +587,6 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
}
}
-func (a *addrSet) Addrs() string {
- var addrs []string
- for _, addr := range a.addrs {
- addrs = append(addrs, addr.String())
- }
-
- a.mu.Lock()
- defer a.mu.Unlock()
- if a.roamAddr != nil {
- addrs = append(addrs, a.roamAddr.String())
- }
- return strings.Join(addrs, ",")
-}
-
// Message types copied from wireguard-go/device/noise-protocol.go
const (
messageInitiationType = 1
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index 1260f85ca..d28a07db3 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -3056,13 +3056,6 @@ func (de *discoEndpoint) String() string {
return fmt.Sprintf("magicsock.discoEndpoint{%v, %v}", de.publicKey.ShortString(), de.discoShort)
}
-func (de *discoEndpoint) Addrs() string {
- // This has to be the same string that was passed to
- // CreateEndpoint, otherwise Reconfig will end up recreating
- // Endpoints and losing state over time.
- return de.wgEndpointHostPort
-}
-
func (de *discoEndpoint) ClearSrc() {}
func (de *discoEndpoint) SrcToString() string { panic("unused") } // unused by wireguard-go
func (de *discoEndpoint) SrcIP() net.IP { panic("unused") } // unused by wireguard-go
diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go
index 620394b12..dfa1f6230 100644
--- a/wgengine/magicsock/magicsock_test.go
+++ b/wgengine/magicsock/magicsock_test.go
@@ -28,7 +28,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun/tuntest"
- "github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
@@ -46,6 +45,7 @@ import (
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/tstun"
+ "tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wglog"
)
@@ -196,7 +196,7 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
s.wgLogger.SetPeers(cfg.Peers)
- return s.dev.Reconfig(cfg)
+ return wgcfg.ReconfigDevice(s.dev, cfg, s.conn.logf)
}
func (s *magicStack) String() string {
@@ -1131,7 +1131,11 @@ func testTwoDevicePing(t *testing.T, d *devices) {
defer setT(outerT)
pingSeq(t, 50, 700*time.Millisecond, false)
- ep2 := m2.dev.Config().Peers[0].Endpoints
+ cfg, err := wgcfg.DeviceConfig(m2.dev)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ep2 := cfg.Peers[0].Endpoints
if len(ep2) != 2 {
t.Error("handshake spray failed to find real route")
}
diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go
index f65d12742..d6e10dac1 100644
--- a/wgengine/router/router_linux.go
+++ b/wgengine/router/router_linux.go
@@ -116,7 +116,7 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (
v6err := checkIPv6()
if v6err != nil {
- logf("disabling IPv6 due to system IPv6 config: %v", v6err)
+ logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
}
supportsV6 := v6err == nil
supportsV6NAT := supportsV6 && supportsV6NAT()
@@ -366,7 +366,9 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
// address is already assigned to the interface, or if the addition
// fails.
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
-
+ if !r.v6Available && addr.IP.Is6() {
+ return nil
+ }
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
}
@@ -380,6 +382,9 @@ func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
// the address is not assigned to the interface, or if the removal
// fails.
func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error {
+ if !r.v6Available && addr.IP.Is6() {
+ return nil
+ }
if err := r.delLoopbackRule(addr.IP); err != nil {
return err
}
@@ -437,6 +442,9 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
// interface. Fails if the route already exists, or if adding the
// route fails.
func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
+ if !r.v6Available && cidr.IP.Is6() {
+ return nil
+ }
args := []string{
"ip", "route", "add",
normalizeCIDR(cidr),
@@ -452,6 +460,9 @@ func (r *linuxRouter) addRoute(cidr netaddr.IPPrefix) error {
// interface. Fails if the route doesn't exist, or if removing the
// route fails.
func (r *linuxRouter) delRoute(cidr netaddr.IPPrefix) error {
+ if !r.v6Available && cidr.IP.Is6() {
+ return nil
+ }
args := []string{
"ip", "route", "del",
normalizeCIDR(cidr),
@@ -1034,18 +1045,22 @@ func checkIPv6() error {
return errors.New("disable_ipv6 is set")
}
- // Older kernels don't support IPv6 policy routing.
+ // Older kernels don't support IPv6 policy routing. Some kernels
+ // support policy routing but don't have this knob, so absence of
+ // the knob is not fatal.
bs, err = ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
- if err != nil {
- // Absent knob means policy routing is unsupported.
- return err
+ if err == nil {
+ disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
+ if err != nil {
+ return errors.New("disable_policy has invalid bool")
+ }
+ if disabled {
+ return errors.New("disable_policy is set")
+ }
}
- disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
- if err != nil {
- return errors.New("disable_policy has invalid bool")
- }
- if disabled {
- return errors.New("disable_policy is set")
+
+ if err := checkIPRuleSupportsV6(); err != nil {
+ return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
}
// Some distros ship ip6tables separately from iptables.
@@ -1053,10 +1068,6 @@ func checkIPv6() error {
return err
}
- if err := checkIPRuleSupportsV6(); err != nil {
- return err
- }
-
return nil
}
@@ -1077,13 +1088,17 @@ func supportsV6NAT() bool {
}
func checkIPRuleSupportsV6() error {
- // First add a rule for "ip rule del" to delete.
- // We ignore the "add" operation's error because it can also
- // fail if the rule already exists.
- exec.Command("ip", "-6", "rule", "add",
- "pref", "123", "fwmark", tailscaleBypassMark, "table", fmt.Sprint(tailscaleRouteTable)).Run()
- out, err := exec.Command("ip", "-6", "rule", "del",
- "pref", "123", "fwmark", tailscaleBypassMark, "table", fmt.Sprint(tailscaleRouteTable)).CombinedOutput()
+ add := []string{"-6", "rule", "add", "pref", "1234", "fwmark", tailscaleBypassMark, "table", tailscaleRouteTable}
+ del := []string{"-6", "rule", "del", "pref", "1234", "fwmark", tailscaleBypassMark, "table", tailscaleRouteTable}
+
+ // First delete the rule unconditionally, and don't check for
+ // errors. This is just cleaning up anything that might be already
+ // there.
+ exec.Command("ip", del...).Run()
+
+ // Try adding the rule. This will fail on systems that support
+ // IPv6, but not IPv6 policy routing.
+ out, err := exec.Command("ip", add...).CombinedOutput()
if err != nil {
out = bytes.TrimSpace(out)
var detail interface{} = out
@@ -1092,5 +1107,8 @@ func checkIPRuleSupportsV6() error {
}
return fmt.Errorf("ip -6 rule failed: %s", detail)
}
+
+ // Delete again.
+ exec.Command("ip", del...).Run()
return nil
}
diff --git a/wgengine/userspace.go b/wgengine/userspace.go
index d4746487a..8638b3d38 100644
--- a/wgengine/userspace.go
+++ b/wgengine/userspace.go
@@ -23,7 +23,6 @@ import (
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
- "github.com/tailscale/wireguard-go/wgcfg"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
@@ -46,6 +45,7 @@ import (
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/tsdns"
"tailscale.com/wgengine/tstun"
+ "tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wglog"
)
@@ -194,6 +194,7 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
e, err := NewUserspaceEngineAdvanced(conf)
if err != nil {
+ tun.Close()
return nil, err
}
return e, err
@@ -835,7 +836,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
}
if numRemove > 0 {
e.logf("wgengine: Reconfig: removing session keys for %d peers", numRemove)
- if err := e.wgdev.Reconfig(&minner); err != nil {
+ if err := wgcfg.ReconfigDevice(e.wgdev, &minner, e.logf); err != nil {
e.logf("wgdev.Reconfig: %v", err)
return err
}
@@ -843,7 +844,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
}
e.logf("wgengine: Reconfig: configuring userspace wireguard config (with %d/%d peers)", len(min.Peers), len(full.Peers))
- if err := e.wgdev.Reconfig(&min); err != nil {
+ if err := wgcfg.ReconfigDevice(e.wgdev, &min, e.logf); err != nil {
e.logf("wgdev.Reconfig: %v", err)
return err
}
@@ -1035,6 +1036,8 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
return e.statusCallback
}
+var singleNewline = []byte{'\n'}
+
func (e *userspaceEngine) getStatus() (*Status, error) {
// Grab derpConns before acquiring wgLock to not violate lock ordering;
// the DERPs method acquires magicsock.Conn.mu.
@@ -1060,6 +1063,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
}
pr, pw := io.Pipe()
+ defer pr.Close() // to unblock writes on error path returns
errc := make(chan error, 1)
go func() {
@@ -1096,9 +1100,9 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
break
}
if err != nil {
- pr.Close()
return nil, fmt.Errorf("reading from UAPI pipe: %w", err)
}
+ line = bytes.TrimSuffix(line, singleNewline)
k := line
var v mem.RO
if i := bytes.IndexByte(line, '='); i != -1 {
@@ -1109,7 +1113,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
case "public_key":
pk, err := key.NewPublicFromHexMem(v)
if err != nil {
- return nil, fmt.Errorf("IpcGetOperation: invalid key %#v", v)
+ return nil, fmt.Errorf("IpcGetOperation: invalid key in line %q", line)
}
p = &PeerStatus{}
pp[wgkey.Key(pk)] = p
diff --git a/wgengine/userspace_test.go b/wgengine/userspace_test.go
index eed9ab941..a5cc9d965 100644
--- a/wgengine/userspace_test.go
+++ b/wgengine/userspace_test.go
@@ -11,13 +11,13 @@ import (
"testing"
"time"
- "github.com/tailscale/wireguard-go/wgcfg"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/tstun"
+ "tailscale.com/wgengine/wgcfg"
)
func TestNoteReceiveActivity(t *testing.T) {
diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go
index ee0fc3045..91b5fe04e 100644
--- a/wgengine/watchdog.go
+++ b/wgengine/watchdog.go
@@ -12,7 +12,6 @@ import (
"strings"
"time"
- "github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
@@ -21,6 +20,7 @@ import (
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/tsdns"
+ "tailscale.com/wgengine/wgcfg"
)
// NewWatchdog wraps an Engine and makes sure that all methods complete
diff --git a/wgengine/wgcfg/config.go b/wgengine/wgcfg/config.go
new file mode 100644
index 000000000..af86b36d6
--- /dev/null
+++ b/wgengine/wgcfg/config.go
@@ -0,0 +1,67 @@
+// 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 wgcfg has types and a parser for representing WireGuard config.
+package wgcfg
+
+import (
+ "inet.af/netaddr"
+)
+
+// Config is a WireGuard configuration.
+// It only supports the set of things Tailscale uses.
+type Config struct {
+ Name string
+ PrivateKey PrivateKey
+ Addresses []netaddr.IPPrefix
+ ListenPort uint16
+ MTU uint16
+ DNS []netaddr.IP
+ Peers []Peer
+}
+
+type Peer struct {
+ PublicKey Key
+ AllowedIPs []netaddr.IPPrefix
+ Endpoints string // comma-separated host/port pairs: "1.2.3.4:56,[::]:80"
+ PersistentKeepalive uint16
+}
+
+// Copy makes a deep copy of Config.
+// The result aliases no memory with the original.
+func (cfg Config) Copy() Config {
+ res := cfg
+ if res.Addresses != nil {
+ res.Addresses = append([]netaddr.IPPrefix{}, res.Addresses...)
+ }
+ if res.DNS != nil {
+ res.DNS = append([]netaddr.IP{}, res.DNS...)
+ }
+ peers := make([]Peer, 0, len(res.Peers))
+ for _, peer := range res.Peers {
+ peers = append(peers, peer.Copy())
+ }
+ res.Peers = peers
+ return res
+}
+
+// Copy makes a deep copy of Peer.
+// The result aliases no memory with the original.
+func (peer Peer) Copy() Peer {
+ res := peer
+ if res.AllowedIPs != nil {
+ res.AllowedIPs = append([]netaddr.IPPrefix{}, res.AllowedIPs...)
+ }
+ return res
+}
+
+// PeerWithKey returns the Peer with key k and reports whether it was found.
+func (config Config) PeerWithKey(k Key) (Peer, bool) {
+ for _, p := range config.Peers {
+ if p.PublicKey == k {
+ return p, true
+ }
+ }
+ return Peer{}, false
+}
diff --git a/wgengine/wgcfg/device.go b/wgengine/wgcfg/device.go
new file mode 100644
index 000000000..fd00f2229
--- /dev/null
+++ b/wgengine/wgcfg/device.go
@@ -0,0 +1,61 @@
+// 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 wgcfg
+
+import (
+ "io"
+ "sort"
+
+ "github.com/tailscale/wireguard-go/device"
+ "tailscale.com/types/logger"
+)
+
+func DeviceConfig(d *device.Device) (*Config, error) {
+ r, w := io.Pipe()
+ errc := make(chan error, 1)
+ go func() {
+ errc <- d.IpcGetOperation(w)
+ w.Close()
+ }()
+ cfg, err := FromUAPI(r)
+ if err != nil {
+ return nil, err
+ }
+ if err := <-errc; err != nil {
+ return nil, err
+ }
+
+ sort.Slice(cfg.Peers, func(i, j int) bool {
+ return cfg.Peers[i].PublicKey.LessThan(&cfg.Peers[j].PublicKey)
+ })
+ return cfg, nil
+}
+
+// ReconfigDevice replaces the existing device configuration with cfg.
+func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
+ defer func() {
+ if err != nil {
+ logf("wgcfg.Reconfig failed: %v", err)
+ }
+ }()
+
+ prev, err := DeviceConfig(d)
+ if err != nil {
+ return err
+ }
+
+ r, w := io.Pipe()
+ errc := make(chan error)
+ go func() {
+ errc <- d.IpcSetOperation(r)
+ }()
+
+ err = cfg.ToUAPI(w, prev)
+ if err != nil {
+ return err
+ }
+ w.Close()
+ return <-errc
+}
diff --git a/wgengine/wgcfg/device_test.go b/wgengine/wgcfg/device_test.go
new file mode 100644
index 000000000..d48da7c52
--- /dev/null
+++ b/wgengine/wgcfg/device_test.go
@@ -0,0 +1,242 @@
+// 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 wgcfg
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "os"
+ "sort"
+ "strings"
+ "sync"
+ "testing"
+
+ "github.com/tailscale/wireguard-go/device"
+ "github.com/tailscale/wireguard-go/tun"
+ "inet.af/netaddr"
+ "tailscale.com/types/wgkey"
+)
+
+func TestDeviceConfig(t *testing.T) {
+ newPrivateKey := func() (Key, PrivateKey) {
+ t.Helper()
+ pk, err := wgkey.NewPrivate()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return Key(pk.Public()), PrivateKey(pk)
+ }
+ k1, pk1 := newPrivateKey()
+ ip1 := netaddr.MustParseIPPrefix("10.0.0.1/32")
+
+ k2, pk2 := newPrivateKey()
+ ip2 := netaddr.MustParseIPPrefix("10.0.0.2/32")
+
+ k3, _ := newPrivateKey()
+ ip3 := netaddr.MustParseIPPrefix("10.0.0.3/32")
+
+ cfg1 := &Config{
+ PrivateKey: PrivateKey(pk1),
+ Peers: []Peer{{
+ PublicKey: k2,
+ AllowedIPs: []netaddr.IPPrefix{ip2},
+ }},
+ }
+
+ cfg2 := &Config{
+ PrivateKey: PrivateKey(pk2),
+ Peers: []Peer{{
+ PublicKey: k1,
+ AllowedIPs: []netaddr.IPPrefix{ip1},
+ PersistentKeepalive: 5,
+ }},
+ }
+
+ device1 := device.NewDevice(newNilTun(), &device.DeviceOptions{
+ Logger: device.NewLogger(device.LogLevelError, "device1"),
+ })
+ device2 := device.NewDevice(newNilTun(), &device.DeviceOptions{
+ Logger: device.NewLogger(device.LogLevelError, "device2"),
+ })
+ defer device1.Close()
+ defer device2.Close()
+
+ cmp := func(t *testing.T, d *device.Device, want *Config) {
+ t.Helper()
+ got, err := DeviceConfig(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prev := new(Config)
+ gotbuf := new(strings.Builder)
+ err = got.ToUAPI(gotbuf, prev)
+ gotStr := gotbuf.String()
+ if err != nil {
+ t.Errorf("got.ToUAPI(): error: %v", err)
+ return
+ }
+ wantbuf := new(strings.Builder)
+ err = want.ToUAPI(wantbuf, prev)
+ wantStr := wantbuf.String()
+ if err != nil {
+ t.Errorf("want.ToUAPI(): error: %v", err)
+ return
+ }
+ if gotStr != wantStr {
+ buf := new(bytes.Buffer)
+ w := bufio.NewWriter(buf)
+ if err := d.IpcGetOperation(w); err != nil {
+ t.Errorf("on error, could not IpcGetOperation: %v", err)
+ }
+ w.Flush()
+ t.Errorf("cfg:\n%s\n---- want:\n%s\n---- uapi:\n%s", gotStr, wantStr, buf.String())
+ }
+ }
+
+ t.Run("device1 config", func(t *testing.T) {
+ if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device1, cfg1)
+ })
+
+ t.Run("device2 config", func(t *testing.T) {
+ if err := ReconfigDevice(device2, cfg2, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device2, cfg2)
+ })
+
+ // This is only to test that Config and Reconfig are properly synchronized.
+ t.Run("device2 config/reconfig", func(t *testing.T) {
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ go func() {
+ ReconfigDevice(device2, cfg2, t.Logf)
+ wg.Done()
+ }()
+
+ go func() {
+ DeviceConfig(device2)
+ wg.Done()
+ }()
+
+ wg.Wait()
+ })
+
+ t.Run("device1 modify peer", func(t *testing.T) {
+ cfg1.Peers[0].Endpoints = "1.2.3.4:12345"
+ if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device1, cfg1)
+ })
+
+ t.Run("device1 replace endpoint", func(t *testing.T) {
+ cfg1.Peers[0].Endpoints = "1.1.1.1:123"
+ if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device1, cfg1)
+ })
+
+ t.Run("device1 add new peer", func(t *testing.T) {
+ cfg1.Peers = append(cfg1.Peers, Peer{
+ PublicKey: k3,
+ AllowedIPs: []netaddr.IPPrefix{ip3},
+ })
+ sort.Slice(cfg1.Peers, func(i, j int) bool {
+ return cfg1.Peers[i].PublicKey.LessThan(&cfg1.Peers[j].PublicKey)
+ })
+
+ origCfg, err := DeviceConfig(device1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device1, cfg1)
+
+ newCfg, err := DeviceConfig(device1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ peer0 := func(cfg *Config) Peer {
+ p, ok := cfg.PeerWithKey(k2)
+ if !ok {
+ t.Helper()
+ t.Fatal("failed to look up peer 2")
+ }
+ return p
+ }
+ peersEqual := func(p, q Peer) bool {
+ return p.PublicKey == q.PublicKey && p.PersistentKeepalive == q.PersistentKeepalive &&
+ p.Endpoints == q.Endpoints && cidrsEqual(p.AllowedIPs, q.AllowedIPs)
+ }
+ if !peersEqual(peer0(origCfg), peer0(newCfg)) {
+ t.Error("reconfig modified old peer")
+ }
+ })
+
+ t.Run("device1 remove peer", func(t *testing.T) {
+ removeKey := cfg1.Peers[len(cfg1.Peers)-1].PublicKey
+ cfg1.Peers = cfg1.Peers[:len(cfg1.Peers)-1]
+
+ if err := ReconfigDevice(device1, cfg1, t.Logf); err != nil {
+ t.Fatal(err)
+ }
+ cmp(t, device1, cfg1)
+
+ newCfg, err := DeviceConfig(device1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, ok := newCfg.PeerWithKey(removeKey)
+ if ok {
+ t.Error("reconfig failed to remove peer")
+ }
+ })
+}
+
+// TODO: replace with a loopback tunnel
+type nilTun struct {
+ events chan tun.Event
+ closed chan struct{}
+}
+
+func newNilTun() tun.Device {
+ return &nilTun{
+ events: make(chan tun.Event),
+ closed: make(chan struct{}),
+ }
+}
+
+func (t *nilTun) File() *os.File { return nil }
+func (t *nilTun) Flush() error { return nil }
+func (t *nilTun) MTU() (int, error) { return 1420, nil }
+func (t *nilTun) Name() (string, error) { return "niltun", nil }
+func (t *nilTun) Events() chan tun.Event { return t.events }
+
+func (t *nilTun) Read(data []byte, offset int) (int, error) {
+ <-t.closed
+ return 0, io.EOF
+}
+
+func (t *nilTun) Write(data []byte, offset int) (int, error) {
+ <-t.closed
+ return 0, io.EOF
+}
+
+func (t *nilTun) Close() error {
+ close(t.events)
+ close(t.closed)
+ return nil
+}
diff --git a/wgengine/wgcfg/key.go b/wgengine/wgcfg/key.go
new file mode 100644
index 000000000..48601df98
--- /dev/null
+++ b/wgengine/wgcfg/key.go
@@ -0,0 +1,240 @@
+// 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 wgcfg
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/subtle"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "strings"
+
+ "golang.org/x/crypto/chacha20poly1305"
+ "golang.org/x/crypto/curve25519"
+)
+
+const KeySize = 32
+
+// Key is curve25519 key.
+// It is used by WireGuard to represent public and preshared keys.
+type Key [KeySize]byte
+
+// NewPresharedKey generates a new random key.
+func NewPresharedKey() (*Key, error) {
+ var k [KeySize]byte
+ _, err := rand.Read(k[:])
+ if err != nil {
+ return nil, err
+ }
+ return (*Key)(&k), nil
+}
+
+func ParseKey(b64 string) (*Key, error) { return parseKeyBase64(base64.StdEncoding, b64) }
+
+func ParseHexKey(s string) (Key, error) {
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ return Key{}, &ParseError{"invalid hex key: " + err.Error(), s}
+ }
+ if len(b) != KeySize {
+ return Key{}, &ParseError{fmt.Sprintf("invalid hex key length: %d", len(b)), s}
+ }
+
+ var key Key
+ copy(key[:], b)
+ return key, nil
+}
+
+func ParsePrivateHexKey(v string) (PrivateKey, error) {
+ k, err := ParseHexKey(v)
+ if err != nil {
+ return PrivateKey{}, err
+ }
+ pk := PrivateKey(k)
+ if pk.IsZero() {
+ // Do not clamp a zero key, pass the zero through
+ // (much like NaN propagation) so that IsZero reports
+ // a useful result.
+ return pk, nil
+ }
+ pk.clamp()
+ return pk, nil
+}
+
+func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
+func (k Key) String() string { return k.ShortString() }
+func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
+func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
+
+func (k *Key) ShortString() string {
+ long := k.Base64()
+ return "[" + long[0:5] + "]"
+}
+
+func (k *Key) IsZero() bool {
+ if k == nil {
+ return true
+ }
+ var zeros Key
+ return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
+}
+
+func (k *Key) MarshalJSON() ([]byte, error) {
+ if k == nil {
+ return []byte("null"), nil
+ }
+ buf := new(bytes.Buffer)
+ fmt.Fprintf(buf, `"%x"`, k[:])
+ return buf.Bytes(), nil
+}
+
+func (k *Key) UnmarshalJSON(b []byte) error {
+ if k == nil {
+ return errors.New("wgcfg.Key: UnmarshalJSON on nil pointer")
+ }
+ if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
+ return errors.New("wgcfg.Key: UnmarshalJSON not given a string")
+ }
+ b = b[1 : len(b)-1]
+ key, err := ParseHexKey(string(b))
+ if err != nil {
+ return fmt.Errorf("wgcfg.Key: UnmarshalJSON: %v", err)
+ }
+ copy(k[:], key[:])
+ return nil
+}
+
+func (a *Key) LessThan(b *Key) bool {
+ for i := range a {
+ if a[i] < b[i] {
+ return true
+ } else if a[i] > b[i] {
+ return false
+ }
+ }
+ return false
+}
+
+// PrivateKey is curve25519 key.
+// It is used by WireGuard to represent private keys.
+type PrivateKey [KeySize]byte
+
+// NewPrivateKey generates a new curve25519 secret key.
+// It conforms to the format described on https://cr.yp.to/ecdh.html.
+func NewPrivateKey() (PrivateKey, error) {
+ k, err := NewPresharedKey()
+ if err != nil {
+ return PrivateKey{}, err
+ }
+ k[0] &= 248
+ k[31] = (k[31] & 127) | 64
+ return (PrivateKey)(*k), nil
+}
+
+func ParsePrivateKey(b64 string) (*PrivateKey, error) {
+ k, err := parseKeyBase64(base64.StdEncoding, b64)
+ return (*PrivateKey)(k), err
+}
+
+func (k *PrivateKey) String() string { return base64.StdEncoding.EncodeToString(k[:]) }
+func (k *PrivateKey) HexString() string { return hex.EncodeToString(k[:]) }
+func (k *PrivateKey) Equal(k2 PrivateKey) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
+
+func (k *PrivateKey) IsZero() bool {
+ pk := Key(*k)
+ return pk.IsZero()
+}
+
+func (k *PrivateKey) clamp() {
+ k[0] &= 248
+ k[31] = (k[31] & 127) | 64
+}
+
+// Public computes the public key matching this curve25519 secret key.
+func (k *PrivateKey) Public() Key {
+ pk := Key(*k)
+ if pk.IsZero() {
+ panic("Tried to generate emptyPrivateKey.Public()")
+ }
+ var p [KeySize]byte
+ curve25519.ScalarBaseMult(&p, (*[KeySize]byte)(k))
+ return (Key)(p)
+}
+
+func (k PrivateKey) MarshalText() ([]byte, error) {
+ buf := new(bytes.Buffer)
+ fmt.Fprintf(buf, `privkey:%x`, k[:])
+ return buf.Bytes(), nil
+}
+
+func (k *PrivateKey) UnmarshalText(b []byte) error {
+ s := string(b)
+ if !strings.HasPrefix(s, `privkey:`) {
+ return errors.New("wgcfg.PrivateKey: UnmarshalText not given a private-key string")
+ }
+ s = strings.TrimPrefix(s, `privkey:`)
+ key, err := ParseHexKey(s)
+ if err != nil {
+ return fmt.Errorf("wgcfg.PrivateKey: UnmarshalText: %v", err)
+ }
+ copy(k[:], key[:])
+ return nil
+}
+
+func (k PrivateKey) SharedSecret(pub Key) (ss [KeySize]byte) {
+ apk := (*[KeySize]byte)(&pub)
+ ask := (*[KeySize]byte)(&k)
+ curve25519.ScalarMult(&ss, ask, apk) //lint:ignore SA1019 Jason says this is OK; match wireguard-go exactyl
+ return ss
+}
+
+func parseKeyBase64(enc *base64.Encoding, s string) (*Key, error) {
+ k, err := enc.DecodeString(s)
+ if err != nil {
+ return nil, &ParseError{"Invalid key: " + err.Error(), s}
+ }
+ if len(k) != KeySize {
+ return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
+ }
+ var key Key
+ copy(key[:], k)
+ return &key, nil
+}
+
+func ParseSymmetricKey(b64 string) (SymmetricKey, error) {
+ k, err := parseKeyBase64(base64.StdEncoding, b64)
+ if err != nil {
+ return SymmetricKey{}, err
+ }
+ return SymmetricKey(*k), nil
+}
+
+func ParseSymmetricHexKey(s string) (SymmetricKey, error) {
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ return SymmetricKey{}, &ParseError{"invalid symmetric hex key: " + err.Error(), s}
+ }
+ if len(b) != chacha20poly1305.KeySize {
+ return SymmetricKey{}, &ParseError{fmt.Sprintf("invalid symmetric hex key length: %d", len(b)), s}
+ }
+ var key SymmetricKey
+ copy(key[:], b)
+ return key, nil
+}
+
+// SymmetricKey is a chacha20poly1305 key.
+// It is used by WireGuard to represent pre-shared symmetric keys.
+type SymmetricKey [chacha20poly1305.KeySize]byte
+
+func (k SymmetricKey) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
+func (k SymmetricKey) String() string { return "sym:" + k.Base64()[:8] }
+func (k SymmetricKey) HexString() string { return hex.EncodeToString(k[:]) }
+func (k SymmetricKey) IsZero() bool { return k.Equal(SymmetricKey{}) }
+func (k SymmetricKey) Equal(k2 SymmetricKey) bool {
+ return subtle.ConstantTimeCompare(k[:], k2[:]) == 1
+}
diff --git a/wgengine/wgcfg/key_test.go b/wgengine/wgcfg/key_test.go
new file mode 100644
index 000000000..709b1afcc
--- /dev/null
+++ b/wgengine/wgcfg/key_test.go
@@ -0,0 +1,111 @@
+// 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 wgcfg
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestKeyBasics(t *testing.T) {
+ k1, err := NewPresharedKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b, err := k1.MarshalJSON()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run("JSON round-trip", func(t *testing.T) {
+ // should preserve the keys
+ k2 := new(Key)
+ if err := k2.UnmarshalJSON(b); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(k1[:], k2[:]) {
+ t.Fatalf("k1 %v != k2 %v", k1[:], k2[:])
+ }
+ if b1, b2 := k1.String(), k2.String(); b1 != b2 {
+ t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
+ }
+ })
+
+ t.Run("JSON incompatible with PrivateKey", func(t *testing.T) {
+ k2 := new(PrivateKey)
+ if err := k2.UnmarshalText(b); err == nil {
+ t.Fatalf("successfully decoded key as private key")
+ }
+ })
+
+ t.Run("second key", func(t *testing.T) {
+ // A second call to NewPresharedKey should make a new key.
+ k3, err := NewPresharedKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bytes.Equal(k1[:], k3[:]) {
+ t.Fatalf("k1 %v == k3 %v", k1[:], k3[:])
+ }
+ // Check for obvious comparables to make sure we are not generating bad strings somewhere.
+ if b1, b2 := k1.String(), k3.String(); b1 == b2 {
+ t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
+ }
+ })
+}
+func TestPrivateKeyBasics(t *testing.T) {
+ pri, err := NewPrivateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ b, err := pri.MarshalText()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run("JSON round-trip", func(t *testing.T) {
+ // should preserve the keys
+ pri2 := new(PrivateKey)
+ if err := pri2.UnmarshalText(b); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(pri[:], pri2[:]) {
+ t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:])
+ }
+ if b1, b2 := pri.String(), pri2.String(); b1 != b2 {
+ t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
+ }
+ if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 {
+ t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2)
+ }
+ })
+
+ t.Run("JSON incompatible with Key", func(t *testing.T) {
+ k2 := new(Key)
+ if err := k2.UnmarshalJSON(b); err == nil {
+ t.Fatalf("successfully decoded private key as key")
+ }
+ })
+
+ t.Run("second key", func(t *testing.T) {
+ // A second call to New should make a new key.
+ pri3, err := NewPrivateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bytes.Equal(pri[:], pri3[:]) {
+ t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:])
+ }
+ // Check for obvious comparables to make sure we are not generating bad strings somewhere.
+ if b1, b2 := pri.String(), pri3.String(); b1 == b2 {
+ t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
+ }
+ if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 {
+ t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2)
+ }
+ })
+}
diff --git a/wgengine/wgcfg/parser.go b/wgengine/wgcfg/parser.go
new file mode 100644
index 000000000..bf0b45835
--- /dev/null
+++ b/wgengine/wgcfg/parser.go
@@ -0,0 +1,201 @@
+// 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 wgcfg
+
+import (
+ "bufio"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net"
+ "strconv"
+ "strings"
+
+ "inet.af/netaddr"
+)
+
+type ParseError struct {
+ why string
+ offender string
+}
+
+func (e *ParseError) Error() string {
+ return fmt.Sprintf("%s: %q", e.why, e.offender)
+}
+
+func validateEndpoints(s string) error {
+ if s == "" {
+ // Otherwise strings.Split of the empty string produces [""].
+ return nil
+ }
+ vals := strings.Split(s, ",")
+ for _, val := range vals {
+ _, _, err := parseEndpoint(val)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func parseEndpoint(s string) (host string, port uint16, err error) {
+ i := strings.LastIndexByte(s, ':')
+ if i < 0 {
+ return "", 0, &ParseError{"Missing port from endpoint", s}
+ }
+ host, portStr := s[:i], s[i+1:]
+ if len(host) < 1 {
+ return "", 0, &ParseError{"Invalid endpoint host", host}
+ }
+ uport, err := strconv.ParseUint(portStr, 10, 16)
+ if err != nil {
+ return "", 0, err
+ }
+ hostColon := strings.IndexByte(host, ':')
+ if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
+ err := &ParseError{"Brackets must contain an IPv6 address", host}
+ if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
+ maybeV6 := net.ParseIP(host[1 : len(host)-1])
+ if maybeV6 == nil || len(maybeV6) != net.IPv6len {
+ return "", 0, err
+ }
+ } else {
+ return "", 0, err
+ }
+ host = host[1 : len(host)-1]
+ }
+ return host, uint16(uport), nil
+}
+
+func parseKeyHex(s string) (*Key, error) {
+ k, err := hex.DecodeString(s)
+ if err != nil {
+ return nil, &ParseError{"Invalid key: " + err.Error(), s}
+ }
+ if len(k) != KeySize {
+ return nil, &ParseError{"Keys must decode to exactly 32 bytes", s}
+ }
+ var key Key
+ copy(key[:], k)
+ return &key, nil
+}
+
+// FromUAPI generates a Config from r.
+// r should be generated by calling device.IpcGetOperation;
+// it is not compatible with other uapi streams.
+func FromUAPI(r io.Reader) (*Config, error) {
+ cfg := new(Config)
+ var peer *Peer // current peer being operated on
+ deviceConfig := true
+
+ scanner := bufio.NewScanner(r)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if line == "" {
+ continue
+ }
+ parts := strings.Split(line, "=")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("failed to parse line %q, found %d =-separated parts, want 2", line, len(parts))
+ }
+ key := parts[0]
+ value := parts[1]
+
+ if key == "public_key" {
+ if deviceConfig {
+ deviceConfig = false
+ }
+ // Load/create the peer we are now configuring.
+ var err error
+ peer, err = cfg.handlePublicKeyLine(value)
+ if err != nil {
+ return nil, err
+ }
+ continue
+ }
+
+ var err error
+ if deviceConfig {
+ err = cfg.handleDeviceLine(key, value)
+ } else {
+ err = cfg.handlePeerLine(peer, key, value)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return cfg, nil
+}
+
+func (cfg *Config) handleDeviceLine(key, value string) error {
+ switch key {
+ case "private_key":
+ k, err := parseKeyHex(value)
+ if err != nil {
+ return err
+ }
+ // wireguard-go guarantees not to send zero value; private keys are already clamped.
+ cfg.PrivateKey = PrivateKey(*k)
+ case "listen_port":
+ port, err := strconv.ParseUint(value, 10, 16)
+ if err != nil {
+ return fmt.Errorf("failed to parse listen_port: %w", err)
+ }
+ cfg.ListenPort = uint16(port)
+ case "fwmark":
+ // ignore
+ default:
+ return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
+ }
+ return nil
+}
+
+func (cfg *Config) handlePublicKeyLine(value string) (*Peer, error) {
+ k, err := parseKeyHex(value)
+ if err != nil {
+ return nil, err
+ }
+ cfg.Peers = append(cfg.Peers, Peer{})
+ peer := &cfg.Peers[len(cfg.Peers)-1]
+ peer.PublicKey = *k
+ return peer, nil
+}
+
+func (cfg *Config) handlePeerLine(peer *Peer, key, value string) error {
+ switch key {
+ case "endpoint":
+ err := validateEndpoints(value)
+ if err != nil {
+ return err
+ }
+ peer.Endpoints = value
+ case "persistent_keepalive_interval":
+ n, err := strconv.ParseUint(value, 10, 16)
+ if err != nil {
+ return err
+ }
+ peer.PersistentKeepalive = uint16(n)
+ case "allowed_ip":
+ ipp, err := netaddr.ParseIPPrefix(value)
+ if err != nil {
+ return err
+ }
+ peer.AllowedIPs = append(peer.AllowedIPs, ipp)
+ case "protocol_version":
+ if value != "1" {
+ return fmt.Errorf("invalid protocol version: %v", value)
+ }
+ case "preshared_key", "last_handshake_time_sec", "last_handshake_time_nsec", "tx_bytes", "rx_bytes":
+ // ignore
+ default:
+ return fmt.Errorf("unexpected IpcGetOperation key: %v", key)
+ }
+ return nil
+}
diff --git a/wgengine/wgcfg/parser_test.go b/wgengine/wgcfg/parser_test.go
new file mode 100644
index 000000000..9d4fe1992
--- /dev/null
+++ b/wgengine/wgcfg/parser_test.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 wgcfg
+
+import (
+ "reflect"
+ "runtime"
+ "testing"
+)
+
+func noError(t *testing.T, err error) bool {
+ if err == nil {
+ return true
+ }
+ _, fn, line, _ := runtime.Caller(1)
+ t.Errorf("Error at %s:%d: %#v", fn, line, err)
+ return false
+}
+
+func equal(t *testing.T, expected, actual interface{}) bool {
+ if reflect.DeepEqual(expected, actual) {
+ return true
+ }
+ _, fn, line, _ := runtime.Caller(1)
+ t.Errorf("Failed equals at %s:%d\nactual %#v\nexpected %#v", fn, line, actual, expected)
+ return false
+}
+
+func TestParseEndpoint(t *testing.T) {
+ _, _, err := parseEndpoint("[192.168.42.0:]:51880")
+ if err == nil {
+ t.Error("Error was expected")
+ }
+ host, port, err := parseEndpoint("192.168.42.0:51880")
+ if noError(t, err) {
+ equal(t, "192.168.42.0", host)
+ equal(t, uint16(51880), port)
+ }
+ host, port, err = parseEndpoint("test.wireguard.com:18981")
+ if noError(t, err) {
+ equal(t, "test.wireguard.com", host)
+ equal(t, uint16(18981), port)
+ }
+ host, port, err = parseEndpoint("[2607:5300:60:6b0::c05f:543]:2468")
+ if noError(t, err) {
+ equal(t, "2607:5300:60:6b0::c05f:543", host)
+ equal(t, uint16(2468), port)
+ }
+ _, _, err = parseEndpoint("[::::::invalid:18981")
+ if err == nil {
+ t.Error("Error was expected")
+ }
+}
+
+func TestValidateEndpoints(t *testing.T) {
+ tests := []struct {
+ in string
+ want error
+ }{
+ {"", nil},
+ {"1.2.3.4:5", nil},
+ {"1.2.3.4:5,6.7.8.9:10", nil},
+ {",", &ParseError{why: "Missing port from endpoint", offender: ""}},
+ }
+ for _, tt := range tests {
+ got := validateEndpoints(tt.in)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("%q = %#v (%s); want %#v (%s)", tt.in, got, got, tt.want, tt.want)
+ }
+ }
+}
diff --git a/wgengine/wgcfg/writer.go b/wgengine/wgcfg/writer.go
new file mode 100644
index 000000000..079c1eb5e
--- /dev/null
+++ b/wgengine/wgcfg/writer.go
@@ -0,0 +1,141 @@
+// 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 wgcfg
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+
+ "inet.af/netaddr"
+)
+
+// ToUAPI writes cfg in UAPI format to w.
+// Prev is the previous device Config.
+// Prev is required so that we can remove now-defunct peers
+// without having to remove and re-add all peers.
+func (cfg *Config) ToUAPI(w io.Writer, prev *Config) error {
+ var stickyErr error
+ set := func(key, value string) {
+ if stickyErr != nil {
+ return
+ }
+ _, err := fmt.Fprintf(w, "%s=%s\n", key, value)
+ if err != nil {
+ stickyErr = err
+ }
+ }
+ setUint16 := func(key string, value uint16) {
+ set(key, strconv.FormatUint(uint64(value), 10))
+ }
+ setPeer := func(peer Peer) {
+ set("public_key", peer.PublicKey.HexString())
+ }
+
+ // Device config.
+ if prev.PrivateKey != cfg.PrivateKey {
+ set("private_key", cfg.PrivateKey.HexString())
+ }
+ if prev.ListenPort != cfg.ListenPort {
+ setUint16("listen_port", cfg.ListenPort)
+ }
+
+ old := make(map[Key]Peer)
+ for _, p := range prev.Peers {
+ old[p.PublicKey] = p
+ }
+
+ // Add/configure all new peers.
+ for _, p := range cfg.Peers {
+ oldPeer := old[p.PublicKey]
+ setPeer(p)
+ set("protocol_version", "1")
+
+ if !endpointsEqual(oldPeer.Endpoints, p.Endpoints) {
+ set("endpoint", p.Endpoints)
+ }
+
+ // TODO: replace_allowed_ips is expensive.
+ // If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
+ // then skip replace_allowed_ips and instead add only
+ // the new ipps with allowed_ip.
+ if !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) {
+ set("replace_allowed_ips", "true")
+ for _, ipp := range p.AllowedIPs {
+ set("allowed_ip", ipp.String())
+ }
+ }
+
+ // Set PersistentKeepalive after the peer is otherwise configured,
+ // because it can trigger handshake packets.
+ if oldPeer.PersistentKeepalive != p.PersistentKeepalive {
+ setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
+ }
+ }
+
+ // Remove peers that were present but should no longer be.
+ for _, p := range cfg.Peers {
+ delete(old, p.PublicKey)
+ }
+ for _, p := range old {
+ setPeer(p)
+ set("remove", "true")
+ }
+
+ if stickyErr != nil {
+ stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
+ }
+ return stickyErr
+}
+
+func endpointsEqual(x, y string) bool {
+ // Cheap comparisons.
+ if x == y {
+ return true
+ }
+ xs := strings.Split(x, ",")
+ ys := strings.Split(y, ",")
+ if len(xs) != len(ys) {
+ return false
+ }
+ // Otherwise, see if they're the same, but out of order.
+ sort.Strings(xs)
+ sort.Strings(ys)
+ x = strings.Join(xs, ",")
+ y = strings.Join(ys, ",")
+ return x == y
+}
+
+func cidrsEqual(x, y []netaddr.IPPrefix) bool {
+ // TODO: re-implement using netaddr.IPSet.Equal.
+ if len(x) != len(y) {
+ return false
+ }
+ // First see if they're equal in order, without allocating.
+ exact := true
+ for i := range x {
+ if x[i] != y[i] {
+ exact = false
+ break
+ }
+ }
+ if exact {
+ return true
+ }
+
+ // Otherwise, see if they're the same, but out of order.
+ m := make(map[netaddr.IPPrefix]bool)
+ for _, v := range x {
+ m[v] = true
+ }
+ for _, v := range y {
+ if !m[v] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go
index 3139dc1f2..563888083 100644
--- a/wgengine/wgengine.go
+++ b/wgengine/wgengine.go
@@ -8,7 +8,6 @@ import (
"errors"
"time"
- "github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
@@ -17,6 +16,7 @@ import (
"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.
diff --git a/wgengine/wglog/wglog.go b/wgengine/wglog/wglog.go
index e833bb27a..7786edd82 100644
--- a/wgengine/wglog/wglog.go
+++ b/wgengine/wglog/wglog.go
@@ -12,8 +12,8 @@ import (
"sync/atomic"
"github.com/tailscale/wireguard-go/device"
- "github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/wgcfg"
)
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
@@ -36,6 +36,10 @@ func NewLogger(logf logger.Logf) *Logger {
// Drop those; there are a lot of them, and they're just noise.
return
}
+ if strings.Contains(msg, "Failed to send data packet") {
+ // Drop. See https://github.com/tailscale/tailscale/issues/1239.
+ return
+ }
r := ret.replacer.Load()
if r == nil {
// No replacements specified; log as originally planned.
diff --git a/wgengine/wglog/wglog_test.go b/wgengine/wglog/wglog_test.go
index 3a899839e..0b93a130a 100644
--- a/wgengine/wglog/wglog_test.go
+++ b/wgengine/wglog/wglog_test.go
@@ -8,7 +8,7 @@ import (
"fmt"
"testing"
- "github.com/tailscale/wireguard-go/wgcfg"
+ "tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wglog"
)