mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 03:25:35 +00:00
6d84f3409b
We now handle the case where the NetworkMap.SelfNode has already expired and do not return an expiry time in the past (which causes an ~infinite loop of timers to fire). Additionally, we now add an explicit check to ensure that the next expiry time is never before the current local-to-the-system time, to ensure that we don't end up in a similar situation due to clock skew. Finally, we add more tests for this logic to ensure that we don't regress on these edge cases. Fixes #7193 Change-Id: Iaf8e3d83be1d133a7aab7f8d62939e508cc53f9c Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
302 lines
7.1 KiB
Go
302 lines
7.1 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/netmap"
|
|
)
|
|
|
|
func TestFlagExpiredPeers(t *testing.T) {
|
|
n := func(id tailcfg.NodeID, name string, expiry time.Time, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
|
n := &tailcfg.Node{ID: id, Name: name, KeyExpiry: expiry}
|
|
for _, f := range mod {
|
|
f(n)
|
|
}
|
|
return n
|
|
}
|
|
|
|
now := time.Unix(1673373129, 0)
|
|
|
|
timeInPast := now.Add(-1 * time.Hour)
|
|
timeInFuture := now.Add(1 * time.Hour)
|
|
|
|
timeBeforeEpoch := flagExpiredPeersEpoch.Add(-1 * time.Second)
|
|
if now.Before(timeBeforeEpoch) {
|
|
panic("current time in test cannot be before epoch")
|
|
}
|
|
|
|
var expiredKey key.NodePublic
|
|
if err := expiredKey.UnmarshalText([]byte("nodekey:6da774d5d7740000000000000000000000000000000000000000000000000000")); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
controlTime *time.Time
|
|
netmap *netmap.NetworkMap
|
|
want []*tailcfg.Node
|
|
}{
|
|
{
|
|
name: "no_expiry",
|
|
controlTime: &now,
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeInFuture),
|
|
},
|
|
},
|
|
want: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeInFuture),
|
|
},
|
|
},
|
|
{
|
|
name: "expiry",
|
|
controlTime: &now,
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeInPast),
|
|
},
|
|
},
|
|
want: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeInPast, func(n *tailcfg.Node) {
|
|
n.Expired = true
|
|
n.Key = expiredKey
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
name: "bad_ControlTime",
|
|
// controlTime here is intentionally before our hardcoded epoch
|
|
controlTime: &timeBeforeEpoch,
|
|
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
|
|
},
|
|
},
|
|
want: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch
|
|
},
|
|
},
|
|
{
|
|
name: "tagged_node",
|
|
controlTime: &now,
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", time.Time{}), // tagged node; zero expiry
|
|
},
|
|
},
|
|
want: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", time.Time{}), // not expired
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
em := newExpiryManager(t.Logf)
|
|
em.timeNow = func() time.Time { return now }
|
|
|
|
if tt.controlTime != nil {
|
|
em.onControlTime(*tt.controlTime)
|
|
}
|
|
em.flagExpiredPeers(tt.netmap, now)
|
|
if !reflect.DeepEqual(tt.netmap.Peers, tt.want) {
|
|
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.netmap.Peers), formatNodes(tt.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNextPeerExpiry(t *testing.T) {
|
|
n := func(id tailcfg.NodeID, name string, expiry time.Time, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
|
n := &tailcfg.Node{ID: id, Name: name, KeyExpiry: expiry}
|
|
for _, f := range mod {
|
|
f(n)
|
|
}
|
|
return n
|
|
}
|
|
|
|
now := time.Unix(1675725516, 0)
|
|
|
|
noExpiry := time.Time{}
|
|
timeInPast := now.Add(-1 * time.Hour)
|
|
timeInFuture := now.Add(1 * time.Hour)
|
|
timeInMoreFuture := now.Add(2 * time.Hour)
|
|
|
|
tests := []struct {
|
|
name string
|
|
netmap *netmap.NetworkMap
|
|
want time.Time
|
|
}{
|
|
{
|
|
name: "no_expiry",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", noExpiry),
|
|
n(2, "bar", noExpiry),
|
|
},
|
|
SelfNode: n(3, "self", noExpiry),
|
|
},
|
|
want: noExpiry,
|
|
},
|
|
{
|
|
name: "future_expiry_from_peer",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", noExpiry),
|
|
n(2, "bar", timeInFuture),
|
|
},
|
|
SelfNode: n(3, "self", noExpiry),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "future_expiry_from_self",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", noExpiry),
|
|
n(2, "bar", noExpiry),
|
|
},
|
|
SelfNode: n(3, "self", timeInFuture),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "future_expiry_from_multiple_peers",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
n(2, "bar", timeInMoreFuture),
|
|
},
|
|
SelfNode: n(3, "self", noExpiry),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "future_expiry_from_peer_and_self",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInMoreFuture),
|
|
},
|
|
SelfNode: n(2, "self", timeInFuture),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "only_self",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{},
|
|
SelfNode: n(1, "self", timeInFuture),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "peer_already_expired",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInPast),
|
|
},
|
|
SelfNode: n(2, "self", timeInFuture),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "self_already_expired",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInFuture),
|
|
},
|
|
SelfNode: n(2, "self", timeInPast),
|
|
},
|
|
want: timeInFuture,
|
|
},
|
|
{
|
|
name: "all_nodes_already_expired",
|
|
netmap: &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInPast),
|
|
},
|
|
SelfNode: n(2, "self", timeInPast),
|
|
},
|
|
want: noExpiry,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
em := newExpiryManager(t.Logf)
|
|
em.timeNow = func() time.Time { return now }
|
|
got := em.nextPeerExpiry(tt.netmap, now)
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
|
|
} else if !got.IsZero() && got.Before(now) {
|
|
t.Errorf("unexpectedly got expiry %q before now %q", got.Format(time.RFC3339), now.Format(time.RFC3339))
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("ClockSkew", func(t *testing.T) {
|
|
t.Logf("local time: %q", now.Format(time.RFC3339))
|
|
em := newExpiryManager(t.Logf)
|
|
em.timeNow = func() time.Time { return now }
|
|
|
|
// The local clock is "running fast"; our clock skew is -2h
|
|
em.clockDelta.Store(-2 * time.Hour)
|
|
t.Logf("'real' time: %q", now.Add(-2*time.Hour).Format(time.RFC3339))
|
|
|
|
// If we don't adjust for the local time, this would return a
|
|
// time in the past.
|
|
nm := &netmap.NetworkMap{
|
|
Peers: []*tailcfg.Node{
|
|
n(1, "foo", timeInPast),
|
|
},
|
|
}
|
|
got := em.nextPeerExpiry(nm, now)
|
|
want := now.Add(30 * time.Second)
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
|
|
}
|
|
})
|
|
}
|
|
|
|
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)
|
|
|
|
if n.Online != nil {
|
|
fmt.Fprintf(&sb, ", online=%v", *n.Online)
|
|
}
|
|
if n.LastSeen != nil {
|
|
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix())
|
|
}
|
|
if n.Key != (key.NodePublic{}) {
|
|
fmt.Fprintf(&sb, ", key=%v", n.Key.String())
|
|
}
|
|
if n.Expired {
|
|
fmt.Fprintf(&sb, ", expired=true")
|
|
}
|
|
sb.WriteString(")")
|
|
}
|
|
return sb.String()
|
|
}
|