mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
cmd,ipn/ipnlocal,tailcfg: implement TKA disablement
* Plumb disablement values through some of the internals of TKA enablement. * Transmit the node's TKA hash at the end of sync so the control plane understands each node's head. * Implement /machine/tka/disable RPC to actuate disablement on the control plane. There is a partner PR for the control server I'll send shortly. Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
3d8eda5b72
commit
d98305c537
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 @@ type tkaSyncScenario struct {
|
||||
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 @@ type tkaSyncScenario struct {
|
||||
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 @@ type tkaSyncScenario struct {
|
||||
}
|
||||
|
||||
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(<bad secret>).err = %v, want 'incorrect disablement secret'", err)
|
||||
}
|
||||
if err := b.NetworkLockDisable(disablementSecret); err != nil {
|
||||
t.Errorf("NetworkLockDisable() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 @@ type initRequest struct {
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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?)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user