summaryrefslogtreecommitdiffhomepage
path: root/ipn
diff options
context:
space:
mode:
authorNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
committerNick Khyl <nickk@tailscale.com>2024-12-05 13:16:48 -0600
commit0267fe83b200f1702a2fa0a395442c02a053fadb (patch)
tree63654c55225eeb834de59a5a0bc8d19033c6145b /ipn
parent87546a5edf6b6503a87eeb2d666baba57398a066 (diff)
downloadtailscale-1.78.0.tar.xz
tailscale-1.78.0.zip
VERSION.txt: this is v1.78.0v1.78.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Diffstat (limited to 'ipn')
-rw-r--r--ipn/ipnlocal/breaktcp_darwin.go60
-rw-r--r--ipn/ipnlocal/breaktcp_linux.go60
-rw-r--r--ipn/ipnlocal/expiry_test.go602
-rw-r--r--ipn/ipnlocal/peerapi_h2c.go40
-rw-r--r--ipn/ipnlocal/testdata/example.com-key.pem54
-rw-r--r--ipn/ipnlocal/testdata/example.com.pem50
-rw-r--r--ipn/ipnlocal/testdata/rootCA.pem58
-rw-r--r--ipn/ipnserver/proxyconnect_js.go20
-rw-r--r--ipn/ipnserver/server_test.go92
-rw-r--r--ipn/localapi/disabled_stubs.go30
-rw-r--r--ipn/localapi/pprof.go56
-rw-r--r--ipn/policy/policy.go94
-rw-r--r--ipn/store/awsstore/store_aws.go372
-rw-r--r--ipn/store/awsstore/store_aws_stub.go36
-rw-r--r--ipn/store/awsstore/store_aws_test.go328
-rw-r--r--ipn/store/stores_test.go358
-rw-r--r--ipn/store_test.go96
17 files changed, 1203 insertions, 1203 deletions
diff --git a/ipn/ipnlocal/breaktcp_darwin.go b/ipn/ipnlocal/breaktcp_darwin.go
index 13566198c..289e760e1 100644
--- a/ipn/ipnlocal/breaktcp_darwin.go
+++ b/ipn/ipnlocal/breaktcp_darwin.go
@@ -1,30 +1,30 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipnlocal
-
-import (
- "log"
-
- "golang.org/x/sys/unix"
-)
-
-func init() {
- breakTCPConns = breakTCPConnsDarwin
-}
-
-func breakTCPConnsDarwin() error {
- var matched int
- for fd := 0; fd < 1000; fd++ {
- _, err := unix.GetsockoptTCPConnectionInfo(fd, unix.IPPROTO_TCP, unix.TCP_CONNECTION_INFO)
- if err == nil {
- matched++
- err = unix.Close(fd)
- log.Printf("debug: closed TCP fd %v: %v", fd, err)
- }
- }
- if matched == 0 {
- log.Printf("debug: no TCP connections found")
- }
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnlocal
+
+import (
+ "log"
+
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ breakTCPConns = breakTCPConnsDarwin
+}
+
+func breakTCPConnsDarwin() error {
+ var matched int
+ for fd := 0; fd < 1000; fd++ {
+ _, err := unix.GetsockoptTCPConnectionInfo(fd, unix.IPPROTO_TCP, unix.TCP_CONNECTION_INFO)
+ if err == nil {
+ matched++
+ err = unix.Close(fd)
+ log.Printf("debug: closed TCP fd %v: %v", fd, err)
+ }
+ }
+ if matched == 0 {
+ log.Printf("debug: no TCP connections found")
+ }
+ return nil
+}
diff --git a/ipn/ipnlocal/breaktcp_linux.go b/ipn/ipnlocal/breaktcp_linux.go
index b82f65212..d078103cf 100644
--- a/ipn/ipnlocal/breaktcp_linux.go
+++ b/ipn/ipnlocal/breaktcp_linux.go
@@ -1,30 +1,30 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipnlocal
-
-import (
- "log"
-
- "golang.org/x/sys/unix"
-)
-
-func init() {
- breakTCPConns = breakTCPConnsLinux
-}
-
-func breakTCPConnsLinux() error {
- var matched int
- for fd := 0; fd < 1000; fd++ {
- _, err := unix.GetsockoptTCPInfo(fd, unix.IPPROTO_TCP, unix.TCP_INFO)
- if err == nil {
- matched++
- err = unix.Close(fd)
- log.Printf("debug: closed TCP fd %v: %v", fd, err)
- }
- }
- if matched == 0 {
- log.Printf("debug: no TCP connections found")
- }
- return nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnlocal
+
+import (
+ "log"
+
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ breakTCPConns = breakTCPConnsLinux
+}
+
+func breakTCPConnsLinux() error {
+ var matched int
+ for fd := 0; fd < 1000; fd++ {
+ _, err := unix.GetsockoptTCPInfo(fd, unix.IPPROTO_TCP, unix.TCP_INFO)
+ if err == nil {
+ matched++
+ err = unix.Close(fd)
+ log.Printf("debug: closed TCP fd %v: %v", fd, err)
+ }
+ }
+ if matched == 0 {
+ log.Printf("debug: no TCP connections found")
+ }
+ return nil
+}
diff --git a/ipn/ipnlocal/expiry_test.go b/ipn/ipnlocal/expiry_test.go
index af1aa337b..efc18133f 100644
--- a/ipn/ipnlocal/expiry_test.go
+++ b/ipn/ipnlocal/expiry_test.go
@@ -1,301 +1,301 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipnlocal
-
-import (
- "fmt"
- "reflect"
- "strings"
- "testing"
- "time"
-
- "tailscale.com/tailcfg"
- "tailscale.com/tstest"
- "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.NodeView
- }{
- {
- name: "no_expiry",
- controlTime: &now,
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", timeInFuture),
- }),
- },
- want: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", timeInFuture),
- }),
- },
- {
- name: "expiry",
- controlTime: &now,
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", timeInPast),
- }),
- },
- want: nodeViews([]*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: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
- }),
- },
- want: nodeViews([]*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: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", time.Time{}), // tagged node; zero expiry
- }),
- },
- want: nodeViews([]*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.clock = tstest.NewClock(tstest.ClockOpts{Start: 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: nodeViews([]*tailcfg.Node{
- n(1, "foo", noExpiry),
- n(2, "bar", noExpiry),
- }),
- SelfNode: n(3, "self", noExpiry).View(),
- },
- want: noExpiry,
- },
- {
- name: "future_expiry_from_peer",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", noExpiry),
- n(2, "bar", timeInFuture),
- }),
- SelfNode: n(3, "self", noExpiry).View(),
- },
- want: timeInFuture,
- },
- {
- name: "future_expiry_from_self",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", noExpiry),
- n(2, "bar", noExpiry),
- }),
- SelfNode: n(3, "self", timeInFuture).View(),
- },
- want: timeInFuture,
- },
- {
- name: "future_expiry_from_multiple_peers",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- n(2, "bar", timeInMoreFuture),
- }),
- SelfNode: n(3, "self", noExpiry).View(),
- },
- want: timeInFuture,
- },
- {
- name: "future_expiry_from_peer_and_self",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInMoreFuture),
- }),
- SelfNode: n(2, "self", timeInFuture).View(),
- },
- want: timeInFuture,
- },
- {
- name: "only_self",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{}),
- SelfNode: n(1, "self", timeInFuture).View(),
- },
- want: timeInFuture,
- },
- {
- name: "peer_already_expired",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInPast),
- }),
- SelfNode: n(2, "self", timeInFuture).View(),
- },
- want: timeInFuture,
- },
- {
- name: "self_already_expired",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInFuture),
- }),
- SelfNode: n(2, "self", timeInPast).View(),
- },
- want: timeInFuture,
- },
- {
- name: "all_nodes_already_expired",
- netmap: &netmap.NetworkMap{
- Peers: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInPast),
- }),
- SelfNode: n(2, "self", timeInPast).View(),
- },
- want: noExpiry,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- em := newExpiryManager(t.Logf)
- em.clock = tstest.NewClock(tstest.ClockOpts{Start: now})
- got := em.nextPeerExpiry(tt.netmap, now)
- if !got.Equal(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.clock = tstest.NewClock(tstest.ClockOpts{Start: 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: nodeViews([]*tailcfg.Node{
- n(1, "foo", timeInPast),
- }),
- }
- got := em.nextPeerExpiry(nm, now)
- want := now.Add(30 * time.Second)
- if !got.Equal(want) {
- t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
- }
- })
-}
-
-func formatNodes(nodes []tailcfg.NodeView) 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()
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnlocal
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "tailscale.com/tailcfg"
+ "tailscale.com/tstest"
+ "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.NodeView
+ }{
+ {
+ name: "no_expiry",
+ controlTime: &now,
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", timeInFuture),
+ }),
+ },
+ want: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", timeInFuture),
+ }),
+ },
+ {
+ name: "expiry",
+ controlTime: &now,
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", timeInPast),
+ }),
+ },
+ want: nodeViews([]*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: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
+ }),
+ },
+ want: nodeViews([]*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: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", time.Time{}), // tagged node; zero expiry
+ }),
+ },
+ want: nodeViews([]*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.clock = tstest.NewClock(tstest.ClockOpts{Start: 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: nodeViews([]*tailcfg.Node{
+ n(1, "foo", noExpiry),
+ n(2, "bar", noExpiry),
+ }),
+ SelfNode: n(3, "self", noExpiry).View(),
+ },
+ want: noExpiry,
+ },
+ {
+ name: "future_expiry_from_peer",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", noExpiry),
+ n(2, "bar", timeInFuture),
+ }),
+ SelfNode: n(3, "self", noExpiry).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "future_expiry_from_self",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", noExpiry),
+ n(2, "bar", noExpiry),
+ }),
+ SelfNode: n(3, "self", timeInFuture).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "future_expiry_from_multiple_peers",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ n(2, "bar", timeInMoreFuture),
+ }),
+ SelfNode: n(3, "self", noExpiry).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "future_expiry_from_peer_and_self",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInMoreFuture),
+ }),
+ SelfNode: n(2, "self", timeInFuture).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "only_self",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{}),
+ SelfNode: n(1, "self", timeInFuture).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "peer_already_expired",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInPast),
+ }),
+ SelfNode: n(2, "self", timeInFuture).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "self_already_expired",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInFuture),
+ }),
+ SelfNode: n(2, "self", timeInPast).View(),
+ },
+ want: timeInFuture,
+ },
+ {
+ name: "all_nodes_already_expired",
+ netmap: &netmap.NetworkMap{
+ Peers: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInPast),
+ }),
+ SelfNode: n(2, "self", timeInPast).View(),
+ },
+ want: noExpiry,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ em := newExpiryManager(t.Logf)
+ em.clock = tstest.NewClock(tstest.ClockOpts{Start: now})
+ got := em.nextPeerExpiry(tt.netmap, now)
+ if !got.Equal(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.clock = tstest.NewClock(tstest.ClockOpts{Start: 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: nodeViews([]*tailcfg.Node{
+ n(1, "foo", timeInPast),
+ }),
+ }
+ got := em.nextPeerExpiry(nm, now)
+ want := now.Add(30 * time.Second)
+ if !got.Equal(want) {
+ t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
+ }
+ })
+}
+
+func formatNodes(nodes []tailcfg.NodeView) 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()
+}
diff --git a/ipn/ipnlocal/peerapi_h2c.go b/ipn/ipnlocal/peerapi_h2c.go
index fbfa86398..e6335fe2b 100644
--- a/ipn/ipnlocal/peerapi_h2c.go
+++ b/ipn/ipnlocal/peerapi_h2c.go
@@ -1,20 +1,20 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !ios && !android && !js
-
-package ipnlocal
-
-import (
- "net/http"
-
- "golang.org/x/net/http2"
- "golang.org/x/net/http2/h2c"
-)
-
-func init() {
- addH2C = func(s *http.Server) {
- h2s := &http2.Server{}
- s.Handler = h2c.NewHandler(s.Handler, h2s)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !ios && !android && !js
+
+package ipnlocal
+
+import (
+ "net/http"
+
+ "golang.org/x/net/http2"
+ "golang.org/x/net/http2/h2c"
+)
+
+func init() {
+ addH2C = func(s *http.Server) {
+ h2s := &http2.Server{}
+ s.Handler = h2c.NewHandler(s.Handler, h2s)
+ }
+}
diff --git a/ipn/ipnlocal/testdata/example.com-key.pem b/ipn/ipnlocal/testdata/example.com-key.pem
index 06902f4c9..9020553f1 100644
--- a/ipn/ipnlocal/testdata/example.com-key.pem
+++ b/ipn/ipnlocal/testdata/example.com-key.pem
@@ -1,28 +1,28 @@
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCejQaJrntrJSgE
-QtScyTU6TXOU+v1FdFjrsyHFK5mjV1C5pVQxnLn93GRshtIrGOLLrd3Wv2TVYZOX
-xH7f1ZLFbneDURCXbS+7nmsg+TLHRSRKfODbE3oYZj7NSJ163CCvwSJKTdmLpXbn
-ui9F04tyk0zxO4Wre4ukwf6xtse8G5zl2RJrueiVAiouTG/pJdIS08dGQa0GM1n9
-Aesa+TerlZcpRZR6X402yQqa8q/QqbIuzrlfDmgOb8sm6T8+JMtj3hEvnYdpMVOg
-w/XiTlX0v/YrB9sVQ9XnqGsqwTL0OMG0choMNKipwLi2n+XPSCIiRhi666zNNivE
-K1qaPS5RAgMBAAECggEAV9dAGQWPISR70CiKjLa5A60nbRHFQjackTE0c32daC6W
-7dOYGsh/DxOMm8fyJqhp9nhEYJa3MbUWxU27ER3NbA6wrhM6gvqeKG8zYRhPNrGq
-0o3vMdDPozb6cldZ0Fimz1jMO6h373NjtiyjxibWqkrLpRbaDtCq5EQKbMEcVa2D
-Xt5hxCOaCA3OZ/mAcGUNFmDNgNsGP/r6eXdI5pbqnUNMPkv/JsHl8h2HuyKUm4hf
-TRnXPAak6DkUod9QXYFKVBVPa5pjiO09e0aiMUvJ8vYd/6bNIsAKWLPa1PYuUE2l
-kg8Nik+P/XLzffKsLxiFKY0nCqrorM9K5q7baofGdQKBgQDPujjebFg6OKw6MS3S
-PESopvL//C/XgtgifcSSZCWzIZRVBVTbbJCGRtqFzF0XO4YRX3EOAyD/L7wYUPzO
-+W3AU2W3/DVJYdcm2CASABbHNy0kk52LI0HHAssbFDgyB9XuuWP+vVZk7B5OmCAD
-Bppuj6Mnu03i282nKNJzvRiVnwKBgQDDZUXv22K8y7GkKw/ZW/wQP2zBNtFc15he
-1EOyUGHlXuQixnDSaqonkwec6IOlo7Sx/vwO/7+v4Jzc24Wq3DFAmMu/EYJgvI+m
-m3kpB4H7Xus4JqnhxqN7GB7zOdguCWZF1HLemZNZlVrUjG5mQ9cizzvvYptnQDLq
-FEJ1hddWDwKBgB+vy276Xfb7oCH8UH4KXXrQhK7RvEaGmgug3bRq/Gk3zRWvC4Ox
-KtagxkK0qtqZZNkPkwJNLeJfWLTo3beAyuIUlqabHVHFT/mH7FRymQbofsVekyCf
-TzBZV7wYuH3BPjv9IajBHwWkEvdwMyni/vmwhXXRF49schF2o6uuA6sHAoGBAL1J
-Xnb+EKjUq0JedPwcIBOdXb3PXQKT2QgEmZAkTrHlOxx1INa2fh/YT4ext9a+wE2u
-tn/RQeEfttY90z+yEASEAN0YGTWddYvxEW6t1z2stjGvQuN1ium0dEcrwkDW2jzL
-knwSSqx+A3/kiw6GqeMO3wEIhYOArdIVzkwLXJABAoGAOXLGhz5u5FWjF3zAeYme
-uHTU/3Z3jeI80PvShGrgAakPOBt3cIFpUaiOEslcqqgDUSGE3EnmkRqaEch+UapF
-ty6Zz7cKjXhQSWOjew1uUW2ANNEpsnYbmZOOnfvosd7jfHSVbL6KIhWmIdC6h0NP
-c/bJnTXEEVsWjLZTwYaq0Us=
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCejQaJrntrJSgE
+QtScyTU6TXOU+v1FdFjrsyHFK5mjV1C5pVQxnLn93GRshtIrGOLLrd3Wv2TVYZOX
+xH7f1ZLFbneDURCXbS+7nmsg+TLHRSRKfODbE3oYZj7NSJ163CCvwSJKTdmLpXbn
+ui9F04tyk0zxO4Wre4ukwf6xtse8G5zl2RJrueiVAiouTG/pJdIS08dGQa0GM1n9
+Aesa+TerlZcpRZR6X402yQqa8q/QqbIuzrlfDmgOb8sm6T8+JMtj3hEvnYdpMVOg
+w/XiTlX0v/YrB9sVQ9XnqGsqwTL0OMG0choMNKipwLi2n+XPSCIiRhi666zNNivE
+K1qaPS5RAgMBAAECggEAV9dAGQWPISR70CiKjLa5A60nbRHFQjackTE0c32daC6W
+7dOYGsh/DxOMm8fyJqhp9nhEYJa3MbUWxU27ER3NbA6wrhM6gvqeKG8zYRhPNrGq
+0o3vMdDPozb6cldZ0Fimz1jMO6h373NjtiyjxibWqkrLpRbaDtCq5EQKbMEcVa2D
+Xt5hxCOaCA3OZ/mAcGUNFmDNgNsGP/r6eXdI5pbqnUNMPkv/JsHl8h2HuyKUm4hf
+TRnXPAak6DkUod9QXYFKVBVPa5pjiO09e0aiMUvJ8vYd/6bNIsAKWLPa1PYuUE2l
+kg8Nik+P/XLzffKsLxiFKY0nCqrorM9K5q7baofGdQKBgQDPujjebFg6OKw6MS3S
+PESopvL//C/XgtgifcSSZCWzIZRVBVTbbJCGRtqFzF0XO4YRX3EOAyD/L7wYUPzO
++W3AU2W3/DVJYdcm2CASABbHNy0kk52LI0HHAssbFDgyB9XuuWP+vVZk7B5OmCAD
+Bppuj6Mnu03i282nKNJzvRiVnwKBgQDDZUXv22K8y7GkKw/ZW/wQP2zBNtFc15he
+1EOyUGHlXuQixnDSaqonkwec6IOlo7Sx/vwO/7+v4Jzc24Wq3DFAmMu/EYJgvI+m
+m3kpB4H7Xus4JqnhxqN7GB7zOdguCWZF1HLemZNZlVrUjG5mQ9cizzvvYptnQDLq
+FEJ1hddWDwKBgB+vy276Xfb7oCH8UH4KXXrQhK7RvEaGmgug3bRq/Gk3zRWvC4Ox
+KtagxkK0qtqZZNkPkwJNLeJfWLTo3beAyuIUlqabHVHFT/mH7FRymQbofsVekyCf
+TzBZV7wYuH3BPjv9IajBHwWkEvdwMyni/vmwhXXRF49schF2o6uuA6sHAoGBAL1J
+Xnb+EKjUq0JedPwcIBOdXb3PXQKT2QgEmZAkTrHlOxx1INa2fh/YT4ext9a+wE2u
+tn/RQeEfttY90z+yEASEAN0YGTWddYvxEW6t1z2stjGvQuN1ium0dEcrwkDW2jzL
+knwSSqx+A3/kiw6GqeMO3wEIhYOArdIVzkwLXJABAoGAOXLGhz5u5FWjF3zAeYme
+uHTU/3Z3jeI80PvShGrgAakPOBt3cIFpUaiOEslcqqgDUSGE3EnmkRqaEch+UapF
+ty6Zz7cKjXhQSWOjew1uUW2ANNEpsnYbmZOOnfvosd7jfHSVbL6KIhWmIdC6h0NP
+c/bJnTXEEVsWjLZTwYaq0Us=
-----END PRIVATE KEY----- \ No newline at end of file
diff --git a/ipn/ipnlocal/testdata/example.com.pem b/ipn/ipnlocal/testdata/example.com.pem
index 588850813..65e7110a8 100644
--- a/ipn/ipnlocal/testdata/example.com.pem
+++ b/ipn/ipnlocal/testdata/example.com.pem
@@ -1,26 +1,26 @@
------BEGIN CERTIFICATE-----
-MIIEcDCCAtigAwIBAgIRAPmUKRkyFAkVVxFblB/233cwDQYJKoZIhvcNAQELBQAw
-gZ8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE6MDgGA1UECwwxZnJv
-bWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWljaGFlbCBKLiBGcm9tYmVyZ2VyKTFB
-MD8GA1UEAww4bWtjZXJ0IGZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hh
-ZWwgSi4gRnJvbWJlcmdlcikwHhcNMjMwMjA3MjAzNDE4WhcNMjUwNTA3MTkzNDE4
-WjBlMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxOjA4
-BgNVBAsMMWZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hhZWwgSi4gRnJv
-bWJlcmdlcikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCejQaJrntr
-JSgEQtScyTU6TXOU+v1FdFjrsyHFK5mjV1C5pVQxnLn93GRshtIrGOLLrd3Wv2TV
-YZOXxH7f1ZLFbneDURCXbS+7nmsg+TLHRSRKfODbE3oYZj7NSJ163CCvwSJKTdmL
-pXbnui9F04tyk0zxO4Wre4ukwf6xtse8G5zl2RJrueiVAiouTG/pJdIS08dGQa0G
-M1n9Aesa+TerlZcpRZR6X402yQqa8q/QqbIuzrlfDmgOb8sm6T8+JMtj3hEvnYdp
-MVOgw/XiTlX0v/YrB9sVQ9XnqGsqwTL0OMG0choMNKipwLi2n+XPSCIiRhi666zN
-NivEK1qaPS5RAgMBAAGjYDBeMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
-BgEFBQcDATAfBgNVHSMEGDAWgBTXyq2jQVrnqQKL8fB9C4L0QJftwDAWBgNVHREE
-DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAYEAQWzpOaBkRR4M+WqB
-CsT4ARyM6WpZ+jpeSblCzPdlDRW+50G1HV7K930zayq4DwncPY/SqSn0Q31WuzZv
-bTWHkWa+MLPGYANHsusOmMR8Eh16G4+5+GGf8psWa0npAYO35cuNkyyCCc1LEB4M
-NrzCB2+KZ+SyOdfCCA5VzEKN3I8wvVLaYovi24Zjwv+0uETG92TlZmLQRhj8uPxN
-deeLM45aBkQZSYCbGMDVDK/XYKBkNLn3kxD/eZeXxxr41v4pH44+46FkYcYJzdn8
-ccAg5LRGieqTozhLiXARNK1vTy6kR1l/Az8DIx6GN4sP2/LMFYFijiiOCDKS1wWA
-xQgZeHt4GIuBym+Kd+Z5KXcP0AT+47Cby3+B10Kq8vHwjTELiF0UFeEYYMdynPAW
-pbEwVLhsfMsBqFtj3dsxHr8Kz3rnarOYzkaw7EMZnLAthb2CN7y5uGV9imQC5RMI
-/qZdRSuCYZ3A1E/WJkGbPY/YdPql/IE+LIAgKGFHZZNftBCo
+-----BEGIN CERTIFICATE-----
+MIIEcDCCAtigAwIBAgIRAPmUKRkyFAkVVxFblB/233cwDQYJKoZIhvcNAQELBQAw
+gZ8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE6MDgGA1UECwwxZnJv
+bWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWljaGFlbCBKLiBGcm9tYmVyZ2VyKTFB
+MD8GA1UEAww4bWtjZXJ0IGZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hh
+ZWwgSi4gRnJvbWJlcmdlcikwHhcNMjMwMjA3MjAzNDE4WhcNMjUwNTA3MTkzNDE4
+WjBlMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxOjA4
+BgNVBAsMMWZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hhZWwgSi4gRnJv
+bWJlcmdlcikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCejQaJrntr
+JSgEQtScyTU6TXOU+v1FdFjrsyHFK5mjV1C5pVQxnLn93GRshtIrGOLLrd3Wv2TV
+YZOXxH7f1ZLFbneDURCXbS+7nmsg+TLHRSRKfODbE3oYZj7NSJ163CCvwSJKTdmL
+pXbnui9F04tyk0zxO4Wre4ukwf6xtse8G5zl2RJrueiVAiouTG/pJdIS08dGQa0G
+M1n9Aesa+TerlZcpRZR6X402yQqa8q/QqbIuzrlfDmgOb8sm6T8+JMtj3hEvnYdp
+MVOgw/XiTlX0v/YrB9sVQ9XnqGsqwTL0OMG0choMNKipwLi2n+XPSCIiRhi666zN
+NivEK1qaPS5RAgMBAAGjYDBeMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
+BgEFBQcDATAfBgNVHSMEGDAWgBTXyq2jQVrnqQKL8fB9C4L0QJftwDAWBgNVHREE
+DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAYEAQWzpOaBkRR4M+WqB
+CsT4ARyM6WpZ+jpeSblCzPdlDRW+50G1HV7K930zayq4DwncPY/SqSn0Q31WuzZv
+bTWHkWa+MLPGYANHsusOmMR8Eh16G4+5+GGf8psWa0npAYO35cuNkyyCCc1LEB4M
+NrzCB2+KZ+SyOdfCCA5VzEKN3I8wvVLaYovi24Zjwv+0uETG92TlZmLQRhj8uPxN
+deeLM45aBkQZSYCbGMDVDK/XYKBkNLn3kxD/eZeXxxr41v4pH44+46FkYcYJzdn8
+ccAg5LRGieqTozhLiXARNK1vTy6kR1l/Az8DIx6GN4sP2/LMFYFijiiOCDKS1wWA
+xQgZeHt4GIuBym+Kd+Z5KXcP0AT+47Cby3+B10Kq8vHwjTELiF0UFeEYYMdynPAW
+pbEwVLhsfMsBqFtj3dsxHr8Kz3rnarOYzkaw7EMZnLAthb2CN7y5uGV9imQC5RMI
+/qZdRSuCYZ3A1E/WJkGbPY/YdPql/IE+LIAgKGFHZZNftBCo
-----END CERTIFICATE----- \ No newline at end of file
diff --git a/ipn/ipnlocal/testdata/rootCA.pem b/ipn/ipnlocal/testdata/rootCA.pem
index 88a16f47a..28bd25467 100644
--- a/ipn/ipnlocal/testdata/rootCA.pem
+++ b/ipn/ipnlocal/testdata/rootCA.pem
@@ -1,30 +1,30 @@
------BEGIN CERTIFICATE-----
-MIIFEDCCA3igAwIBAgIRANf5NdPojIfj70wMfJVYUg8wDQYJKoZIhvcNAQELBQAw
-gZ8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE6MDgGA1UECwwxZnJv
-bWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWljaGFlbCBKLiBGcm9tYmVyZ2VyKTFB
-MD8GA1UEAww4bWtjZXJ0IGZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hh
-ZWwgSi4gRnJvbWJlcmdlcikwHhcNMjMwMjA3MjAzNDE4WhcNMzMwMjA3MjAzNDE4
-WjCBnzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTowOAYDVQQLDDFm
-cm9tYmVyZ2VyQHN0YXJkdXN0LmxvY2FsIChNaWNoYWVsIEouIEZyb21iZXJnZXIp
-MUEwPwYDVQQDDDhta2NlcnQgZnJvbWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWlj
-aGFlbCBKLiBGcm9tYmVyZ2VyKTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC
-ggGBAL5uXNnrZ6dgjcvK0Hc7ZNUIRYEWst9qbO0P9H7le08pJ6d9T2BUWruZtVjk
-Q12msv5/bVWHhVk8dZclI9FLXuMsIrocH8bsoP4wruPMyRyp6EedSKODN51fFSRv
-/jHbS5vzUVAWTYy9qYmd6qL0uhsHCZCCT6gfigamHPUFKM3sHDn5ZHWvySMwcyGl
-AicmPAIkBWqiCZAkB5+WM7+oyRLjmrIalfWIZYxW/rojGLwTfneHv6J5WjVQnpJB
-ayWCzCzaiXukK9MeBWeTOe8UfVN0Engd74/rjLWvjbfC+uZSr6RVkZvs2jANLwPF
-zgzBPHgRPfAhszU1NNAMjnNQ47+OMOTKRt7e6jYzhO5fyO1qVAAvGBqcfpj+JfDk
-cccaUMhUvdiGrhGf1V1tN/PislxvALirzcFipjD01isBKwn0fxRugzvJNrjEo8RA
-RvbcdeKcwex7M0o/Cd0+G2B13gZNOFvR33PmG7iTpp7IUrUKfQg28I83Sp8tMY3s
-ljJSawIDAQABo0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIB
-ADAdBgNVHQ4EFgQU18qto0Fa56kCi/HwfQuC9ECX7cAwDQYJKoZIhvcNAQELBQAD
-ggGBAAzs96LwZVOsRSlBdQqMo8oMAvs7HgnYbXt8SqaACLX3+kJ3cV/vrCE3iJrW
-ma4CiQbxS/HqsiZjota5m4lYeEevRnUDpXhp+7ugZTiz33Flm1RU99c9UYfQ+919
-ANPAKeqNpoPco/HF5Bz0ocepjcfKQrVZZNTj6noLs8o12FHBLO5976AcF9mqlNfh
-8/F0gDJXq6+x7VT5y8u0rY004XKPRe3CklRt8kpeMiP6mhRyyUehOaHeIbNx8ubi
-Pi44ByN/ueAnuRhF9zYtyZVZZOaSLysJge01tuPXF8rBXGruoJIv35xTTBa9BzaP
-YDOGbGn1ZnajdNagHqCba8vjTLDSpqMvgRj3TFrGHdETA2LDQat38uVxX8gxm68K
-va5Tyv7n+6BQ5YTpJjTPnmSJKaXZrrhdLPvG0OU2TxeEsvbcm5LFQofirOOw86Se
-vzF2cQ94mmHRZiEk0Av3NO0jF93ELDrBCuiccVyEKq6TknuvPQlutCXKDOYSEb8I
-MHctBg==
+-----BEGIN CERTIFICATE-----
+MIIFEDCCA3igAwIBAgIRANf5NdPojIfj70wMfJVYUg8wDQYJKoZIhvcNAQELBQAw
+gZ8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE6MDgGA1UECwwxZnJv
+bWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWljaGFlbCBKLiBGcm9tYmVyZ2VyKTFB
+MD8GA1UEAww4bWtjZXJ0IGZyb21iZXJnZXJAc3RhcmR1c3QubG9jYWwgKE1pY2hh
+ZWwgSi4gRnJvbWJlcmdlcikwHhcNMjMwMjA3MjAzNDE4WhcNMzMwMjA3MjAzNDE4
+WjCBnzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTowOAYDVQQLDDFm
+cm9tYmVyZ2VyQHN0YXJkdXN0LmxvY2FsIChNaWNoYWVsIEouIEZyb21iZXJnZXIp
+MUEwPwYDVQQDDDhta2NlcnQgZnJvbWJlcmdlckBzdGFyZHVzdC5sb2NhbCAoTWlj
+aGFlbCBKLiBGcm9tYmVyZ2VyKTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoC
+ggGBAL5uXNnrZ6dgjcvK0Hc7ZNUIRYEWst9qbO0P9H7le08pJ6d9T2BUWruZtVjk
+Q12msv5/bVWHhVk8dZclI9FLXuMsIrocH8bsoP4wruPMyRyp6EedSKODN51fFSRv
+/jHbS5vzUVAWTYy9qYmd6qL0uhsHCZCCT6gfigamHPUFKM3sHDn5ZHWvySMwcyGl
+AicmPAIkBWqiCZAkB5+WM7+oyRLjmrIalfWIZYxW/rojGLwTfneHv6J5WjVQnpJB
+ayWCzCzaiXukK9MeBWeTOe8UfVN0Engd74/rjLWvjbfC+uZSr6RVkZvs2jANLwPF
+zgzBPHgRPfAhszU1NNAMjnNQ47+OMOTKRt7e6jYzhO5fyO1qVAAvGBqcfpj+JfDk
+cccaUMhUvdiGrhGf1V1tN/PislxvALirzcFipjD01isBKwn0fxRugzvJNrjEo8RA
+RvbcdeKcwex7M0o/Cd0+G2B13gZNOFvR33PmG7iTpp7IUrUKfQg28I83Sp8tMY3s
+ljJSawIDAQABo0UwQzAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIB
+ADAdBgNVHQ4EFgQU18qto0Fa56kCi/HwfQuC9ECX7cAwDQYJKoZIhvcNAQELBQAD
+ggGBAAzs96LwZVOsRSlBdQqMo8oMAvs7HgnYbXt8SqaACLX3+kJ3cV/vrCE3iJrW
+ma4CiQbxS/HqsiZjota5m4lYeEevRnUDpXhp+7ugZTiz33Flm1RU99c9UYfQ+919
+ANPAKeqNpoPco/HF5Bz0ocepjcfKQrVZZNTj6noLs8o12FHBLO5976AcF9mqlNfh
+8/F0gDJXq6+x7VT5y8u0rY004XKPRe3CklRt8kpeMiP6mhRyyUehOaHeIbNx8ubi
+Pi44ByN/ueAnuRhF9zYtyZVZZOaSLysJge01tuPXF8rBXGruoJIv35xTTBa9BzaP
+YDOGbGn1ZnajdNagHqCba8vjTLDSpqMvgRj3TFrGHdETA2LDQat38uVxX8gxm68K
+va5Tyv7n+6BQ5YTpJjTPnmSJKaXZrrhdLPvG0OU2TxeEsvbcm5LFQofirOOw86Se
+vzF2cQ94mmHRZiEk0Av3NO0jF93ELDrBCuiccVyEKq6TknuvPQlutCXKDOYSEb8I
+MHctBg==
-----END CERTIFICATE----- \ No newline at end of file
diff --git a/ipn/ipnserver/proxyconnect_js.go b/ipn/ipnserver/proxyconnect_js.go
index 368221e22..27448fa0d 100644
--- a/ipn/ipnserver/proxyconnect_js.go
+++ b/ipn/ipnserver/proxyconnect_js.go
@@ -1,10 +1,10 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipnserver
-
-import "net/http"
-
-func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) {
- panic("unreachable")
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnserver
+
+import "net/http"
+
+func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) {
+ panic("unreachable")
+}
diff --git a/ipn/ipnserver/server_test.go b/ipn/ipnserver/server_test.go
index b7d5ea144..49fb4d01f 100644
--- a/ipn/ipnserver/server_test.go
+++ b/ipn/ipnserver/server_test.go
@@ -1,46 +1,46 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipnserver
-
-import (
- "context"
- "sync"
- "testing"
-)
-
-func TestWaiterSet(t *testing.T) {
- var s waiterSet
-
- wantLen := func(want int, when string) {
- t.Helper()
- if got := len(s); got != want {
- t.Errorf("%s: len = %v; want %v", when, got, want)
- }
- }
- wantLen(0, "initial")
- var mu sync.Mutex
- ctx, cancel := context.WithCancel(context.Background())
-
- ready, cleanup := s.add(&mu, ctx)
- wantLen(1, "after add")
-
- select {
- case <-ready:
- t.Fatal("should not be ready")
- default:
- }
- s.wakeAll()
- <-ready
-
- wantLen(1, "after fire")
- cleanup()
- wantLen(0, "after cleanup")
-
- // And again but on an already-expired ctx.
- cancel()
- ready, cleanup = s.add(&mu, ctx)
- <-ready // shouldn't block
- cleanup()
- wantLen(0, "at end")
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipnserver
+
+import (
+ "context"
+ "sync"
+ "testing"
+)
+
+func TestWaiterSet(t *testing.T) {
+ var s waiterSet
+
+ wantLen := func(want int, when string) {
+ t.Helper()
+ if got := len(s); got != want {
+ t.Errorf("%s: len = %v; want %v", when, got, want)
+ }
+ }
+ wantLen(0, "initial")
+ var mu sync.Mutex
+ ctx, cancel := context.WithCancel(context.Background())
+
+ ready, cleanup := s.add(&mu, ctx)
+ wantLen(1, "after add")
+
+ select {
+ case <-ready:
+ t.Fatal("should not be ready")
+ default:
+ }
+ s.wakeAll()
+ <-ready
+
+ wantLen(1, "after fire")
+ cleanup()
+ wantLen(0, "after cleanup")
+
+ // And again but on an already-expired ctx.
+ cancel()
+ ready, cleanup = s.add(&mu, ctx)
+ <-ready // shouldn't block
+ cleanup()
+ wantLen(0, "at end")
+}
diff --git a/ipn/localapi/disabled_stubs.go b/ipn/localapi/disabled_stubs.go
index c744f34d5..230553c14 100644
--- a/ipn/localapi/disabled_stubs.go
+++ b/ipn/localapi/disabled_stubs.go
@@ -1,15 +1,15 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build ios || android || js
-
-package localapi
-
-import (
- "net/http"
- "runtime"
-)
-
-func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
- http.Error(w, "disabled on "+runtime.GOOS, http.StatusNotFound)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build ios || android || js
+
+package localapi
+
+import (
+ "net/http"
+ "runtime"
+)
+
+func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "disabled on "+runtime.GOOS, http.StatusNotFound)
+}
diff --git a/ipn/localapi/pprof.go b/ipn/localapi/pprof.go
index 8c9429b31..5cc4daca1 100644
--- a/ipn/localapi/pprof.go
+++ b/ipn/localapi/pprof.go
@@ -1,28 +1,28 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !ios && !android && !js
-
-// We don't include it on mobile where we're more memory constrained and
-// there's no CLI to get at the results anyway.
-
-package localapi
-
-import (
- "net/http"
- "net/http/pprof"
-)
-
-func init() {
- servePprofFunc = servePprof
-}
-
-func servePprof(w http.ResponseWriter, r *http.Request) {
- name := r.FormValue("name")
- switch name {
- case "profile":
- pprof.Profile(w, r)
- default:
- pprof.Handler(name).ServeHTTP(w, r)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !ios && !android && !js
+
+// We don't include it on mobile where we're more memory constrained and
+// there's no CLI to get at the results anyway.
+
+package localapi
+
+import (
+ "net/http"
+ "net/http/pprof"
+)
+
+func init() {
+ servePprofFunc = servePprof
+}
+
+func servePprof(w http.ResponseWriter, r *http.Request) {
+ name := r.FormValue("name")
+ switch name {
+ case "profile":
+ pprof.Profile(w, r)
+ default:
+ pprof.Handler(name).ServeHTTP(w, r)
+ }
+}
diff --git a/ipn/policy/policy.go b/ipn/policy/policy.go
index 494a0dc40..834706f31 100644
--- a/ipn/policy/policy.go
+++ b/ipn/policy/policy.go
@@ -1,47 +1,47 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package policy contains various policy decisions that need to be
-// shared between the node client & control server.
-package policy
-
-import (
- "tailscale.com/tailcfg"
-)
-
-// IsInterestingService reports whether service s on the given operating
-// system (a version.OS value) is an interesting enough port to report
-// to our peer nodes for discovery purposes.
-func IsInterestingService(s tailcfg.Service, os string) bool {
- switch s.Proto {
- case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
- return true
- }
- if s.Proto != tailcfg.TCP {
- return false
- }
- if os != "windows" {
- // For non-Windows machines, assume all TCP listeners
- // are interesting enough. We don't see listener spam
- // there.
- return true
- }
- // Windows has tons of TCP listeners. We need to move to a denylist
- // model later, but for now we just allow some common ones:
- switch s.Port {
- case 22, // ssh
- 80, // http
- 443, // https (but no hostname, so little useless)
- 3389, // rdp
- 5900, // vnc
- 32400, // plex
-
- // And now some arbitrary HTTP dev server ports:
- // Eventually we'll remove this and make all ports
- // work, once we nicely filter away noisy system
- // ports.
- 8000, 8080, 8443, 8888:
- return true
- }
- return false
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package policy contains various policy decisions that need to be
+// shared between the node client & control server.
+package policy
+
+import (
+ "tailscale.com/tailcfg"
+)
+
+// IsInterestingService reports whether service s on the given operating
+// system (a version.OS value) is an interesting enough port to report
+// to our peer nodes for discovery purposes.
+func IsInterestingService(s tailcfg.Service, os string) bool {
+ switch s.Proto {
+ case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
+ return true
+ }
+ if s.Proto != tailcfg.TCP {
+ return false
+ }
+ if os != "windows" {
+ // For non-Windows machines, assume all TCP listeners
+ // are interesting enough. We don't see listener spam
+ // there.
+ return true
+ }
+ // Windows has tons of TCP listeners. We need to move to a denylist
+ // model later, but for now we just allow some common ones:
+ switch s.Port {
+ case 22, // ssh
+ 80, // http
+ 443, // https (but no hostname, so little useless)
+ 3389, // rdp
+ 5900, // vnc
+ 32400, // plex
+
+ // And now some arbitrary HTTP dev server ports:
+ // Eventually we'll remove this and make all ports
+ // work, once we nicely filter away noisy system
+ // ports.
+ 8000, 8080, 8443, 8888:
+ return true
+ }
+ return false
+}
diff --git a/ipn/store/awsstore/store_aws.go b/ipn/store/awsstore/store_aws.go
index 0fb78d45a..84059af67 100644
--- a/ipn/store/awsstore/store_aws.go
+++ b/ipn/store/awsstore/store_aws.go
@@ -1,186 +1,186 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux && !ts_omit_aws
-
-// Package awsstore contains an ipn.StateStore implementation using AWS SSM.
-package awsstore
-
-import (
- "context"
- "errors"
- "fmt"
- "regexp"
-
- "github.com/aws/aws-sdk-go-v2/aws"
- "github.com/aws/aws-sdk-go-v2/aws/arn"
- "github.com/aws/aws-sdk-go-v2/config"
- "github.com/aws/aws-sdk-go-v2/service/ssm"
- ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
- "tailscale.com/ipn"
- "tailscale.com/ipn/store/mem"
- "tailscale.com/types/logger"
-)
-
-const (
- parameterNameRxStr = `^parameter(/.*)`
-)
-
-var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
-
-// awsSSMClient is an interface allowing us to mock the couple of
-// API calls we are leveraging with the AWSStore provider
-type awsSSMClient interface {
- GetParameter(ctx context.Context,
- params *ssm.GetParameterInput,
- optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
-
- PutParameter(ctx context.Context,
- params *ssm.PutParameterInput,
- optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
-}
-
-// store is a store which leverages AWS SSM parameter store
-// to persist the state
-type awsStore struct {
- ssmClient awsSSMClient
- ssmARN arn.ARN
-
- memory mem.Store
-}
-
-// New returns a new ipn.StateStore using the AWS SSM storage
-// location given by ssmARN.
-//
-// Note that we store the entire store in a single parameter
-// key, therefore if the state is above 8kb, it can cause
-// Tailscaled to only only store new state in-memory and
-// restarting Tailscaled can fail until you delete your state
-// from the AWS Parameter Store.
-func New(_ logger.Logf, ssmARN string) (ipn.StateStore, error) {
- return newStore(ssmARN, nil)
-}
-
-// newStore is NewStore, but for tests. If client is non-nil, it's
-// used instead of making one.
-func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
- s := &awsStore{
- ssmClient: client,
- }
-
- var err error
-
- // Parse the ARN
- if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
- return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
- }
-
- // Validate the ARN corresponds to the SSM service
- if s.ssmARN.Service != "ssm" {
- return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
- }
-
- // Validate the ARN corresponds to a parameter store resource
- if !parameterNameRx.MatchString(s.ssmARN.Resource) {
- return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
- }
-
- if s.ssmClient == nil {
- var cfg aws.Config
- if cfg, err = config.LoadDefaultConfig(
- context.TODO(),
- config.WithRegion(s.ssmARN.Region),
- ); err != nil {
- return nil, err
- }
- s.ssmClient = ssm.NewFromConfig(cfg)
- }
-
- // Hydrate cache with the potentially current state
- if err := s.LoadState(); err != nil {
- return nil, err
- }
- return s, nil
-
-}
-
-// LoadState attempts to read the state from AWS SSM parameter store key.
-func (s *awsStore) LoadState() error {
- param, err := s.ssmClient.GetParameter(
- context.TODO(),
- &ssm.GetParameterInput{
- Name: aws.String(s.ParameterName()),
- WithDecryption: aws.Bool(true),
- },
- )
-
- if err != nil {
- var pnf *ssmTypes.ParameterNotFound
- if errors.As(err, &pnf) {
- // Create the parameter as it does not exist yet
- // and return directly as it is defacto empty
- return s.persistState()
- }
- return err
- }
-
- // Load the content in-memory
- return s.memory.LoadFromJSON([]byte(*param.Parameter.Value))
-}
-
-// ParameterName returns the parameter name extracted from
-// the provided ARN
-func (s *awsStore) ParameterName() (name string) {
- values := parameterNameRx.FindStringSubmatch(s.ssmARN.Resource)
- if len(values) == 2 {
- name = values[1]
- }
- return
-}
-
-// String returns the awsStore and the ARN of the SSM parameter store
-// configured to store the state
-func (s *awsStore) String() string { return fmt.Sprintf("awsStore(%q)", s.ssmARN.String()) }
-
-// ReadState implements the Store interface.
-func (s *awsStore) ReadState(id ipn.StateKey) (bs []byte, err error) {
- return s.memory.ReadState(id)
-}
-
-// WriteState implements the Store interface.
-func (s *awsStore) WriteState(id ipn.StateKey, bs []byte) (err error) {
- // Write the state in-memory
- if err = s.memory.WriteState(id, bs); err != nil {
- return
- }
-
- // Persist the state in AWS SSM parameter store
- return s.persistState()
-}
-
-// PersistState saves the states into the AWS SSM parameter store
-func (s *awsStore) persistState() error {
- // Generate JSON from in-memory cache
- bs, err := s.memory.ExportToJSON()
- if err != nil {
- return err
- }
-
- // Store in AWS SSM parameter store.
- //
- // We use intelligent tiering so that when the state is below 4kb, it uses Standard tiering
- // which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
- // doubling the capacity to 8kb per the following docs:
- // https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
- _, err = s.ssmClient.PutParameter(
- context.TODO(),
- &ssm.PutParameterInput{
- Name: aws.String(s.ParameterName()),
- Value: aws.String(string(bs)),
- Overwrite: aws.Bool(true),
- Tier: ssmTypes.ParameterTierIntelligentTiering,
- Type: ssmTypes.ParameterTypeSecureString,
- },
- )
- return err
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux && !ts_omit_aws
+
+// Package awsstore contains an ipn.StateStore implementation using AWS SSM.
+package awsstore
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "regexp"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/aws/arn"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/ssm"
+ ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+ "tailscale.com/ipn"
+ "tailscale.com/ipn/store/mem"
+ "tailscale.com/types/logger"
+)
+
+const (
+ parameterNameRxStr = `^parameter(/.*)`
+)
+
+var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
+
+// awsSSMClient is an interface allowing us to mock the couple of
+// API calls we are leveraging with the AWSStore provider
+type awsSSMClient interface {
+ GetParameter(ctx context.Context,
+ params *ssm.GetParameterInput,
+ optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
+
+ PutParameter(ctx context.Context,
+ params *ssm.PutParameterInput,
+ optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
+}
+
+// store is a store which leverages AWS SSM parameter store
+// to persist the state
+type awsStore struct {
+ ssmClient awsSSMClient
+ ssmARN arn.ARN
+
+ memory mem.Store
+}
+
+// New returns a new ipn.StateStore using the AWS SSM storage
+// location given by ssmARN.
+//
+// Note that we store the entire store in a single parameter
+// key, therefore if the state is above 8kb, it can cause
+// Tailscaled to only only store new state in-memory and
+// restarting Tailscaled can fail until you delete your state
+// from the AWS Parameter Store.
+func New(_ logger.Logf, ssmARN string) (ipn.StateStore, error) {
+ return newStore(ssmARN, nil)
+}
+
+// newStore is NewStore, but for tests. If client is non-nil, it's
+// used instead of making one.
+func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
+ s := &awsStore{
+ ssmClient: client,
+ }
+
+ var err error
+
+ // Parse the ARN
+ if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
+ return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
+ }
+
+ // Validate the ARN corresponds to the SSM service
+ if s.ssmARN.Service != "ssm" {
+ return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
+ }
+
+ // Validate the ARN corresponds to a parameter store resource
+ if !parameterNameRx.MatchString(s.ssmARN.Resource) {
+ return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
+ }
+
+ if s.ssmClient == nil {
+ var cfg aws.Config
+ if cfg, err = config.LoadDefaultConfig(
+ context.TODO(),
+ config.WithRegion(s.ssmARN.Region),
+ ); err != nil {
+ return nil, err
+ }
+ s.ssmClient = ssm.NewFromConfig(cfg)
+ }
+
+ // Hydrate cache with the potentially current state
+ if err := s.LoadState(); err != nil {
+ return nil, err
+ }
+ return s, nil
+
+}
+
+// LoadState attempts to read the state from AWS SSM parameter store key.
+func (s *awsStore) LoadState() error {
+ param, err := s.ssmClient.GetParameter(
+ context.TODO(),
+ &ssm.GetParameterInput{
+ Name: aws.String(s.ParameterName()),
+ WithDecryption: aws.Bool(true),
+ },
+ )
+
+ if err != nil {
+ var pnf *ssmTypes.ParameterNotFound
+ if errors.As(err, &pnf) {
+ // Create the parameter as it does not exist yet
+ // and return directly as it is defacto empty
+ return s.persistState()
+ }
+ return err
+ }
+
+ // Load the content in-memory
+ return s.memory.LoadFromJSON([]byte(*param.Parameter.Value))
+}
+
+// ParameterName returns the parameter name extracted from
+// the provided ARN
+func (s *awsStore) ParameterName() (name string) {
+ values := parameterNameRx.FindStringSubmatch(s.ssmARN.Resource)
+ if len(values) == 2 {
+ name = values[1]
+ }
+ return
+}
+
+// String returns the awsStore and the ARN of the SSM parameter store
+// configured to store the state
+func (s *awsStore) String() string { return fmt.Sprintf("awsStore(%q)", s.ssmARN.String()) }
+
+// ReadState implements the Store interface.
+func (s *awsStore) ReadState(id ipn.StateKey) (bs []byte, err error) {
+ return s.memory.ReadState(id)
+}
+
+// WriteState implements the Store interface.
+func (s *awsStore) WriteState(id ipn.StateKey, bs []byte) (err error) {
+ // Write the state in-memory
+ if err = s.memory.WriteState(id, bs); err != nil {
+ return
+ }
+
+ // Persist the state in AWS SSM parameter store
+ return s.persistState()
+}
+
+// PersistState saves the states into the AWS SSM parameter store
+func (s *awsStore) persistState() error {
+ // Generate JSON from in-memory cache
+ bs, err := s.memory.ExportToJSON()
+ if err != nil {
+ return err
+ }
+
+ // Store in AWS SSM parameter store.
+ //
+ // We use intelligent tiering so that when the state is below 4kb, it uses Standard tiering
+ // which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
+ // doubling the capacity to 8kb per the following docs:
+ // https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
+ _, err = s.ssmClient.PutParameter(
+ context.TODO(),
+ &ssm.PutParameterInput{
+ Name: aws.String(s.ParameterName()),
+ Value: aws.String(string(bs)),
+ Overwrite: aws.Bool(true),
+ Tier: ssmTypes.ParameterTierIntelligentTiering,
+ Type: ssmTypes.ParameterTypeSecureString,
+ },
+ )
+ return err
+}
diff --git a/ipn/store/awsstore/store_aws_stub.go b/ipn/store/awsstore/store_aws_stub.go
index 8d2156ce9..7be8b858d 100644
--- a/ipn/store/awsstore/store_aws_stub.go
+++ b/ipn/store/awsstore/store_aws_stub.go
@@ -1,18 +1,18 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !linux || ts_omit_aws
-
-package awsstore
-
-import (
- "fmt"
- "runtime"
-
- "tailscale.com/ipn"
- "tailscale.com/types/logger"
-)
-
-func New(logger.Logf, string) (ipn.StateStore, error) {
- return nil, fmt.Errorf("AWS store is not supported on %v", runtime.GOOS)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !linux || ts_omit_aws
+
+package awsstore
+
+import (
+ "fmt"
+ "runtime"
+
+ "tailscale.com/ipn"
+ "tailscale.com/types/logger"
+)
+
+func New(logger.Logf, string) (ipn.StateStore, error) {
+ return nil, fmt.Errorf("AWS store is not supported on %v", runtime.GOOS)
+}
diff --git a/ipn/store/awsstore/store_aws_test.go b/ipn/store/awsstore/store_aws_test.go
index f6c8fedb3..54e6e18cb 100644
--- a/ipn/store/awsstore/store_aws_test.go
+++ b/ipn/store/awsstore/store_aws_test.go
@@ -1,164 +1,164 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build linux
-
-package awsstore
-
-import (
- "context"
- "testing"
-
- "github.com/aws/aws-sdk-go-v2/aws"
- "github.com/aws/aws-sdk-go-v2/aws/arn"
- "github.com/aws/aws-sdk-go-v2/service/ssm"
- ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
- "tailscale.com/ipn"
- "tailscale.com/tstest"
-)
-
-type mockedAWSSSMClient struct {
- value string
-}
-
-func (sp *mockedAWSSSMClient) GetParameter(_ context.Context, input *ssm.GetParameterInput, _ ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
- output := new(ssm.GetParameterOutput)
- if sp.value == "" {
- return output, &ssmTypes.ParameterNotFound{}
- }
-
- output.Parameter = &ssmTypes.Parameter{
- Value: aws.String(sp.value),
- }
-
- return output, nil
-}
-
-func (sp *mockedAWSSSMClient) PutParameter(_ context.Context, input *ssm.PutParameterInput, _ ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
- sp.value = *input.Value
- return new(ssm.PutParameterOutput), nil
-}
-
-func TestAWSStoreString(t *testing.T) {
- store := &awsStore{
- ssmARN: arn.ARN{
- Service: "ssm",
- Region: "eu-west-1",
- AccountID: "123456789",
- Resource: "parameter/foo",
- },
- }
- want := "awsStore(\"arn::ssm:eu-west-1:123456789:parameter/foo\")"
- if got := store.String(); got != want {
- t.Errorf("AWSStore.String = %q; want %q", got, want)
- }
-}
-
-func TestNewAWSStore(t *testing.T) {
- tstest.PanicOnLog()
-
- mc := &mockedAWSSSMClient{}
- storeParameterARN := arn.ARN{
- Service: "ssm",
- Region: "eu-west-1",
- AccountID: "123456789",
- Resource: "parameter/foo",
- }
-
- s, err := newStore(storeParameterARN.String(), mc)
- if err != nil {
- t.Fatalf("creating aws store failed: %v", err)
- }
- testStoreSemantics(t, s)
-
- // Build a brand new file store and check that both IDs written
- // above are still there.
- s2, err := newStore(storeParameterARN.String(), mc)
- if err != nil {
- t.Fatalf("creating second aws store failed: %v", err)
- }
- store2 := s.(*awsStore)
-
- // This is specific to the test, with the non-mocked API, LoadState() should
- // have been already called and successful as no err is returned from NewAWSStore()
- s2.(*awsStore).LoadState()
-
- expected := map[ipn.StateKey]string{
- "foo": "bar",
- "baz": "quux",
- }
- for id, want := range expected {
- bs, err := store2.ReadState(id)
- if err != nil {
- t.Errorf("reading %q (2nd store): %v", id, err)
- }
- if string(bs) != want {
- t.Errorf("reading %q (2nd store): got %q, want %q", id, string(bs), want)
- }
- }
-}
-
-func testStoreSemantics(t *testing.T, store ipn.StateStore) {
- t.Helper()
-
- tests := []struct {
- // if true, data is data to write. If false, data is expected
- // output of read.
- write bool
- id ipn.StateKey
- data string
- // If write=false, true if we expect a not-exist error.
- notExists bool
- }{
- {
- id: "foo",
- notExists: true,
- },
- {
- write: true,
- id: "foo",
- data: "bar",
- },
- {
- id: "foo",
- data: "bar",
- },
- {
- id: "baz",
- notExists: true,
- },
- {
- write: true,
- id: "baz",
- data: "quux",
- },
- {
- id: "foo",
- data: "bar",
- },
- {
- id: "baz",
- data: "quux",
- },
- }
-
- for _, test := range tests {
- if test.write {
- if err := store.WriteState(test.id, []byte(test.data)); err != nil {
- t.Errorf("writing %q to %q: %v", test.data, test.id, err)
- }
- } else {
- bs, err := store.ReadState(test.id)
- if err != nil {
- if test.notExists && err == ipn.ErrStateNotExist {
- continue
- }
- t.Errorf("reading %q: %v", test.id, err)
- continue
- }
- if string(bs) != test.data {
- t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
- }
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux
+
+package awsstore
+
+import (
+ "context"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/aws/arn"
+ "github.com/aws/aws-sdk-go-v2/service/ssm"
+ ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
+ "tailscale.com/ipn"
+ "tailscale.com/tstest"
+)
+
+type mockedAWSSSMClient struct {
+ value string
+}
+
+func (sp *mockedAWSSSMClient) GetParameter(_ context.Context, input *ssm.GetParameterInput, _ ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
+ output := new(ssm.GetParameterOutput)
+ if sp.value == "" {
+ return output, &ssmTypes.ParameterNotFound{}
+ }
+
+ output.Parameter = &ssmTypes.Parameter{
+ Value: aws.String(sp.value),
+ }
+
+ return output, nil
+}
+
+func (sp *mockedAWSSSMClient) PutParameter(_ context.Context, input *ssm.PutParameterInput, _ ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
+ sp.value = *input.Value
+ return new(ssm.PutParameterOutput), nil
+}
+
+func TestAWSStoreString(t *testing.T) {
+ store := &awsStore{
+ ssmARN: arn.ARN{
+ Service: "ssm",
+ Region: "eu-west-1",
+ AccountID: "123456789",
+ Resource: "parameter/foo",
+ },
+ }
+ want := "awsStore(\"arn::ssm:eu-west-1:123456789:parameter/foo\")"
+ if got := store.String(); got != want {
+ t.Errorf("AWSStore.String = %q; want %q", got, want)
+ }
+}
+
+func TestNewAWSStore(t *testing.T) {
+ tstest.PanicOnLog()
+
+ mc := &mockedAWSSSMClient{}
+ storeParameterARN := arn.ARN{
+ Service: "ssm",
+ Region: "eu-west-1",
+ AccountID: "123456789",
+ Resource: "parameter/foo",
+ }
+
+ s, err := newStore(storeParameterARN.String(), mc)
+ if err != nil {
+ t.Fatalf("creating aws store failed: %v", err)
+ }
+ testStoreSemantics(t, s)
+
+ // Build a brand new file store and check that both IDs written
+ // above are still there.
+ s2, err := newStore(storeParameterARN.String(), mc)
+ if err != nil {
+ t.Fatalf("creating second aws store failed: %v", err)
+ }
+ store2 := s.(*awsStore)
+
+ // This is specific to the test, with the non-mocked API, LoadState() should
+ // have been already called and successful as no err is returned from NewAWSStore()
+ s2.(*awsStore).LoadState()
+
+ expected := map[ipn.StateKey]string{
+ "foo": "bar",
+ "baz": "quux",
+ }
+ for id, want := range expected {
+ bs, err := store2.ReadState(id)
+ if err != nil {
+ t.Errorf("reading %q (2nd store): %v", id, err)
+ }
+ if string(bs) != want {
+ t.Errorf("reading %q (2nd store): got %q, want %q", id, string(bs), want)
+ }
+ }
+}
+
+func testStoreSemantics(t *testing.T, store ipn.StateStore) {
+ t.Helper()
+
+ tests := []struct {
+ // if true, data is data to write. If false, data is expected
+ // output of read.
+ write bool
+ id ipn.StateKey
+ data string
+ // If write=false, true if we expect a not-exist error.
+ notExists bool
+ }{
+ {
+ id: "foo",
+ notExists: true,
+ },
+ {
+ write: true,
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "baz",
+ notExists: true,
+ },
+ {
+ write: true,
+ id: "baz",
+ data: "quux",
+ },
+ {
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "baz",
+ data: "quux",
+ },
+ }
+
+ for _, test := range tests {
+ if test.write {
+ if err := store.WriteState(test.id, []byte(test.data)); err != nil {
+ t.Errorf("writing %q to %q: %v", test.data, test.id, err)
+ }
+ } else {
+ bs, err := store.ReadState(test.id)
+ if err != nil {
+ if test.notExists && err == ipn.ErrStateNotExist {
+ continue
+ }
+ t.Errorf("reading %q: %v", test.id, err)
+ continue
+ }
+ if string(bs) != test.data {
+ t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
+ }
+ }
+ }
+}
diff --git a/ipn/store/stores_test.go b/ipn/store/stores_test.go
index ea09e6ea6..69aa79193 100644
--- a/ipn/store/stores_test.go
+++ b/ipn/store/stores_test.go
@@ -1,179 +1,179 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package store
-
-import (
- "path/filepath"
- "testing"
-
- "tailscale.com/ipn"
- "tailscale.com/ipn/store/mem"
- "tailscale.com/tstest"
- "tailscale.com/types/logger"
-)
-
-func TestNewStore(t *testing.T) {
- regOnce.Do(registerDefaultStores)
- t.Cleanup(func() {
- knownStores = map[string]Provider{}
- registerDefaultStores()
- })
- knownStores = map[string]Provider{}
-
- type store1 struct {
- ipn.StateStore
- path string
- }
-
- type store2 struct {
- ipn.StateStore
- path string
- }
-
- Register("arn:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
- return &store1{new(mem.Store), path}, nil
- })
- Register("kube:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
- return &store2{new(mem.Store), path}, nil
- })
- Register("mem:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
- return new(mem.Store), nil
- })
-
- path := "mem:abcd"
- if s, err := New(t.Logf, path); err != nil {
- t.Fatalf("%q: %v", path, err)
- } else if _, ok := s.(*mem.Store); !ok {
- t.Fatalf("%q: got: %T, want: %T", path, s, new(mem.Store))
- }
-
- path = "arn:foo"
- if s, err := New(t.Logf, path); err != nil {
- t.Fatalf("%q: %v", path, err)
- } else if _, ok := s.(*store1); !ok {
- t.Fatalf("%q: got: %T, want: %T", path, s, new(store1))
- }
-
- path = "kube:abcd"
- if s, err := New(t.Logf, path); err != nil {
- t.Fatalf("%q: %v", path, err)
- } else if _, ok := s.(*store2); !ok {
- t.Fatalf("%q: got: %T, want: %T", path, s, new(store2))
- }
-
- path = filepath.Join(t.TempDir(), "state")
- if s, err := New(t.Logf, path); err != nil {
- t.Fatalf("%q: %v", path, err)
- } else if _, ok := s.(*FileStore); !ok {
- t.Fatalf("%q: got: %T, want: %T", path, s, new(FileStore))
- }
-}
-
-func testStoreSemantics(t *testing.T, store ipn.StateStore) {
- t.Helper()
-
- tests := []struct {
- // if true, data is data to write. If false, data is expected
- // output of read.
- write bool
- id ipn.StateKey
- data string
- // If write=false, true if we expect a not-exist error.
- notExists bool
- }{
- {
- id: "foo",
- notExists: true,
- },
- {
- write: true,
- id: "foo",
- data: "bar",
- },
- {
- id: "foo",
- data: "bar",
- },
- {
- id: "baz",
- notExists: true,
- },
- {
- write: true,
- id: "baz",
- data: "quux",
- },
- {
- id: "foo",
- data: "bar",
- },
- {
- id: "baz",
- data: "quux",
- },
- }
-
- for _, test := range tests {
- if test.write {
- if err := store.WriteState(test.id, []byte(test.data)); err != nil {
- t.Errorf("writing %q to %q: %v", test.data, test.id, err)
- }
- } else {
- bs, err := store.ReadState(test.id)
- if err != nil {
- if test.notExists && err == ipn.ErrStateNotExist {
- continue
- }
- t.Errorf("reading %q: %v", test.id, err)
- continue
- }
- if string(bs) != test.data {
- t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
- }
- }
- }
-}
-
-func TestMemoryStore(t *testing.T) {
- tstest.PanicOnLog()
-
- store := new(mem.Store)
- testStoreSemantics(t, store)
-}
-
-func TestFileStore(t *testing.T) {
- tstest.PanicOnLog()
-
- dir := t.TempDir()
- path := filepath.Join(dir, "test-file-store.conf")
-
- store, err := NewFileStore(nil, path)
- if err != nil {
- t.Fatalf("creating file store failed: %v", err)
- }
-
- testStoreSemantics(t, store)
-
- // Build a brand new file store and check that both IDs written
- // above are still there.
- store, err = NewFileStore(nil, path)
- if err != nil {
- t.Fatalf("creating second file store failed: %v", err)
- }
-
- expected := map[ipn.StateKey]string{
- "foo": "bar",
- "baz": "quux",
- }
- for key, want := range expected {
- bs, err := store.ReadState(key)
- if err != nil {
- t.Errorf("reading %q (2nd store): %v", key, err)
- continue
- }
- if string(bs) != want {
- t.Errorf("reading %q (2nd store): got %q, want %q", key, bs, want)
- }
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package store
+
+import (
+ "path/filepath"
+ "testing"
+
+ "tailscale.com/ipn"
+ "tailscale.com/ipn/store/mem"
+ "tailscale.com/tstest"
+ "tailscale.com/types/logger"
+)
+
+func TestNewStore(t *testing.T) {
+ regOnce.Do(registerDefaultStores)
+ t.Cleanup(func() {
+ knownStores = map[string]Provider{}
+ registerDefaultStores()
+ })
+ knownStores = map[string]Provider{}
+
+ type store1 struct {
+ ipn.StateStore
+ path string
+ }
+
+ type store2 struct {
+ ipn.StateStore
+ path string
+ }
+
+ Register("arn:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
+ return &store1{new(mem.Store), path}, nil
+ })
+ Register("kube:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
+ return &store2{new(mem.Store), path}, nil
+ })
+ Register("mem:", func(_ logger.Logf, path string) (ipn.StateStore, error) {
+ return new(mem.Store), nil
+ })
+
+ path := "mem:abcd"
+ if s, err := New(t.Logf, path); err != nil {
+ t.Fatalf("%q: %v", path, err)
+ } else if _, ok := s.(*mem.Store); !ok {
+ t.Fatalf("%q: got: %T, want: %T", path, s, new(mem.Store))
+ }
+
+ path = "arn:foo"
+ if s, err := New(t.Logf, path); err != nil {
+ t.Fatalf("%q: %v", path, err)
+ } else if _, ok := s.(*store1); !ok {
+ t.Fatalf("%q: got: %T, want: %T", path, s, new(store1))
+ }
+
+ path = "kube:abcd"
+ if s, err := New(t.Logf, path); err != nil {
+ t.Fatalf("%q: %v", path, err)
+ } else if _, ok := s.(*store2); !ok {
+ t.Fatalf("%q: got: %T, want: %T", path, s, new(store2))
+ }
+
+ path = filepath.Join(t.TempDir(), "state")
+ if s, err := New(t.Logf, path); err != nil {
+ t.Fatalf("%q: %v", path, err)
+ } else if _, ok := s.(*FileStore); !ok {
+ t.Fatalf("%q: got: %T, want: %T", path, s, new(FileStore))
+ }
+}
+
+func testStoreSemantics(t *testing.T, store ipn.StateStore) {
+ t.Helper()
+
+ tests := []struct {
+ // if true, data is data to write. If false, data is expected
+ // output of read.
+ write bool
+ id ipn.StateKey
+ data string
+ // If write=false, true if we expect a not-exist error.
+ notExists bool
+ }{
+ {
+ id: "foo",
+ notExists: true,
+ },
+ {
+ write: true,
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "baz",
+ notExists: true,
+ },
+ {
+ write: true,
+ id: "baz",
+ data: "quux",
+ },
+ {
+ id: "foo",
+ data: "bar",
+ },
+ {
+ id: "baz",
+ data: "quux",
+ },
+ }
+
+ for _, test := range tests {
+ if test.write {
+ if err := store.WriteState(test.id, []byte(test.data)); err != nil {
+ t.Errorf("writing %q to %q: %v", test.data, test.id, err)
+ }
+ } else {
+ bs, err := store.ReadState(test.id)
+ if err != nil {
+ if test.notExists && err == ipn.ErrStateNotExist {
+ continue
+ }
+ t.Errorf("reading %q: %v", test.id, err)
+ continue
+ }
+ if string(bs) != test.data {
+ t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
+ }
+ }
+ }
+}
+
+func TestMemoryStore(t *testing.T) {
+ tstest.PanicOnLog()
+
+ store := new(mem.Store)
+ testStoreSemantics(t, store)
+}
+
+func TestFileStore(t *testing.T) {
+ tstest.PanicOnLog()
+
+ dir := t.TempDir()
+ path := filepath.Join(dir, "test-file-store.conf")
+
+ store, err := NewFileStore(nil, path)
+ if err != nil {
+ t.Fatalf("creating file store failed: %v", err)
+ }
+
+ testStoreSemantics(t, store)
+
+ // Build a brand new file store and check that both IDs written
+ // above are still there.
+ store, err = NewFileStore(nil, path)
+ if err != nil {
+ t.Fatalf("creating second file store failed: %v", err)
+ }
+
+ expected := map[ipn.StateKey]string{
+ "foo": "bar",
+ "baz": "quux",
+ }
+ for key, want := range expected {
+ bs, err := store.ReadState(key)
+ if err != nil {
+ t.Errorf("reading %q (2nd store): %v", key, err)
+ continue
+ }
+ if string(bs) != want {
+ t.Errorf("reading %q (2nd store): got %q, want %q", key, bs, want)
+ }
+ }
+}
diff --git a/ipn/store_test.go b/ipn/store_test.go
index fcc082d8a..330f67969 100644
--- a/ipn/store_test.go
+++ b/ipn/store_test.go
@@ -1,48 +1,48 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package ipn
-
-import (
- "bytes"
- "sync"
- "testing"
-
- "tailscale.com/util/mak"
-)
-
-type memStore struct {
- mu sync.Mutex
- writes int
- m map[StateKey][]byte
-}
-
-func (s *memStore) ReadState(k StateKey) ([]byte, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- return bytes.Clone(s.m[k]), nil
-}
-
-func (s *memStore) WriteState(k StateKey, v []byte) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- mak.Set(&s.m, k, bytes.Clone(v))
- s.writes++
- return nil
-}
-
-func TestWriteState(t *testing.T) {
- var ss StateStore = new(memStore)
- WriteState(ss, "foo", []byte("bar"))
- WriteState(ss, "foo", []byte("bar"))
- got, err := ss.ReadState("foo")
- if err != nil {
- t.Fatal(err)
- }
- if want := []byte("bar"); !bytes.Equal(got, want) {
- t.Errorf("got %q; want %q", got, want)
- }
- if got, want := ss.(*memStore).writes, 1; got != want {
- t.Errorf("got %d writes; want %d", got, want)
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package ipn
+
+import (
+ "bytes"
+ "sync"
+ "testing"
+
+ "tailscale.com/util/mak"
+)
+
+type memStore struct {
+ mu sync.Mutex
+ writes int
+ m map[StateKey][]byte
+}
+
+func (s *memStore) ReadState(k StateKey) ([]byte, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return bytes.Clone(s.m[k]), nil
+}
+
+func (s *memStore) WriteState(k StateKey, v []byte) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ mak.Set(&s.m, k, bytes.Clone(v))
+ s.writes++
+ return nil
+}
+
+func TestWriteState(t *testing.T) {
+ var ss StateStore = new(memStore)
+ WriteState(ss, "foo", []byte("bar"))
+ WriteState(ss, "foo", []byte("bar"))
+ got, err := ss.ReadState("foo")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := []byte("bar"); !bytes.Equal(got, want) {
+ t.Errorf("got %q; want %q", got, want)
+ }
+ if got, want := ss.(*memStore).writes, 1; got != want {
+ t.Errorf("got %d writes; want %d", got, want)
+ }
+}