summaryrefslogtreecommitdiffhomepage
path: root/ipn/ipnlocal
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/ipnlocal
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/ipnlocal')
-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
7 files changed, 462 insertions, 462 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