diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 07319e277..703c2b9c2 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -778,13 +778,16 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network } // NetworkLockInit initializes the tailnet key authority. -func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ipnstate.NetworkLockStatus, error) { +// +// TODO(tom): Plumb through disablement secrets. +func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte) (*ipnstate.NetworkLockStatus, error) { var b bytes.Buffer type initRequest struct { - Keys []tka.Key + Keys []tka.Key + DisablementValues [][]byte } - if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys}); err != nil { + if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues}); err != nil { return nil, err } diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index 2c143c2d5..224d67b88 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -5,6 +5,7 @@ package cli import ( + "bytes" "context" "errors" "fmt" @@ -51,7 +52,10 @@ func runNetworkLockInit(ctx context.Context, args []string) error { return err } - status, err := localClient.NetworkLockInit(ctx, keys) + // TODO(tom): Implement specification of disablement values from the command line. + disablementValues := [][]byte{bytes.Repeat([]byte{0xa5}, 32)} + + status, err := localClient.NetworkLockInit(ctx, keys, disablementValues) if err != nil { return err } diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 7ea520b94..0c3c7210c 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -561,6 +561,11 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) { c.sendNewMapRequest() } +// SetTKAHead updates the TKA head hash that map-request infrastructure sends. +func (c *Auto) SetTKAHead(headHash string) { + c.direct.SetTKAHead(headHash) +} + func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) { c.mu.Lock() if c.closed { diff --git a/control/controlclient/client.go b/control/controlclient/client.go index 75cf5dc61..b8e1bde04 100644 --- a/control/controlclient/client.go +++ b/control/controlclient/client.go @@ -65,6 +65,9 @@ type Client interface { // in a separate http request. It has nothing to do with the rest of // the state machine. SetNetInfo(*tailcfg.NetInfo) + // SetTKAHead changes the TKA head hash value that will be sent in + // subsequent netmap requests. + SetTKAHead(headHash string) // UpdateEndpoints changes the Endpoint structure that will be sent // in subsequent node registration requests. // TODO: a server-side change would let us simply upload this diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index b10131475..7c9c48271 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -94,6 +94,7 @@ type Direct struct { hostinfo *tailcfg.Hostinfo // always non-nil netinfo *tailcfg.NetInfo endpoints []tailcfg.Endpoint + tkaHead string everEndpoints bool // whether we've ever had non-empty endpoints lastPingURL string // last PingRequest.URL received, for dup suppression } @@ -317,6 +318,21 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool { return true } +// SetNetInfo stores a new TKA head value for next update. +// It reports whether the TKA head changed. +func (c *Direct) SetTKAHead(tkaHead string) bool { + c.mu.Lock() + defer c.mu.Unlock() + + if tkaHead == c.tkaHead { + return false + } + + c.tkaHead = tkaHead + c.logf("tkaHead: %v", tkaHead) + return true +} + func (c *Direct) GetPersist() persist.Persist { c.mu.Lock() defer c.mu.Unlock() @@ -829,6 +845,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool Hostinfo: hi, DebugFlags: c.debugFlags, OmitPeers: cb == nil, + TKAHead: c.tkaHead, // On initial startup before we know our endpoints, set the ReadOnly flag // to tell the control server not to distribute out our (empty) endpoints to peers. diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3e2ec8c3a..e18c496cb 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -831,6 +831,16 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) { b.logf("[v1] TKA sync error: %v", err) } b.mu.Lock() + if b.tka != nil { + head, err := b.tka.authority.Head().MarshalText() + if err != nil { + b.logf("[v1] error marshalling tka head: %v", err) + } else { + b.cc.SetTKAHead(string(head)) + } + } else { + b.cc.SetTKAHead("") + } if !envknob.TKASkipSignatureCheck() { b.tkaFilterNetmapLocked(st.NetMap) @@ -1226,11 +1236,21 @@ func (b *LocalBackend) Start(opts ipn.Options) error { b.cc = cc b.ccAuto, _ = cc.(*controlclient.Auto) endpoints := b.endpoints + var tkaHead string + if b.tka != nil { + head, err := b.tka.authority.Head().MarshalText() + if err != nil { + b.mu.Unlock() + return fmt.Errorf("marshalling tka head: %w", err) + } + tkaHead = string(head) + } b.mu.Unlock() if endpoints != nil { cc.UpdateEndpoints(endpoints) } + cc.SetTKAHead(tkaHead) b.e.SetNetInfoCallback(b.setNetInfo) diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index bf2b9221a..f5854d18f 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -95,6 +95,8 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error { return nil } + b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead) + b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section. defer b.tkaSyncLock.Unlock() b.mu.Lock() // take mu to protect access to synchronized fields. @@ -125,15 +127,13 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error { } isEnabled = true } else if !wantEnabled && isEnabled { - if b.tka.authority.ValidDisablement(bs.DisablementSecret) { - b.tka = nil - isEnabled = false - - if err := os.RemoveAll(b.chonkPath()); err != nil { - return fmt.Errorf("os.RemoveAll: %v", err) - } + if err := b.tkaApplyDisablementLocked(bs.DisablementSecret); err != nil { + // We log here instead of returning an error (which itself would be + // logged), so that sync will continue even if control gives us an + // incorrect disablement secret. + b.logf("Disablement failed, leaving TKA enabled. Error: %v", err) } else { - b.logf("Disablement secret did not verify, leaving TKA enabled.") + isEnabled = false } } else { return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled") @@ -216,12 +216,11 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error { } } - // NOTE(tom): We could short-circuit here if our HEAD equals the - // control-plane's head, but we don't just so control always has a - // copy of all forks that clients had. - + // NOTE(tom): We always send this RPC so control knows what TKA + // head we landed at. + head := b.tka.authority.Head() b.mu.Unlock() - sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false) + sendResp, err := b.tkaDoSyncSend(ourNodeKey, head, toSendAUMs, false) b.mu.Lock() if err != nil { return fmt.Errorf("send RPC: %v", err) @@ -238,6 +237,21 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error { return nil } +// tkaApplyDisablementLocked checks a disablement secret and locally disables +// TKA (if correct). An error is returned if disablement failed. +// +// b.mu must be held & TKA must be initialized. +func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error { + if b.tka.authority.ValidDisablement(secret) { + if err := os.RemoveAll(b.chonkPath()); err != nil { + return err + } + b.tka = nil + return nil + } + return errors.New("incorrect disablement secret") +} + // chonkPath returns the absolute path to the directory in which TKA // state (the 'tailchonk') is stored. func (b *LocalBackend) chonkPath() string { @@ -334,7 +348,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { // needing signatures is returned as a response. // The Finish RPC submits signatures for all these nodes, at which point // Control has everything it needs to atomically enable network lock. -func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error { +func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte) error { if err := b.CanSupportNetworkLock(); err != nil { return err } @@ -355,8 +369,11 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error { // just in case something goes wrong. _, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{ Keys: keys, - // TODO(tom): Actually plumb a real disablement value. - DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)}, + // TODO(tom): s/tka.State.DisablementSecrets/tka.State.DisablementValues + // This will center on consistent nomenclature: + // - DisablementSecret: value needed to disable. + // - DisablementValue: the KDF of the disablement secret, a public value. + DisablementSecrets: disablementValues, }, b.nlPrivKey) if err != nil { return fmt.Errorf("tka.Create: %v", err) @@ -454,8 +471,9 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err } ourNodeKey := b.prefs.Persist().PublicNodeKey() + head := b.tka.authority.Head() b.mu.Unlock() - resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true) + resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true) b.mu.Lock() if err != nil { return err @@ -474,6 +492,42 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err return nil } +// NetworkLockDisable disables network-lock using the provided disablement secret. +func (b *LocalBackend) NetworkLockDisable(secret []byte) error { + if err := b.CanSupportNetworkLock(); err != nil { + return err + } + + var ( + ourNodeKey key.NodePublic + head tka.AUMHash + err error + ) + + b.mu.Lock() + if b.prefs.Valid() { + ourNodeKey = b.prefs.Persist().PublicNodeKey() + } + if b.tka == nil { + err = errNetworkLockNotActive + } else { + head = b.tka.authority.Head() + if !b.tka.authority.ValidDisablement(secret) { + err = errors.New("incorrect disablement secret") + } + } + b.mu.Unlock() + if err != nil { + return err + } + + if ourNodeKey.IsZero() { + return errors.New("no node-key: is tailscale logged in?") + } + _, err = b.tkaDoDisablement(ourNodeKey, head, secret) + return err +} + func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) { p, err := nodeInfo.NodePublic.MarshalBinary() if err != nil { @@ -519,7 +573,7 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) } a := new(tailcfg.TKAInitBeginResponse) - err = json.NewDecoder(res.Body).Decode(a) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a) res.Body.Close() if err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) @@ -555,7 +609,7 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg. return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) } a := new(tailcfg.TKAInitFinishResponse) - err = json.NewDecoder(res.Body).Decode(a) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a) res.Body.Close() if err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) @@ -603,7 +657,7 @@ func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUM return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) } a := new(tailcfg.TKABootstrapResponse) - err = json.NewDecoder(res.Body).Decode(a) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a) res.Body.Close() if err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) @@ -664,7 +718,7 @@ func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncO return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) } a := new(tailcfg.TKASyncOfferResponse) - err = json.NewDecoder(res.Body).Decode(a) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a) res.Body.Close() if err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) @@ -675,10 +729,16 @@ func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncO // tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane // over noise. This is the second of two RPCs implementing tka synchronization. -func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) { +func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, head tka.AUMHash, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) { + headBytes, err := head.MarshalText() + if err != nil { + return nil, fmt.Errorf("head.MarshalText: %w", err) + } + sendReq := tailcfg.TKASyncSendRequest{ Version: tailcfg.CurrentCapabilityVersion, NodeKey: ourNodeKey, + Head: string(headBytes), MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)), Interactive: interactive, } @@ -707,7 +767,49 @@ func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) } a := new(tailcfg.TKASyncSendResponse) - err = json.NewDecoder(res.Body).Decode(a) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a) + res.Body.Close() + if err != nil { + return nil, fmt.Errorf("decoding JSON: %w", err) + } + + return a, nil +} + +func (b *LocalBackend) tkaDoDisablement(ourNodeKey key.NodePublic, head tka.AUMHash, secret []byte) (*tailcfg.TKADisableResponse, error) { + headBytes, err := head.MarshalText() + if err != nil { + return nil, fmt.Errorf("head.MarshalText: %w", err) + } + + var req bytes.Buffer + if err := json.NewEncoder(&req).Encode(tailcfg.TKADisableRequest{ + Version: tailcfg.CurrentCapabilityVersion, + NodeKey: ourNodeKey, + Head: string(headBytes), + DisablementSecret: secret, + }); err != nil { + return nil, fmt.Errorf("encoding request: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/disable", &req) + if err != nil { + return nil, fmt.Errorf("req: %w", err) + } + res, err := b.DoNoiseRequest(req2) + if err != nil { + return nil, fmt.Errorf("resp: %w", err) + } + if res.StatusCode != 200 { + body, _ := io.ReadAll(res.Body) + res.Body.Close() + return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) + } + a := new(tailcfg.TKADisableResponse) + err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a) res.Body.Close() if err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go index eac318b74..0efe3a312 100644 --- a/ipn/ipnlocal/network-lock_test.go +++ b/ipn/ipnlocal/network-lock_test.go @@ -261,6 +261,8 @@ func TestTKASync(t *testing.T) { someKeyPriv := key.NewNLPrivate() someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1} + disablementSecret := bytes.Repeat([]byte{0xa5}, 32) + type tkaSyncScenario struct { name string // controlAUMs is called (if non-nil) to get any AUMs which the tka state @@ -342,7 +344,7 @@ func TestTKASync(t *testing.T) { controlStorage := &tka.Mem{} controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{ Keys: []tka.Key{key, someKey}, - DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)}, + DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)}, }, nlPriv) if err != nil { t.Fatalf("tka.Create() failed: %v", err) @@ -416,6 +418,11 @@ func TestTKASync(t *testing.T) { t.Fatal(err) } t.Logf("got sync send:\n%+v", body) + + var remoteHead tka.AUMHash + if err := remoteHead.UnmarshalText([]byte(body.Head)); err != nil { + t.Fatalf("head unmarshal: %v", err) + } toApply := make([]tka.AUM, len(body.MissingAUMs)) for i, a := range body.MissingAUMs { if err := toApply[i].Unserialize(a); err != nil { @@ -434,7 +441,9 @@ func TestTKASync(t *testing.T) { } w.WriteHeader(200) - if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{Head: string(head)}); err != nil { + if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{ + Head: string(head), + }); err != nil { t.Fatal(err) } @@ -536,3 +545,87 @@ func TestTKAFilterNetmap(t *testing.T) { t.Errorf("filtered netmap differs (-want, +got):\n%s", diff) } } + +func TestTKADisable(t *testing.T) { + envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1") + temp := t.TempDir() + os.Mkdir(filepath.Join(temp, "tka"), 0755) + nodePriv := key.NewNode() + + // Make a fake TKA authority, to seed local state. + disablementSecret := bytes.Repeat([]byte{0xa5}, 32) + nlPriv := key.NewNLPrivate() + key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2} + chonk, err := tka.ChonkDir(filepath.Join(temp, "tka")) + if err != nil { + t.Fatal(err) + } + authority, _, err := tka.Create(chonk, tka.State{ + Keys: []tka.Key{key}, + DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)}, + }, nlPriv) + if err != nil { + t.Fatalf("tka.Create() failed: %v", err) + } + + ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + switch r.URL.Path { + case "/machine/tka/disable": + body := new(tailcfg.TKADisableRequest) + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + t.Fatal(err) + } + if body.Version != tailcfg.CurrentCapabilityVersion { + t.Errorf("disable CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion) + } + if body.NodeKey != nodePriv.Public() { + t.Errorf("nodeKey = %v, want %v", body.NodeKey, nodePriv.Public()) + } + if !bytes.Equal(body.DisablementSecret, disablementSecret) { + t.Errorf("disablement secret = %x, want %x", body.DisablementSecret, disablementSecret) + } + + var head tka.AUMHash + if err := head.UnmarshalText([]byte(body.Head)); err != nil { + t.Fatalf("failed unmarshal of body.Head: %v", err) + } + if head != authority.Head() { + t.Errorf("reported head = %x, want %x", head, authority.Head()) + } + + w.WriteHeader(200) + if err := json.NewEncoder(w).Encode(tailcfg.TKADisableResponse{}); err != nil { + t.Fatal(err) + } + + default: + t.Errorf("unhandled endpoint path: %v", r.URL.Path) + w.WriteHeader(404) + } + })) + defer ts.Close() + + cc := fakeControlClient(t, client) + b := LocalBackend{ + varRoot: temp, + cc: cc, + ccAuto: cc, + logf: t.Logf, + tka: &tkaState{ + authority: authority, + storage: chonk, + }, + prefs: (&ipn.Prefs{ + Persist: &persist.Persist{PrivateNodeKey: nodePriv}, + }).View(), + } + + // Test that we get an error for an incorrect disablement secret. + if err := b.NetworkLockDisable([]byte{1, 2, 3, 4}); err == nil || err.Error() != "incorrect disablement secret" { + t.Errorf("NetworkLockDisable().err = %v, want 'incorrect disablement secret'", err) + } + if err := b.NetworkLockDisable(disablementSecret); err != nil { + t.Errorf("NetworkLockDisable() failed: %v", err) + } +} diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 983a61bb4..01db3243b 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -248,6 +248,10 @@ func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) { cc.called("SetNetInfo") } +func (cc *mockControl) SetTKAHead(head string) { + cc.logf("SetTKAHead: %s", head) +} + func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) { // validate endpoint information here? cc.logf("UpdateEndpoints: ep=%v", endpoints) diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 114eb1956..57ca7048c 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -932,7 +932,8 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) { } type initRequest struct { - Keys []tka.Key + Keys []tka.Key + DisablementValues [][]byte } var req initRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -940,7 +941,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) { return } - if err := h.b.NetworkLockInit(req.Keys); err != nil { + if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues); err != nil { http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError) return } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 8baa30a76..187c61a34 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -947,6 +947,11 @@ type MapRequest struct { // EndpointTypes are the types of the corresponding endpoints in Endpoints. EndpointTypes []EndpointType `json:",omitempty"` + // TKAHead describes the hash of the latest AUM applied to the local + // tailnet key authority, if one is operating. + // It is encoded as tka.AUMHash.MarshalText. + TKAHead string `json:",omitempty"` + // ReadOnly is whether the client just wants to fetch the // MapResponse, without updating their Endpoints. The // Endpoints field will be ignored and LastSeen will not be diff --git a/tailcfg/tka.go b/tailcfg/tka.go index 091113599..c24020a29 100644 --- a/tailcfg/tka.go +++ b/tailcfg/tka.go @@ -86,9 +86,6 @@ type TKAInfo struct { // // If the Head state differs to that known locally, the node should perform // synchronization via a separate RPC. - // - // TODO(tom): Implement AUM synchronization as noise endpoints - // /machine/tka/sync/offer & /machine/tka/sync/send. Head string `json:",omitempty"` // Disabled indicates the control plane believes TKA should be disabled, @@ -97,9 +94,6 @@ type TKAInfo struct { // disable TKA locally. // This field exists to disambiguate a nil TKAInfo in a delta mapresponse // from a nil TKAInfo indicating TKA should be disabled. - // - // TODO(tom): Implement /machine/tka/bootstrap as a noise endpoint, to - // communicate the genesis AUM & any disablement secrets. Disabled bool `json:",omitempty"` } @@ -162,7 +156,8 @@ type TKASyncOfferResponse struct { } // TKASyncSendRequest encodes AUMs that a node believes the control plane -// is missing. +// is missing, and notifies control of its local TKA state (specifically +// the head hash). type TKASyncSendRequest struct { // Version is the client's capabilities. Version CapabilityVersion @@ -170,9 +165,15 @@ type TKASyncSendRequest struct { // NodeKey is the client's current node key. NodeKey key.NodePublic + // Head represents the node's head AUMHash (tka.Authority.Head) after + // applying any AUMs from the sync-offer response. + // It is encoded as tka.AUMHash.MarshalText. + Head string + // MissingAUMs encodes AUMs that the node believes the control plane // is missing. MissingAUMs []tkatype.MarshaledAUM + // Interactive is true if additional error checking should be performed as // the request is on behalf of an interactive operation (e.g., an // administrator publishing new changes) as opposed to an automatic @@ -187,3 +188,29 @@ type TKASyncSendResponse struct { // after applying the missing AUMs. Head string } + +// TKADisableRequest disables network-lock across the tailnet using the +// provided disablement secret. +// +// This is the request schema for a /tka/disable noise RPC. +type TKADisableRequest struct { + // Version is the client's capabilities. + Version CapabilityVersion + + // NodeKey is the client's current node key. + NodeKey key.NodePublic + + // Head represents the node's head AUMHash (tka.Authority.Head). + // It is encoded as tka.AUMHash.MarshalText. + Head string + + // DisablementSecret encodes the secret necessary to disable TKA. + DisablementSecret []byte +} + +// TKADisableResponse is the JSON response from a /tka/disable RPC. +// This schema describes the successful disablement of the tailnet's +// key authority. +type TKADisableResponse struct { + // Nothing. (yet?) +}