mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
4511e7d64e
Inb987b2ab18
(2021-01-12) when we introduced sharing we mapped the sharer to the userid at a low layer, mostly to fix the display of "tailscale status" and the client UIs, but also some tests. The commit earlier today,7dec09d169
, removed the 2.5yo option to let clients disable that automatic mapping, as clearly we were never getting around to it. This plumbs the Sharer UserID all the way to ipnstatus so the CLI itself can choose to print out the Sharer's identity over the node's original owner. Then we stop mangling Node.User and let clients decide how they want to render things. To ease the migration for the Windows GUI (which currently operates on tailcfg.Node via the NetMap from WatchIPNBus, instead of PeerStatus), a new method Node.SharerOrUser is added to do the mapping of Sharer-else-User. Updates #1909 Updates tailscale/corp#1183 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
394 lines
10 KiB
Go
394 lines
10 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package controlclient
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"sort"
|
|
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/netmap"
|
|
"tailscale.com/types/views"
|
|
"tailscale.com/wgengine/filter"
|
|
)
|
|
|
|
// mapSession holds the state over a long-polled "map" request to the
|
|
// control plane.
|
|
//
|
|
// It accepts incremental tailcfg.MapResponse values to
|
|
// netMapForResponse and returns fully inflated NetworkMaps, filling
|
|
// in the omitted data implicit from prior MapResponse values from
|
|
// within the same session (the same long-poll HTTP response to the
|
|
// one MapRequest).
|
|
type mapSession struct {
|
|
// Immutable fields.
|
|
privateNodeKey key.NodePrivate
|
|
logf logger.Logf
|
|
vlogf logger.Logf
|
|
machinePubKey key.MachinePublic
|
|
|
|
// Fields storing state over the course of multiple MapResponses.
|
|
lastNode *tailcfg.Node
|
|
lastDNSConfig *tailcfg.DNSConfig
|
|
lastDERPMap *tailcfg.DERPMap
|
|
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
|
lastPacketFilterRules views.Slice[tailcfg.FilterRule]
|
|
lastParsedPacketFilter []filter.Match
|
|
lastSSHPolicy *tailcfg.SSHPolicy
|
|
collectServices bool
|
|
previousPeers []*tailcfg.Node // for delta-purposes
|
|
lastDomain string
|
|
lastDomainAuditLogID string
|
|
lastHealth []string
|
|
lastPopBrowserURL string
|
|
stickyDebug tailcfg.Debug // accumulated opt.Bool values
|
|
lastTKAInfo *tailcfg.TKAInfo
|
|
|
|
// netMapBuilding is non-nil during a netmapForResponse call,
|
|
// containing the value to be returned, once fully populated.
|
|
netMapBuilding *netmap.NetworkMap
|
|
}
|
|
|
|
func newMapSession(privateNodeKey key.NodePrivate) *mapSession {
|
|
ms := &mapSession{
|
|
privateNodeKey: privateNodeKey,
|
|
logf: logger.Discard,
|
|
vlogf: logger.Discard,
|
|
lastDNSConfig: new(tailcfg.DNSConfig),
|
|
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
|
|
}
|
|
return ms
|
|
}
|
|
|
|
func (ms *mapSession) addUserProfile(userID tailcfg.UserID) {
|
|
nm := ms.netMapBuilding
|
|
if _, dup := nm.UserProfiles[userID]; dup {
|
|
// Already populated it from a previous peer.
|
|
return
|
|
}
|
|
if up, ok := ms.lastUserProfile[userID]; ok {
|
|
nm.UserProfiles[userID] = up
|
|
}
|
|
}
|
|
|
|
// netmapForResponse returns a fully populated NetworkMap from a full
|
|
// or incremental MapResponse within the session, filling in omitted
|
|
// information from prior MapResponse values.
|
|
func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.NetworkMap {
|
|
undeltaPeers(resp, ms.previousPeers)
|
|
|
|
ms.previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
|
for _, up := range resp.UserProfiles {
|
|
ms.lastUserProfile[up.ID] = up
|
|
}
|
|
|
|
if dm := resp.DERPMap; dm != nil {
|
|
ms.vlogf("netmap: new map contains DERP map")
|
|
|
|
// Zero-valued fields in a DERPMap mean that we're not changing
|
|
// anything and are using the previous value(s).
|
|
if ldm := ms.lastDERPMap; ldm != nil {
|
|
if dm.Regions == nil {
|
|
dm.Regions = ldm.Regions
|
|
dm.OmitDefaultRegions = ldm.OmitDefaultRegions
|
|
}
|
|
if dm.HomeParams == nil {
|
|
dm.HomeParams = ldm.HomeParams
|
|
} else if oldhh := ldm.HomeParams; oldhh != nil {
|
|
// Propagate sub-fields of HomeParams
|
|
hh := dm.HomeParams
|
|
if hh.RegionScore == nil {
|
|
hh.RegionScore = oldhh.RegionScore
|
|
}
|
|
}
|
|
}
|
|
|
|
ms.lastDERPMap = dm
|
|
}
|
|
|
|
if pf := resp.PacketFilter; pf != nil {
|
|
var err error
|
|
ms.lastPacketFilterRules = views.SliceOf(pf)
|
|
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
|
|
if err != nil {
|
|
ms.logf("parsePacketFilter: %v", err)
|
|
}
|
|
}
|
|
if c := resp.DNSConfig; c != nil {
|
|
ms.lastDNSConfig = c
|
|
}
|
|
if p := resp.SSHPolicy; p != nil {
|
|
ms.lastSSHPolicy = p
|
|
}
|
|
|
|
if v, ok := resp.CollectServices.Get(); ok {
|
|
ms.collectServices = v
|
|
}
|
|
if resp.Domain != "" {
|
|
ms.lastDomain = resp.Domain
|
|
}
|
|
if resp.DomainDataPlaneAuditLogID != "" {
|
|
ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID
|
|
}
|
|
if resp.Health != nil {
|
|
ms.lastHealth = resp.Health
|
|
}
|
|
if resp.TKAInfo != nil {
|
|
ms.lastTKAInfo = resp.TKAInfo
|
|
}
|
|
|
|
// TODO(bradfitz): now that this is a view, remove some of the defensive
|
|
// cloning elsewhere in mapSession.
|
|
peerViews := make([]tailcfg.NodeView, len(resp.Peers))
|
|
for i, n := range resp.Peers {
|
|
peerViews[i] = n.View()
|
|
}
|
|
|
|
nm := &netmap.NetworkMap{
|
|
NodeKey: ms.privateNodeKey.Public(),
|
|
PrivateKey: ms.privateNodeKey,
|
|
MachineKey: ms.machinePubKey,
|
|
Peers: peerViews,
|
|
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
|
Domain: ms.lastDomain,
|
|
DomainAuditLogID: ms.lastDomainAuditLogID,
|
|
DNS: *ms.lastDNSConfig,
|
|
PacketFilter: ms.lastParsedPacketFilter,
|
|
PacketFilterRules: ms.lastPacketFilterRules,
|
|
SSHPolicy: ms.lastSSHPolicy,
|
|
CollectServices: ms.collectServices,
|
|
DERPMap: ms.lastDERPMap,
|
|
ControlHealth: ms.lastHealth,
|
|
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
|
}
|
|
ms.netMapBuilding = nm
|
|
|
|
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
|
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
|
|
ms.logf("error unmarshalling TKAHead: %v", err)
|
|
nm.TKAEnabled = false
|
|
}
|
|
}
|
|
|
|
if resp.Node != nil {
|
|
ms.lastNode = resp.Node
|
|
}
|
|
if node := ms.lastNode.Clone(); node != nil {
|
|
nm.SelfNode = node
|
|
nm.Expiry = node.KeyExpiry
|
|
nm.Name = node.Name
|
|
nm.Addresses = filterSelfAddresses(node.Addresses)
|
|
nm.User = node.User
|
|
if node.Hostinfo.Valid() {
|
|
nm.Hostinfo = *node.Hostinfo.AsStruct()
|
|
}
|
|
if node.MachineAuthorized {
|
|
nm.MachineStatus = tailcfg.MachineAuthorized
|
|
} else {
|
|
nm.MachineStatus = tailcfg.MachineUnauthorized
|
|
}
|
|
}
|
|
|
|
ms.addUserProfile(nm.User)
|
|
magicDNSSuffix := nm.MagicDNSSuffix()
|
|
if nm.SelfNode != nil {
|
|
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
|
}
|
|
for _, peer := range resp.Peers {
|
|
peer.InitDisplayNames(magicDNSSuffix)
|
|
if !peer.Sharer.IsZero() {
|
|
ms.addUserProfile(peer.Sharer)
|
|
}
|
|
ms.addUserProfile(peer.User)
|
|
}
|
|
if DevKnob.ForceProxyDNS() {
|
|
nm.DNS.Proxied = true
|
|
}
|
|
ms.netMapBuilding = nil
|
|
return nm
|
|
}
|
|
|
|
// undeltaPeers updates mapRes.Peers to be complete based on the
|
|
// provided previous peer list and the PeersRemoved and PeersChanged
|
|
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
|
|
// maps.
|
|
//
|
|
// It then also nils out the delta fields.
|
|
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
|
if len(mapRes.Peers) > 0 {
|
|
// Not delta encoded.
|
|
if !nodesSorted(mapRes.Peers) {
|
|
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
|
sortNodes(mapRes.Peers)
|
|
}
|
|
return
|
|
}
|
|
|
|
var removed map[tailcfg.NodeID]bool
|
|
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
|
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
|
for _, id := range pr {
|
|
removed[id] = true
|
|
}
|
|
}
|
|
changed := mapRes.PeersChanged
|
|
|
|
if !nodesSorted(changed) {
|
|
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
|
sortNodes(changed)
|
|
}
|
|
if !nodesSorted(prev) {
|
|
// Internal error (unrelated to the network) if we get here.
|
|
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
|
sortNodes(prev)
|
|
}
|
|
|
|
newFull := prev
|
|
if len(removed) > 0 || len(changed) > 0 {
|
|
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
|
for len(prev) > 0 && len(changed) > 0 {
|
|
pID := prev[0].ID
|
|
cID := changed[0].ID
|
|
if removed[pID] {
|
|
prev = prev[1:]
|
|
continue
|
|
}
|
|
switch {
|
|
case pID < cID:
|
|
newFull = append(newFull, prev[0])
|
|
prev = prev[1:]
|
|
case pID == cID:
|
|
newFull = append(newFull, changed[0])
|
|
prev, changed = prev[1:], changed[1:]
|
|
case cID < pID:
|
|
newFull = append(newFull, changed[0])
|
|
changed = changed[1:]
|
|
}
|
|
}
|
|
newFull = append(newFull, changed...)
|
|
for _, n := range prev {
|
|
if !removed[n.ID] {
|
|
newFull = append(newFull, n)
|
|
}
|
|
}
|
|
sortNodes(newFull)
|
|
}
|
|
|
|
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 || len(mapRes.PeersChangedPatch) != 0 {
|
|
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
|
for _, n := range newFull {
|
|
peerByID[n.ID] = n
|
|
}
|
|
now := clock.Now()
|
|
for nodeID, seen := range mapRes.PeerSeenChange {
|
|
if n, ok := peerByID[nodeID]; ok {
|
|
if seen {
|
|
n.LastSeen = &now
|
|
} else {
|
|
n.LastSeen = nil
|
|
}
|
|
}
|
|
}
|
|
for nodeID, online := range mapRes.OnlineChange {
|
|
if n, ok := peerByID[nodeID]; ok {
|
|
online := online
|
|
n.Online = &online
|
|
}
|
|
}
|
|
for _, ec := range mapRes.PeersChangedPatch {
|
|
if n, ok := peerByID[ec.NodeID]; ok {
|
|
if ec.DERPRegion != 0 {
|
|
n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion)
|
|
}
|
|
if ec.Cap != 0 {
|
|
n.Cap = ec.Cap
|
|
}
|
|
if ec.Endpoints != nil {
|
|
n.Endpoints = ec.Endpoints
|
|
}
|
|
if ec.Key != nil {
|
|
n.Key = *ec.Key
|
|
}
|
|
if ec.DiscoKey != nil {
|
|
n.DiscoKey = *ec.DiscoKey
|
|
}
|
|
if v := ec.Online; v != nil {
|
|
n.Online = ptrCopy(v)
|
|
}
|
|
if v := ec.LastSeen; v != nil {
|
|
n.LastSeen = ptrCopy(v)
|
|
}
|
|
if v := ec.KeyExpiry; v != nil {
|
|
n.KeyExpiry = *v
|
|
}
|
|
if v := ec.Capabilities; v != nil {
|
|
n.Capabilities = *v
|
|
}
|
|
if v := ec.KeySignature; v != nil {
|
|
n.KeySignature = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mapRes.Peers = newFull
|
|
mapRes.PeersChanged = nil
|
|
mapRes.PeersRemoved = nil
|
|
}
|
|
|
|
// ptrCopy returns a pointer to a newly allocated shallow copy of *v.
|
|
func ptrCopy[T any](v *T) *T {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
ret := new(T)
|
|
*ret = *v
|
|
return ret
|
|
}
|
|
|
|
func nodesSorted(v []*tailcfg.Node) bool {
|
|
for i, n := range v {
|
|
if i > 0 && n.ID <= v[i-1].ID {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sortNodes(v []*tailcfg.Node) {
|
|
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
|
}
|
|
|
|
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
|
if v1 == nil {
|
|
return nil
|
|
}
|
|
v2 := make([]*tailcfg.Node, len(v1))
|
|
for i, n := range v1 {
|
|
v2[i] = n.Clone()
|
|
}
|
|
return v2
|
|
}
|
|
|
|
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
|
|
|
|
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
|
|
switch {
|
|
default:
|
|
return in
|
|
case debugSelfIPv6Only():
|
|
for _, a := range in {
|
|
if a.Addr().Is6() {
|
|
ret = append(ret, a)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
}
|