mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-23 01:11:40 +00:00
tailcfg, control/controlclient: support delta-encoded netmaps
Should greatly reduce bandwidth for large networks (including our hello.ipn.dev node). Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
b23f2263c1
commit
696020227c
@ -22,6 +22,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -489,6 +490,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
|||||||
request := tailcfg.MapRequest{
|
request := tailcfg.MapRequest{
|
||||||
Version: 4,
|
Version: 4,
|
||||||
IncludeIPv6: true,
|
IncludeIPv6: true,
|
||||||
|
DeltaPeers: true,
|
||||||
KeepAlive: c.keepAlive,
|
KeepAlive: c.keepAlive,
|
||||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||||
DiscoKey: c.discoPubKey,
|
DiscoKey: c.discoPubKey,
|
||||||
@ -573,6 +575,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
|||||||
// the same format before just closing the connection.
|
// the same format before just closing the connection.
|
||||||
// We can use this same read loop either way.
|
// We can use this same read loop either way.
|
||||||
var msg []byte
|
var msg []byte
|
||||||
|
var previousPeers []*tailcfg.Node // for delta-purposes
|
||||||
for i := 0; i < maxPolls || maxPolls < 0; i++ {
|
for i := 0; i < maxPolls || maxPolls < 0; i++ {
|
||||||
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
|
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
|
||||||
var siz [4]byte
|
var siz [4]byte
|
||||||
@ -594,6 +597,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
|||||||
vlogf("netmap: decode error: %v")
|
vlogf("netmap: decode error: %v")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.KeepAlive {
|
if resp.KeepAlive {
|
||||||
vlogf("netmap: got keep-alive")
|
vlogf("netmap: got keep-alive")
|
||||||
} else {
|
} else {
|
||||||
@ -609,6 +613,10 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
|||||||
if resp.KeepAlive {
|
if resp.KeepAlive {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
undeltaPeers(&resp, previousPeers)
|
||||||
|
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||||
|
|
||||||
if resp.DERPMap != nil {
|
if resp.DERPMap != nil {
|
||||||
vlogf("netmap: new map contains DERP map")
|
vlogf("netmap: new map contains DERP map")
|
||||||
lastDERPMap = resp.DERPMap
|
lastDERPMap = resp.DERPMap
|
||||||
@ -851,3 +859,97 @@ func envBool(k string) bool {
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
|
||||||
|
// and the PeersRemoved and PeersChanged fields in mapRes.
|
||||||
|
// 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 len(removed) == 0 && len(changed) == 0 {
|
||||||
|
// No changes fast path.
|
||||||
|
mapRes.Peers = prev
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := 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)
|
||||||
|
mapRes.Peers = newFull
|
||||||
|
mapRes.PeersChanged = nil
|
||||||
|
mapRes.PeersRemoved = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
93
control/controlclient/direct_test.go
Normal file
93
control/controlclient/direct_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package controlclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUndeltaPeers(t *testing.T) {
|
||||||
|
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
|
||||||
|
return &tailcfg.Node{ID: id, Name: name}
|
||||||
|
}
|
||||||
|
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mapRes *tailcfg.MapResponse
|
||||||
|
prev []*tailcfg.Node
|
||||||
|
want []*tailcfg.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full_peers",
|
||||||
|
mapRes: &tailcfg.MapResponse{
|
||||||
|
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
},
|
||||||
|
want: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full_peers_ignores_deltas",
|
||||||
|
mapRes: &tailcfg.MapResponse{
|
||||||
|
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
PeersRemoved: []tailcfg.NodeID{2},
|
||||||
|
},
|
||||||
|
want: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add_and_update",
|
||||||
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
mapRes: &tailcfg.MapResponse{
|
||||||
|
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||||
|
},
|
||||||
|
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove",
|
||||||
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
mapRes: &tailcfg.MapResponse{
|
||||||
|
PeersRemoved: []tailcfg.NodeID{1},
|
||||||
|
},
|
||||||
|
want: peers(n(2, "bar")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add_and_remove",
|
||||||
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
mapRes: &tailcfg.MapResponse{
|
||||||
|
PeersChanged: peers(n(1, "foo2")),
|
||||||
|
PeersRemoved: []tailcfg.NodeID{2},
|
||||||
|
},
|
||||||
|
want: peers(n(1, "foo2")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unchanged",
|
||||||
|
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
mapRes: &tailcfg.MapResponse{},
|
||||||
|
want: peers(n(1, "foo"), n(2, "bar")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
undeltaPeers(tt.mapRes, tt.prev)
|
||||||
|
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||||
|
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatNodes(nodes []*tailcfg.Node) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, n := range nodes {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
@ -443,11 +443,12 @@ type RegisterResponse struct {
|
|||||||
type MapRequest struct {
|
type MapRequest struct {
|
||||||
Version int // current version is 4
|
Version int // current version is 4
|
||||||
Compress string // "zstd" or "" (no compression)
|
Compress string // "zstd" or "" (no compression)
|
||||||
KeepAlive bool // server sends keep-alives
|
KeepAlive bool // whether server should send keep-alives back to us
|
||||||
NodeKey NodeKey
|
NodeKey NodeKey
|
||||||
DiscoKey DiscoKey
|
DiscoKey DiscoKey
|
||||||
Endpoints []string // caller's endpoints (IPv4 or IPv6)
|
Endpoints []string // caller's endpoints (IPv4 or IPv6)
|
||||||
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
|
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
|
||||||
|
DeltaPeers bool // whether the 2nd+ network map in response should be deltas, using PeersChanged, PeersRemoved
|
||||||
Stream bool // if true, multiple MapResponse objects are returned
|
Stream bool // if true, multiple MapResponse objects are returned
|
||||||
Hostinfo *Hostinfo
|
Hostinfo *Hostinfo
|
||||||
|
|
||||||
@ -502,22 +503,36 @@ type DNSConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MapResponse struct {
|
type MapResponse struct {
|
||||||
KeepAlive bool // if set, all other fields are ignored
|
KeepAlive bool `json:",omitempty"` // if set, all other fields are ignored
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
Node *Node
|
Node *Node
|
||||||
Peers []*Node
|
DERPMap *DERPMap `json:",omitempty"` // if non-empty, a change in the DERP map.
|
||||||
DERPMap *DERPMap
|
|
||||||
|
// Peers, if non-empty, is the complete list of peers.
|
||||||
|
// It will be set in the first MapResponse for a long-polled request/response.
|
||||||
|
// Subsequent responses will be delta-encoded if DeltaPeers was set in the request.
|
||||||
|
// If Peers is non-empty, PeersChanged and PeersRemoved should
|
||||||
|
// be ignored (and should be empty).
|
||||||
|
// Peers is always returned sorted by Node.ID.
|
||||||
|
Peers []*Node `json:",omitempty"`
|
||||||
|
// PeersChanged are the Nodes (identified by their ID) that
|
||||||
|
// have changed or been added since the past update on the
|
||||||
|
// HTTP response. It's only set if MapRequest.DeltaPeers was true.
|
||||||
|
// PeersChanged is always returned sorted by Node.ID.
|
||||||
|
PeersChanged []*Node `json:",omitempty"`
|
||||||
|
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||||
|
PeersRemoved []NodeID `json:",omitempty"`
|
||||||
|
|
||||||
// DNS is the same as DNSConfig.Nameservers.
|
// DNS is the same as DNSConfig.Nameservers.
|
||||||
//
|
//
|
||||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||||
DNS []wgcfg.IP
|
DNS []wgcfg.IP `json:",omitempty"`
|
||||||
// SearchPaths are the same as DNSConfig.Domains.
|
// SearchPaths are the same as DNSConfig.Domains.
|
||||||
//
|
//
|
||||||
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
|
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
|
||||||
SearchPaths []string
|
SearchPaths []string `json:",omitempty"`
|
||||||
DNSConfig DNSConfig
|
DNSConfig DNSConfig `json:",omitempty"`
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
Domain string
|
Domain string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user