From 4dab0c17024de47eea4c90d7660dd7ae3a6e5b22 Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Wed, 27 Jan 2021 11:50:31 -0500 Subject: [PATCH 01/23] tailcfg: update node display name fields and methods (#1207) Signed-off-by: Sonia Appasamy Consolidates the node display name logic from each of the clients into tailcfg.Node. UI clients can use these names directly, rather than computing them independently. --- control/controlclient/direct.go | 11 ++-- control/controlclient/netmap.go | 5 +- tailcfg/tailcfg.go | 111 ++++++++++++++++++++++++++------ tailcfg/tailcfg_clone.go | 40 ++++++------ tailcfg/tailcfg_test.go | 3 +- util/dnsname/dnsname.go | 8 --- util/dnsname/dnsname_test.go | 18 ------ 7 files changed, 121 insertions(+), 75 deletions(-) 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..c3007c275 100644 --- a/control/controlclient/netmap.go +++ b/control/controlclient/netmap.go @@ -24,13 +24,12 @@ import ( 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/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 098c95d56..4e7c51f79 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 { 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/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) - } - } -} From 9f5b0d058f4d1e976e617fd4737401bd6f1ddafc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 Jan 2021 10:30:57 -0800 Subject: [PATCH 02/23] wgengine: fix bugs from earlier fix Fixes a regression from e970ed09951a that wasn't covered by tests in this repo. (Our end-to-end tests in another repo caught this.) Updates #1204 --- wgengine/userspace.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index d4746487a..b206a84a6 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1035,6 +1035,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 +1062,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 +1099,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 +1112,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 From 4d943536f152093085978adc06ab55e68bb75c53 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 Jan 2021 10:59:47 -0800 Subject: [PATCH 03/23] wgengine: don't leak TUN device in NewUserspaceEngine error path Updates #1187 Signed-off-by: Brad Fitzpatrick --- wgengine/userspace.go | 1 + 1 file changed, 1 insertion(+) diff --git a/wgengine/userspace.go b/wgengine/userspace.go index b206a84a6..5dbfd3f17 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -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 From 7a16ac80b7bd0189b2b173f957646d0940f3444c Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 27 Jan 2021 18:45:22 -0800 Subject: [PATCH 04/23] VERSION.txt: this is 1.5.0. --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 1e28207a152fd1aaccbae7e046d2ed01b51f4970 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Thu, 28 Jan 2021 10:03:27 -0800 Subject: [PATCH 05/23] types/logger: fix rateFree interaction with verbosity prefixes We log lines like this: c.logf("[v1] magicsock: disco: %v->%v (%v, %v) sent %v", c.discoShort, dstDisco.ShortString(), dstKey.ShortString(), derpStr(dst.String()), disco.MessageSummary(m)) The leading [v1] causes it to get unintentionally rate limited. Until we have a proper fix, work around it. Fixes #1216 Signed-off-by: Josh Bleecher Snyder --- types/logger/logger.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 } } From de497358b84f4b368ce9b637b98ea8899d6b337e Mon Sep 17 00:00:00 2001 From: David Anderson Date: Thu, 28 Jan 2021 12:57:10 -0800 Subject: [PATCH 06/23] cmd/tailscaled: add /run to the allowed paths for iptables. Signed-off-by: David Anderson --- cmd/tailscaled/tailscaled.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/tailscaled/tailscaled.service b/cmd/tailscaled/tailscaled.service index 878e86341..7b847c54e 100644 --- a/cmd/tailscaled/tailscaled.service +++ b/cmd/tailscaled/tailscaled.service @@ -34,6 +34,8 @@ ProtectHome=true ProtectKernelTunables=true ProtectSystem=strict ReadWritePaths=/etc/ +ReadWritePaths=/run/ +ReadWritePaths=/var/run/ RestrictSUIDSGID=true SystemCallArchitectures=native From c7fc4a06dad9eaa5c42e525ddd69d50aede6c919 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 28 Jan 2021 08:06:56 -0800 Subject: [PATCH 07/23] wgengine/router: don't configure IPv6 on Linux when IPv6 is unavailable Fixes #1214 Signed-off-by: Brad Fitzpatrick --- wgengine/router/router_linux.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index f65d12742..2f92d5d69 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -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), From c611d8480bb4a7af35afff3d61ad48168de0d8d6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 28 Jan 2021 15:29:17 -0800 Subject: [PATCH 08/23] cmd/tailscaled: add whois/identd-ish debug handler --- ipn/ipnserver/server.go | 40 +++++++++++++++++++++++++++++++++ ipn/local.go | 49 +++++++++++++++++++++++++++++++++++++++++ tailcfg/tailcfg.go | 6 +++++ 3 files changed, 95 insertions(+) diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 7518c6e2a..69d160d52 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" @@ -620,6 +622,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("/whois", whoIsHandler{b}) } server.b = b @@ -883,3 +886,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..335be2244 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -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/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 4e7c51f79..ba215000e 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -935,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 +} From 0bc73f8e4fdd7cda57775f880bcddeb64a83c267 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 28 Jan 2021 16:06:23 -0800 Subject: [PATCH 09/23] cmd/hello: new hello.ipn.dev server Signed-off-by: Brad Fitzpatrick --- cmd/hello/hello.go | 116 ++++++++++++++++++++++++++++++++++++++ cmd/hello/hello.tmpl.html | 17 ++++++ 2 files changed, 133 insertions(+) create mode 100644 cmd/hello/hello.go create mode 100644 cmd/hello/hello.tmpl.html diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go new file mode 100644 index 000000000..1f7439c0c --- /dev/null +++ b/cmd/hello/hello.go @@ -0,0 +1,116 @@ +// 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 ( + "encoding/json" + "flag" + "fmt" + "html/template" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "strings" + + "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, + }) +} + +func whoIs(ip string) (*tailcfg.WhoIsResponse, error) { + res, err := http.Get("http://127.0.0.1:4242/whois?ip=" + url.QueryEscape(ip)) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + slurp, _ := ioutil.ReadAll(res.Body) + return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp) + } + r := new(tailcfg.WhoIsResponse) + return r, json.NewDecoder(res.Body).Decode(r) +} 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. +

+ + From fe7c3e9c172622438f85fedcb28aa06c30ec6cb7 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 29 Jan 2021 12:16:36 -0800 Subject: [PATCH 10/23] all: move wgcfg from wireguard-go This is mostly code movement from the wireguard-go repo. Most of the new wgcfg package corresponds to the wireguard-go wgcfg package. wgengine/wgcfg/device{_test}.go was device/config{_test}.go. There were substantive but simple changes to device_test.go to remove internal package device references. The API of device.Config (now wgcfg.DeviceConfig) grew an error return; we previously logged the error and threw it away. Signed-off-by: Josh Bleecher Snyder --- cmd/tailscale/depaware.txt | 2 +- cmd/tailscaled/depaware.txt | 2 +- control/controlclient/netmap.go | 2 +- go.mod | 2 +- go.sum | 2 + internal/deepprint/deepprint_test.go | 2 +- ipn/local.go | 2 +- wgengine/magicsock/legacy.go | 2 +- wgengine/magicsock/magicsock_test.go | 10 +- wgengine/userspace.go | 6 +- wgengine/userspace_test.go | 2 +- wgengine/watchdog.go | 2 +- wgengine/wgcfg/config.go | 67 ++++++++ wgengine/wgcfg/device.go | 61 +++++++ wgengine/wgcfg/device_test.go | 242 +++++++++++++++++++++++++++ wgengine/wgcfg/key.go | 240 ++++++++++++++++++++++++++ wgengine/wgcfg/key_test.go | 111 ++++++++++++ wgengine/wgcfg/parser.go | 197 ++++++++++++++++++++++ wgengine/wgcfg/parser_test.go | 55 ++++++ wgengine/wgcfg/writer.go | 141 ++++++++++++++++ wgengine/wgengine.go | 2 +- wgengine/wglog/wglog.go | 2 +- wgengine/wglog/wglog_test.go | 2 +- 23 files changed, 1138 insertions(+), 18 deletions(-) create mode 100644 wgengine/wgcfg/config.go create mode 100644 wgengine/wgcfg/device.go create mode 100644 wgengine/wgcfg/device_test.go create mode 100644 wgengine/wgcfg/key.go create mode 100644 wgengine/wgcfg/key_test.go create mode 100644 wgengine/wgcfg/parser.go create mode 100644 wgengine/wgcfg/parser_test.go create mode 100644 wgengine/wgcfg/writer.go 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/control/controlclient/netmap.go b/control/controlclient/netmap.go index c3007c275..40041a491 100644 --- a/control/controlclient/netmap.go +++ b/control/controlclient/netmap.go @@ -13,12 +13,12 @@ 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 { diff --git a/go.mod b/go.mod index c306a8d67..9fca67cc8 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-20210129202040-ddaf8316eff8 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..4ffdb0116 100644 --- a/go.sum +++ b/go.sum @@ -296,6 +296,8 @@ 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/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/local.go b/ipn/local.go index 335be2244..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() diff --git a/wgengine/magicsock/legacy.go b/wgengine/magicsock/legacy.go index 8fe34e5d8..856700925 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 ( 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/userspace.go b/wgengine/userspace.go index 5dbfd3f17..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" ) @@ -836,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 } @@ -844,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 } 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..518a93992 --- /dev/null +++ b/wgengine/wgcfg/parser.go @@ -0,0 +1,197 @@ +// 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: ā€˜%s’", e.why, e.offender) +} + +func validateEndpoints(s string) error { + 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..e101a3a05 --- /dev/null +++ b/wgengine/wgcfg/parser_test.go @@ -0,0 +1,55 @@ +// 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") + } +} 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..6f3e490f2 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. 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" ) From 006a224f50bd7c74307567463a19f296affca5bd Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 29 Jan 2021 13:18:23 -0800 Subject: [PATCH 11/23] ipn/ipnserver, cmd/hello: do whois over unix socket, not debug http Start of a local HTTP API. Not a stable interface yet. --- cmd/hello/hello.go | 26 +++++++++++++++++++++++--- ipn/ipnserver/server.go | 19 +++++++++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index 1f7439c0c..b88a93244 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -6,6 +6,7 @@ package main // import "tailscale.com/cmd/hello" import ( + "context" "encoding/json" "flag" "fmt" @@ -101,16 +102,35 @@ func root(w http.ResponseWriter, r *http.Request) { }) } +// tsSockClient does HTTP requests to the local tailscaled by dialing +// its Unix socket or whatever type of connection is required on the local +// system. +// TODO(bradfitz): do the macOS dial-the-sandbox dance like cmd/tailscale does. +var tsSockClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", "/var/run/tailscale/tailscaled.sock") + }, + }, +} + func whoIs(ip string) (*tailcfg.WhoIsResponse, error) { - res, err := http.Get("http://127.0.0.1:4242/whois?ip=" + url.QueryEscape(ip)) + 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 { - slurp, _ := ioutil.ReadAll(res.Body) return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp) } r := new(tailcfg.WhoIsResponse) - return r, json.NewDecoder(res.Body).Decode(r) + 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/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 69d160d52..dd8a39ab6 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -115,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 @@ -127,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 { @@ -622,7 +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("/whois", whoIsHandler{b}) + opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b}) } server.b = b @@ -863,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, "Tailscale

Tailscale

This is the local Tailscale daemon.") return From 60e189f6999497203d55c5c80b6e454d334a5e7b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 29 Jan 2021 13:49:17 -0800 Subject: [PATCH 12/23] cmd/hello: use safesocket client to connect --- cmd/hello/hello.go | 10 ++++------ safesocket/safesocket.go | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/hello/hello.go b/cmd/hello/hello.go index b88a93244..bd4145144 100644 --- a/cmd/hello/hello.go +++ b/cmd/hello/hello.go @@ -18,6 +18,7 @@ import ( "net/url" "strings" + "tailscale.com/safesocket" "tailscale.com/tailcfg" ) @@ -102,15 +103,12 @@ func root(w http.ResponseWriter, r *http.Request) { }) } -// tsSockClient does HTTP requests to the local tailscaled by dialing -// its Unix socket or whatever type of connection is required on the local -// system. -// TODO(bradfitz): do the macOS dial-the-sandbox dance like cmd/tailscale does. +// 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) { - var d net.Dialer - return d.DialContext(ctx, "unix", "/var/run/tailscale/tailscaled.sock") + return safesocket.ConnectDefault() }, }, } diff --git a/safesocket/safesocket.go b/safesocket/safesocket.go index ba23a1d65..3fbacc4a2 100644 --- a/safesocket/safesocket.go +++ b/safesocket/safesocket.go @@ -27,6 +27,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) From 914a486af6cde1c9e06c76c27b596b9658fbc18c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 29 Jan 2021 14:32:56 -0800 Subject: [PATCH 13/23] safesocket: refactor macOS auth code, pull out separate LocalTCPPortAndToken --- safesocket/basic_test.go | 2 +- safesocket/safesocket.go | 20 +++++++++++++ safesocket/safesocket_darwin.go | 52 +++++++++++++++++++++++++++++++++ safesocket/safesocket_test.go | 13 +++++++++ safesocket/unixsocket.go | 40 +++++++------------------ 5 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 safesocket/safesocket_darwin.go create mode 100644 safesocket/safesocket_test.go 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 3fbacc4a2..19e183463 100644 --- a/safesocket/safesocket.go +++ b/safesocket/safesocket.go @@ -7,7 +7,9 @@ package safesocket import ( + "errors" "net" + "runtime" ) type closeable interface { @@ -43,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) } From 761188e5d2bc837ad07d8ab84404627b384f32d3 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 30 Jan 2021 11:17:55 -0800 Subject: [PATCH 14/23] wgengine/wgcfg: fix validateEndpoints of empty string Updates tailscale/corp#1238 --- wgengine/wgcfg/parser.go | 6 +++++- wgengine/wgcfg/parser_test.go | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/wgengine/wgcfg/parser.go b/wgengine/wgcfg/parser.go index 518a93992..bf0b45835 100644 --- a/wgengine/wgcfg/parser.go +++ b/wgengine/wgcfg/parser.go @@ -22,10 +22,14 @@ type ParseError struct { } func (e *ParseError) Error() string { - return fmt.Sprintf("%s: ā€˜%s’", e.why, e.offender) + 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) diff --git a/wgengine/wgcfg/parser_test.go b/wgengine/wgcfg/parser_test.go index e101a3a05..9d4fe1992 100644 --- a/wgengine/wgcfg/parser_test.go +++ b/wgengine/wgcfg/parser_test.go @@ -53,3 +53,21 @@ func TestParseEndpoint(t *testing.T) { 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) + } + } +} From 2889fabaefc50040507ead652d6d2b212f476c2b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Feb 2021 13:12:15 -0800 Subject: [PATCH 15/23] cmd/tailscaled/tailscaled.service: revert recent hardening for now It broke Debian Stretch. We'll try again later. Updates #1245 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/tailscaled.service | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cmd/tailscaled/tailscaled.service b/cmd/tailscaled/tailscaled.service index 7b847c54e..71dc89f1b 100644 --- a/cmd/tailscaled/tailscaled.service +++ b/cmd/tailscaled/tailscaled.service @@ -20,24 +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/ -ReadWritePaths=/run/ -ReadWritePaths=/var/run/ -RestrictSUIDSGID=true -SystemCallArchitectures=native - [Install] WantedBy=multi-user.target From c7d4bf2333b195b1553e588040f008ca307089ea Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Feb 2021 13:52:01 -0800 Subject: [PATCH 16/23] cmd/tailscale/cli: recommend sudo for 'tailscale up' on failure Fixes #1220 --- cmd/tailscale/cli/up.go | 11 ++++++++++- ipn/message.go | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) 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/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 } From dd10babaed7811885a9fe946b88620be9a789909 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Fri, 29 Jan 2021 13:11:11 -0800 Subject: [PATCH 17/23] wgenginer/magicsock: remove Addrs methods They are now unused. Signed-off-by: Josh Bleecher Snyder --- go.mod | 2 +- go.sum | 2 ++ wgengine/magicsock/legacy.go | 26 +++++++++++--------------- wgengine/magicsock/magicsock.go | 7 ------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 9fca67cc8..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-20210129202040-ddaf8316eff8 + 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 4ffdb0116..d5becc8b9 100644 --- a/go.sum +++ b/go.sum @@ -298,6 +298,8 @@ github.com/tailscale/wireguard-go v0.0.0-20210120212909-7ad8a0443bd3 h1:wpgSErXu 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/wgengine/magicsock/legacy.go b/wgengine/magicsock/legacy.go index 856700925..7620cc1ce 100644 --- a/wgengine/magicsock/legacy.go +++ b/wgengine/magicsock/legacy.go @@ -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 From 516e8a483807d49b77c775d88fc148db0da8a2cd Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Mon, 1 Feb 2021 10:50:36 -0800 Subject: [PATCH 18/23] tsweb: add num_goroutines expvar Signed-off-by: Josh Bleecher Snyder --- tsweb/tsweb.go | 1 + 1 file changed, 1 insertion(+) 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))) From 717c715c964c70a63d67804086a6bdde4c2dc0c7 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Mon, 1 Feb 2021 14:37:58 -0800 Subject: [PATCH 19/23] wgengine/wglog: don't log failure to send data packets Fixes #1239 --- wgengine/wglog/wglog.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wgengine/wglog/wglog.go b/wgengine/wglog/wglog.go index 6f3e490f2..7786edd82 100644 --- a/wgengine/wglog/wglog.go +++ b/wgengine/wglog/wglog.go @@ -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. From 267531e4f8649c3c14915299eecf1f29b94a8b9e Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 1 Feb 2021 14:32:50 -0800 Subject: [PATCH 20/23] wgengine/router: probe better for v6 policy routing support. Previously we disabled v6 support if the disable_policy knob was missing in /proc, but some kernels support policy routing without exposing the toggle. So instead, treat disable_policy absence as a "maybe", and make the direct `ip -6 rule` probing a bit more elaborate to compensate. Fixes #1241. Signed-off-by: David Anderson --- wgengine/router/router_linux.go | 49 +++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index 2f92d5d69..50898e071 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -1045,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. @@ -1064,10 +1068,6 @@ func checkIPv6() error { return err } - if err := checkIPRuleSupportsV6(); err != nil { - return err - } - return nil } @@ -1088,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 @@ -1103,5 +1107,8 @@ func checkIPRuleSupportsV6() error { } return fmt.Errorf("ip -6 rule failed: %s", detail) } + + // Delete again. + exec.Command("ip", del...).Run() return nil } From d139fa9c92d9e86667628bf9ae9e8f671abe4068 Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Tue, 2 Feb 2021 10:03:26 -0800 Subject: [PATCH 21/23] net/interfaces: use a uint32_t for ipv4 address The code was using a C "int", which is a signed 32-bit integer. That means some valid IP addresses were negative numbers. (In particular, the default router address handed out by AT&T fiber: 192.168.1.254. No I don't know why they do that.) A negative number is < 255, and so was treated by the Go code as an error. This fixes the unit test failure: $ go test -v -run=TestLikelyHomeRouterIPSyscallExec ./net/interfaces === RUN TestLikelyHomeRouterIPSyscallExec interfaces_darwin_cgo_test.go:15: syscall() = invalid IP, false, netstat = 192.168.1.254, true --- FAIL: TestLikelyHomeRouterIPSyscallExec (0.00s) Signed-off-by: David Crawshaw --- net/interfaces/interfaces_darwin_cgo.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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); From a2aa6cd2edede39fb1d931fa6b50ada8fc3aa11f Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 1 Feb 2021 14:08:46 -0800 Subject: [PATCH 22/23] wgengine/router: clarify disabled IPv6 message on Linux --- wgengine/router/router_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index 50898e071..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() From 9a7078985338c4699228373b16d93d3227b3723e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Feb 2021 14:48:39 -0800 Subject: [PATCH 23/23] cmd/tailscale: fix IPN message reading stall in tailscale status -web Fixes #1234 Updates #1254 --- cmd/tailscale/cli/status.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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)