summaryrefslogtreecommitdiffhomepage
path: root/control/controlclient
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 /control/controlclient
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 'control/controlclient')
-rw-r--r--control/controlclient/sign.go84
-rw-r--r--control/controlclient/sign_supported_test.go472
-rw-r--r--control/controlclient/sign_unsupported.go32
-rw-r--r--control/controlclient/status.go250
4 files changed, 419 insertions, 419 deletions
diff --git a/control/controlclient/sign.go b/control/controlclient/sign.go
index e3a479c28..5e72f1cf4 100644
--- a/control/controlclient/sign.go
+++ b/control/controlclient/sign.go
@@ -1,42 +1,42 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package controlclient
-
-import (
- "crypto"
- "errors"
- "fmt"
- "time"
-
- "tailscale.com/tailcfg"
- "tailscale.com/types/key"
-)
-
-var (
- errNoCertStore = errors.New("no certificate store")
- errCertificateNotConfigured = errors.New("no certificate subject configured")
- errUnsupportedSignatureVersion = errors.New("unsupported signature version")
-)
-
-// HashRegisterRequest generates the hash required sign or verify a
-// tailcfg.RegisterRequest.
-func HashRegisterRequest(
- version tailcfg.SignatureType, ts time.Time, serverURL string, deviceCert []byte,
- serverPubKey, machinePubKey key.MachinePublic) ([]byte, error) {
- h := crypto.SHA256.New()
-
- // hash.Hash.Write never returns an error, so we don't check for one here.
- switch version {
- case tailcfg.SignatureV1:
- fmt.Fprintf(h, "%s%s%s%s%s",
- ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey.ShortString(), machinePubKey.ShortString())
- case tailcfg.SignatureV2:
- fmt.Fprintf(h, "%s%s%s%s%s",
- ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
- default:
- return nil, errUnsupportedSignatureVersion
- }
-
- return h.Sum(nil), nil
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package controlclient
+
+import (
+ "crypto"
+ "errors"
+ "fmt"
+ "time"
+
+ "tailscale.com/tailcfg"
+ "tailscale.com/types/key"
+)
+
+var (
+ errNoCertStore = errors.New("no certificate store")
+ errCertificateNotConfigured = errors.New("no certificate subject configured")
+ errUnsupportedSignatureVersion = errors.New("unsupported signature version")
+)
+
+// HashRegisterRequest generates the hash required sign or verify a
+// tailcfg.RegisterRequest.
+func HashRegisterRequest(
+ version tailcfg.SignatureType, ts time.Time, serverURL string, deviceCert []byte,
+ serverPubKey, machinePubKey key.MachinePublic) ([]byte, error) {
+ h := crypto.SHA256.New()
+
+ // hash.Hash.Write never returns an error, so we don't check for one here.
+ switch version {
+ case tailcfg.SignatureV1:
+ fmt.Fprintf(h, "%s%s%s%s%s",
+ ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey.ShortString(), machinePubKey.ShortString())
+ case tailcfg.SignatureV2:
+ fmt.Fprintf(h, "%s%s%s%s%s",
+ ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
+ default:
+ return nil, errUnsupportedSignatureVersion
+ }
+
+ return h.Sum(nil), nil
+}
diff --git a/control/controlclient/sign_supported_test.go b/control/controlclient/sign_supported_test.go
index e20349a4e..ca41794d1 100644
--- a/control/controlclient/sign_supported_test.go
+++ b/control/controlclient/sign_supported_test.go
@@ -1,236 +1,236 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build windows && cgo
-
-package controlclient
-
-import (
- "crypto"
- "crypto/x509"
- "crypto/x509/pkix"
- "errors"
- "reflect"
- "testing"
- "time"
-
- "github.com/tailscale/certstore"
-)
-
-const (
- testRootCommonName = "testroot"
- testRootSubject = "CN=testroot"
-)
-
-type testIdentity struct {
- chain []*x509.Certificate
-}
-
-func makeChain(rootCommonName string, notBefore, notAfter time.Time) []*x509.Certificate {
- return []*x509.Certificate{
- {
- NotBefore: notBefore,
- NotAfter: notAfter,
- PublicKeyAlgorithm: x509.RSA,
- },
- {
- Subject: pkix.Name{
- CommonName: rootCommonName,
- },
- PublicKeyAlgorithm: x509.RSA,
- },
- }
-}
-
-func (t *testIdentity) Certificate() (*x509.Certificate, error) {
- return t.chain[0], nil
-}
-
-func (t *testIdentity) CertificateChain() ([]*x509.Certificate, error) {
- return t.chain, nil
-}
-
-func (t *testIdentity) Signer() (crypto.Signer, error) {
- return nil, errors.New("not implemented")
-}
-
-func (t *testIdentity) Delete() error {
- return errors.New("not implemented")
-}
-
-func (t *testIdentity) Close() {}
-
-func TestSelectIdentityFromSlice(t *testing.T) {
- var times []time.Time
- for _, ts := range []string{
- "2000-01-01T00:00:00Z",
- "2001-01-01T00:00:00Z",
- "2002-01-01T00:00:00Z",
- "2003-01-01T00:00:00Z",
- } {
- tm, err := time.Parse(time.RFC3339, ts)
- if err != nil {
- t.Fatal(err)
- }
- times = append(times, tm)
- }
-
- tests := []struct {
- name string
- subject string
- ids []certstore.Identity
- now time.Time
- // wantIndex is an index into ids, or -1 for nil.
- wantIndex int
- }{
- {
- name: "single unexpired identity",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[2]),
- },
- },
- now: times[1],
- wantIndex: 0,
- },
- {
- name: "single expired identity",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[1]),
- },
- },
- now: times[2],
- wantIndex: -1,
- },
- {
- name: "unrelated ids",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain("something", times[0], times[2]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[2]),
- },
- &testIdentity{
- chain: makeChain("else", times[0], times[2]),
- },
- },
- now: times[1],
- wantIndex: 1,
- },
- {
- name: "expired with unrelated ids",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain("something", times[0], times[3]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[1]),
- },
- &testIdentity{
- chain: makeChain("else", times[0], times[3]),
- },
- },
- now: times[2],
- wantIndex: -1,
- },
- {
- name: "one expired",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[1]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[1], times[3]),
- },
- },
- now: times[2],
- wantIndex: 1,
- },
- {
- name: "two certs both unexpired",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[3]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[1], times[3]),
- },
- },
- now: times[2],
- wantIndex: 1,
- },
- {
- name: "two unexpired one expired",
- subject: testRootSubject,
- ids: []certstore.Identity{
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[3]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[1], times[3]),
- },
- &testIdentity{
- chain: makeChain(testRootCommonName, times[0], times[1]),
- },
- },
- now: times[2],
- wantIndex: 1,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- gotId, gotChain := selectIdentityFromSlice(tt.subject, tt.ids, tt.now)
-
- if gotId == nil && gotChain != nil {
- t.Error("id is nil: got non-nil chain, want nil chain")
- return
- }
- if gotId != nil && gotChain == nil {
- t.Error("id is not nil: got nil chain, want non-nil chain")
- return
- }
- if tt.wantIndex == -1 {
- if gotId != nil {
- t.Error("got non-nil id, want nil id")
- }
- return
- }
- if gotId == nil {
- t.Error("got nil id, want non-nil id")
- return
- }
- if gotId != tt.ids[tt.wantIndex] {
- found := -1
- for i := range tt.ids {
- if tt.ids[i] == gotId {
- found = i
- break
- }
- }
- if found == -1 {
- t.Errorf("got unknown id, want id at index %v", tt.wantIndex)
- } else {
- t.Errorf("got id at index %v, want id at index %v", found, tt.wantIndex)
- }
- }
-
- tid, ok := tt.ids[tt.wantIndex].(*testIdentity)
- if !ok {
- t.Error("got non-testIdentity, want testIdentity")
- return
- }
-
- if !reflect.DeepEqual(tid.chain, gotChain) {
- t.Errorf("got unknown chain, want chain from id at index %v", tt.wantIndex)
- }
- })
- }
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build windows && cgo
+
+package controlclient
+
+import (
+ "crypto"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "errors"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/tailscale/certstore"
+)
+
+const (
+ testRootCommonName = "testroot"
+ testRootSubject = "CN=testroot"
+)
+
+type testIdentity struct {
+ chain []*x509.Certificate
+}
+
+func makeChain(rootCommonName string, notBefore, notAfter time.Time) []*x509.Certificate {
+ return []*x509.Certificate{
+ {
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ PublicKeyAlgorithm: x509.RSA,
+ },
+ {
+ Subject: pkix.Name{
+ CommonName: rootCommonName,
+ },
+ PublicKeyAlgorithm: x509.RSA,
+ },
+ }
+}
+
+func (t *testIdentity) Certificate() (*x509.Certificate, error) {
+ return t.chain[0], nil
+}
+
+func (t *testIdentity) CertificateChain() ([]*x509.Certificate, error) {
+ return t.chain, nil
+}
+
+func (t *testIdentity) Signer() (crypto.Signer, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (t *testIdentity) Delete() error {
+ return errors.New("not implemented")
+}
+
+func (t *testIdentity) Close() {}
+
+func TestSelectIdentityFromSlice(t *testing.T) {
+ var times []time.Time
+ for _, ts := range []string{
+ "2000-01-01T00:00:00Z",
+ "2001-01-01T00:00:00Z",
+ "2002-01-01T00:00:00Z",
+ "2003-01-01T00:00:00Z",
+ } {
+ tm, err := time.Parse(time.RFC3339, ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ times = append(times, tm)
+ }
+
+ tests := []struct {
+ name string
+ subject string
+ ids []certstore.Identity
+ now time.Time
+ // wantIndex is an index into ids, or -1 for nil.
+ wantIndex int
+ }{
+ {
+ name: "single unexpired identity",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[2]),
+ },
+ },
+ now: times[1],
+ wantIndex: 0,
+ },
+ {
+ name: "single expired identity",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[1]),
+ },
+ },
+ now: times[2],
+ wantIndex: -1,
+ },
+ {
+ name: "unrelated ids",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain("something", times[0], times[2]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[2]),
+ },
+ &testIdentity{
+ chain: makeChain("else", times[0], times[2]),
+ },
+ },
+ now: times[1],
+ wantIndex: 1,
+ },
+ {
+ name: "expired with unrelated ids",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain("something", times[0], times[3]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[1]),
+ },
+ &testIdentity{
+ chain: makeChain("else", times[0], times[3]),
+ },
+ },
+ now: times[2],
+ wantIndex: -1,
+ },
+ {
+ name: "one expired",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[1]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[1], times[3]),
+ },
+ },
+ now: times[2],
+ wantIndex: 1,
+ },
+ {
+ name: "two certs both unexpired",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[3]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[1], times[3]),
+ },
+ },
+ now: times[2],
+ wantIndex: 1,
+ },
+ {
+ name: "two unexpired one expired",
+ subject: testRootSubject,
+ ids: []certstore.Identity{
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[3]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[1], times[3]),
+ },
+ &testIdentity{
+ chain: makeChain(testRootCommonName, times[0], times[1]),
+ },
+ },
+ now: times[2],
+ wantIndex: 1,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotId, gotChain := selectIdentityFromSlice(tt.subject, tt.ids, tt.now)
+
+ if gotId == nil && gotChain != nil {
+ t.Error("id is nil: got non-nil chain, want nil chain")
+ return
+ }
+ if gotId != nil && gotChain == nil {
+ t.Error("id is not nil: got nil chain, want non-nil chain")
+ return
+ }
+ if tt.wantIndex == -1 {
+ if gotId != nil {
+ t.Error("got non-nil id, want nil id")
+ }
+ return
+ }
+ if gotId == nil {
+ t.Error("got nil id, want non-nil id")
+ return
+ }
+ if gotId != tt.ids[tt.wantIndex] {
+ found := -1
+ for i := range tt.ids {
+ if tt.ids[i] == gotId {
+ found = i
+ break
+ }
+ }
+ if found == -1 {
+ t.Errorf("got unknown id, want id at index %v", tt.wantIndex)
+ } else {
+ t.Errorf("got id at index %v, want id at index %v", found, tt.wantIndex)
+ }
+ }
+
+ tid, ok := tt.ids[tt.wantIndex].(*testIdentity)
+ if !ok {
+ t.Error("got non-testIdentity, want testIdentity")
+ return
+ }
+
+ if !reflect.DeepEqual(tid.chain, gotChain) {
+ t.Errorf("got unknown chain, want chain from id at index %v", tt.wantIndex)
+ }
+ })
+ }
+}
diff --git a/control/controlclient/sign_unsupported.go b/control/controlclient/sign_unsupported.go
index 5e161dcbc..4ec40d502 100644
--- a/control/controlclient/sign_unsupported.go
+++ b/control/controlclient/sign_unsupported.go
@@ -1,16 +1,16 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-//go:build !windows
-
-package controlclient
-
-import (
- "tailscale.com/tailcfg"
- "tailscale.com/types/key"
-)
-
-// signRegisterRequest on non-supported platforms always returns errNoCertStore.
-func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
- return errNoCertStore
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !windows
+
+package controlclient
+
+import (
+ "tailscale.com/tailcfg"
+ "tailscale.com/types/key"
+)
+
+// signRegisterRequest on non-supported platforms always returns errNoCertStore.
+func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) error {
+ return errNoCertStore
+}
diff --git a/control/controlclient/status.go b/control/controlclient/status.go
index d0fdf80d7..7dba14d3f 100644
--- a/control/controlclient/status.go
+++ b/control/controlclient/status.go
@@ -1,125 +1,125 @@
-// Copyright (c) Tailscale Inc & AUTHORS
-// SPDX-License-Identifier: BSD-3-Clause
-
-package controlclient
-
-import (
- "encoding/json"
- "fmt"
- "reflect"
-
- "tailscale.com/types/netmap"
- "tailscale.com/types/persist"
- "tailscale.com/types/structs"
-)
-
-// State is the high-level state of the client. It is used only in
-// unit tests for proper sequencing, don't depend on it anywhere else.
-//
-// TODO(apenwarr): eliminate the state, as it's now obsolete.
-//
-// apenwarr: Historical note: controlclient.Auto was originally
-// intended to be the state machine for the whole tailscale client, but that
-// turned out to not be the right abstraction layer, and it moved to
-// ipn.Backend. Since ipn.Backend now has a state machine, it would be
-// much better if controlclient could be a simple stateless API. But the
-// current server-side API (two interlocking polling https calls) makes that
-// very hard to implement. A server side API change could untangle this and
-// remove all the statefulness.
-type State int
-
-const (
- StateNew = State(iota)
- StateNotAuthenticated
- StateAuthenticating
- StateURLVisitRequired
- StateAuthenticated
- StateSynchronized // connected and received map update
-)
-
-func (s State) AppendText(b []byte) ([]byte, error) {
- return append(b, s.String()...), nil
-}
-
-func (s State) MarshalText() ([]byte, error) {
- return []byte(s.String()), nil
-}
-
-func (s State) String() string {
- switch s {
- case StateNew:
- return "state:new"
- case StateNotAuthenticated:
- return "state:not-authenticated"
- case StateAuthenticating:
- return "state:authenticating"
- case StateURLVisitRequired:
- return "state:url-visit-required"
- case StateAuthenticated:
- return "state:authenticated"
- case StateSynchronized:
- return "state:synchronized"
- default:
- return fmt.Sprintf("state:unknown:%d", int(s))
- }
-}
-
-type Status struct {
- _ structs.Incomparable
-
- // Err, if non-nil, is an error that occurred while logging in.
- //
- // If it's of type UserVisibleError then it's meant to be shown to users in
- // their Tailscale client. Otherwise it's just logged to tailscaled's logs.
- Err error
-
- // URL, if non-empty, is the interactive URL to visit to finish logging in.
- URL string
-
- // NetMap is the latest server-pushed state of the tailnet network.
- NetMap *netmap.NetworkMap
-
- // Persist, when Valid, is the locally persisted configuration.
- //
- // TODO(bradfitz,maisem): clarify this.
- Persist persist.PersistView
-
- // state is the internal state. It should not be exposed outside this
- // package, but we have some automated tests elsewhere that need to
- // use it via the StateForTest accessor.
- // TODO(apenwarr): Unexport or remove these.
- state State
-}
-
-// LoginFinished reports whether the controlclient is in its "StateAuthenticated"
-// state where it's in a happy register state but not yet in a map poll.
-//
-// TODO(bradfitz): delete this and everything around Status.state.
-func (s *Status) LoginFinished() bool { return s.state == StateAuthenticated }
-
-// StateForTest returns the internal state of s for tests only.
-func (s *Status) StateForTest() State { return s.state }
-
-// SetStateForTest sets the internal state of s for tests only.
-func (s *Status) SetStateForTest(state State) { s.state = state }
-
-// Equal reports whether s and s2 are equal.
-func (s *Status) Equal(s2 *Status) bool {
- if s == nil && s2 == nil {
- return true
- }
- return s != nil && s2 != nil &&
- s.Err == s2.Err &&
- s.URL == s2.URL &&
- s.state == s2.state &&
- reflect.DeepEqual(s.Persist, s2.Persist) &&
- reflect.DeepEqual(s.NetMap, s2.NetMap)
-}
-
-func (s Status) String() string {
- b, err := json.MarshalIndent(s, "", "\t")
- if err != nil {
- panic(err)
- }
- return s.state.String() + " " + string(b)
-}
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package controlclient
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+
+ "tailscale.com/types/netmap"
+ "tailscale.com/types/persist"
+ "tailscale.com/types/structs"
+)
+
+// State is the high-level state of the client. It is used only in
+// unit tests for proper sequencing, don't depend on it anywhere else.
+//
+// TODO(apenwarr): eliminate the state, as it's now obsolete.
+//
+// apenwarr: Historical note: controlclient.Auto was originally
+// intended to be the state machine for the whole tailscale client, but that
+// turned out to not be the right abstraction layer, and it moved to
+// ipn.Backend. Since ipn.Backend now has a state machine, it would be
+// much better if controlclient could be a simple stateless API. But the
+// current server-side API (two interlocking polling https calls) makes that
+// very hard to implement. A server side API change could untangle this and
+// remove all the statefulness.
+type State int
+
+const (
+ StateNew = State(iota)
+ StateNotAuthenticated
+ StateAuthenticating
+ StateURLVisitRequired
+ StateAuthenticated
+ StateSynchronized // connected and received map update
+)
+
+func (s State) AppendText(b []byte) ([]byte, error) {
+ return append(b, s.String()...), nil
+}
+
+func (s State) MarshalText() ([]byte, error) {
+ return []byte(s.String()), nil
+}
+
+func (s State) String() string {
+ switch s {
+ case StateNew:
+ return "state:new"
+ case StateNotAuthenticated:
+ return "state:not-authenticated"
+ case StateAuthenticating:
+ return "state:authenticating"
+ case StateURLVisitRequired:
+ return "state:url-visit-required"
+ case StateAuthenticated:
+ return "state:authenticated"
+ case StateSynchronized:
+ return "state:synchronized"
+ default:
+ return fmt.Sprintf("state:unknown:%d", int(s))
+ }
+}
+
+type Status struct {
+ _ structs.Incomparable
+
+ // Err, if non-nil, is an error that occurred while logging in.
+ //
+ // If it's of type UserVisibleError then it's meant to be shown to users in
+ // their Tailscale client. Otherwise it's just logged to tailscaled's logs.
+ Err error
+
+ // URL, if non-empty, is the interactive URL to visit to finish logging in.
+ URL string
+
+ // NetMap is the latest server-pushed state of the tailnet network.
+ NetMap *netmap.NetworkMap
+
+ // Persist, when Valid, is the locally persisted configuration.
+ //
+ // TODO(bradfitz,maisem): clarify this.
+ Persist persist.PersistView
+
+ // state is the internal state. It should not be exposed outside this
+ // package, but we have some automated tests elsewhere that need to
+ // use it via the StateForTest accessor.
+ // TODO(apenwarr): Unexport or remove these.
+ state State
+}
+
+// LoginFinished reports whether the controlclient is in its "StateAuthenticated"
+// state where it's in a happy register state but not yet in a map poll.
+//
+// TODO(bradfitz): delete this and everything around Status.state.
+func (s *Status) LoginFinished() bool { return s.state == StateAuthenticated }
+
+// StateForTest returns the internal state of s for tests only.
+func (s *Status) StateForTest() State { return s.state }
+
+// SetStateForTest sets the internal state of s for tests only.
+func (s *Status) SetStateForTest(state State) { s.state = state }
+
+// Equal reports whether s and s2 are equal.
+func (s *Status) Equal(s2 *Status) bool {
+ if s == nil && s2 == nil {
+ return true
+ }
+ return s != nil && s2 != nil &&
+ s.Err == s2.Err &&
+ s.URL == s2.URL &&
+ s.state == s2.state &&
+ reflect.DeepEqual(s.Persist, s2.Persist) &&
+ reflect.DeepEqual(s.NetMap, s2.NetMap)
+}
+
+func (s Status) String() string {
+ b, err := json.MarshalIndent(s, "", "\t")
+ if err != nil {
+ panic(err)
+ }
+ return s.state.String() + " " + string(b)
+}