mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +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.
|
// 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
|
var b bytes.Buffer
|
||||||
type initRequest struct {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -51,7 +52,10 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -561,6 +561,11 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
|||||||
c.sendNewMapRequest()
|
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) {
|
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.closed {
|
if c.closed {
|
||||||
|
@ -65,6 +65,9 @@ type Client interface {
|
|||||||
// in a separate http request. It has nothing to do with the rest of
|
// in a separate http request. It has nothing to do with the rest of
|
||||||
// the state machine.
|
// the state machine.
|
||||||
SetNetInfo(*tailcfg.NetInfo)
|
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
|
// UpdateEndpoints changes the Endpoint structure that will be sent
|
||||||
// in subsequent node registration requests.
|
// in subsequent node registration requests.
|
||||||
// TODO: a server-side change would let us simply upload this
|
// TODO: a server-side change would let us simply upload this
|
||||||
|
@ -94,6 +94,7 @@ type Direct struct {
|
|||||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||||
netinfo *tailcfg.NetInfo
|
netinfo *tailcfg.NetInfo
|
||||||
endpoints []tailcfg.Endpoint
|
endpoints []tailcfg.Endpoint
|
||||||
|
tkaHead string
|
||||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||||
}
|
}
|
||||||
@ -317,6 +318,21 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
|
|||||||
return true
|
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 {
|
func (c *Direct) GetPersist() persist.Persist {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@ -829,6 +845,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
|||||||
Hostinfo: hi,
|
Hostinfo: hi,
|
||||||
DebugFlags: c.debugFlags,
|
DebugFlags: c.debugFlags,
|
||||||
OmitPeers: cb == nil,
|
OmitPeers: cb == nil,
|
||||||
|
TKAHead: c.tkaHead,
|
||||||
|
|
||||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
// 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.
|
// 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.logf("[v1] TKA sync error: %v", err)
|
||||||
}
|
}
|
||||||
b.mu.Lock()
|
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() {
|
if !envknob.TKASkipSignatureCheck() {
|
||||||
b.tkaFilterNetmapLocked(st.NetMap)
|
b.tkaFilterNetmapLocked(st.NetMap)
|
||||||
@ -1226,11 +1236,21 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
b.cc = cc
|
b.cc = cc
|
||||||
b.ccAuto, _ = cc.(*controlclient.Auto)
|
b.ccAuto, _ = cc.(*controlclient.Auto)
|
||||||
endpoints := b.endpoints
|
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()
|
b.mu.Unlock()
|
||||||
|
|
||||||
if endpoints != nil {
|
if endpoints != nil {
|
||||||
cc.UpdateEndpoints(endpoints)
|
cc.UpdateEndpoints(endpoints)
|
||||||
}
|
}
|
||||||
|
cc.SetTKAHead(tkaHead)
|
||||||
|
|
||||||
b.e.SetNetInfoCallback(b.setNetInfo)
|
b.e.SetNetInfoCallback(b.setNetInfo)
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
|
|||||||
return nil
|
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.
|
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
|
||||||
defer b.tkaSyncLock.Unlock()
|
defer b.tkaSyncLock.Unlock()
|
||||||
b.mu.Lock() // take mu to protect access to synchronized fields.
|
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
|
isEnabled = true
|
||||||
} else if !wantEnabled && isEnabled {
|
} else if !wantEnabled && isEnabled {
|
||||||
if b.tka.authority.ValidDisablement(bs.DisablementSecret) {
|
if err := b.tkaApplyDisablementLocked(bs.DisablementSecret); err != nil {
|
||||||
b.tka = nil
|
// We log here instead of returning an error (which itself would be
|
||||||
isEnabled = false
|
// logged), so that sync will continue even if control gives us an
|
||||||
|
// incorrect disablement secret.
|
||||||
if err := os.RemoveAll(b.chonkPath()); err != nil {
|
b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
|
||||||
return fmt.Errorf("os.RemoveAll: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
b.logf("Disablement secret did not verify, leaving TKA enabled.")
|
isEnabled = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
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
|
// NOTE(tom): We always send this RPC so control knows what TKA
|
||||||
// control-plane's head, but we don't just so control always has a
|
// head we landed at.
|
||||||
// copy of all forks that clients had.
|
head := b.tka.authority.Head()
|
||||||
|
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false)
|
sendResp, err := b.tkaDoSyncSend(ourNodeKey, head, toSendAUMs, false)
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("send RPC: %v", err)
|
return fmt.Errorf("send RPC: %v", err)
|
||||||
@ -238,6 +237,21 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
|
|||||||
return nil
|
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
|
// chonkPath returns the absolute path to the directory in which TKA
|
||||||
// state (the 'tailchonk') is stored.
|
// state (the 'tailchonk') is stored.
|
||||||
func (b *LocalBackend) chonkPath() string {
|
func (b *LocalBackend) chonkPath() string {
|
||||||
@ -334,7 +348,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
|||||||
// needing signatures is returned as a response.
|
// needing signatures is returned as a response.
|
||||||
// The Finish RPC submits signatures for all these nodes, at which point
|
// The Finish RPC submits signatures for all these nodes, at which point
|
||||||
// Control has everything it needs to atomically enable network lock.
|
// 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 {
|
if err := b.CanSupportNetworkLock(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -355,8 +369,11 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
|||||||
// just in case something goes wrong.
|
// just in case something goes wrong.
|
||||||
_, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
_, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
||||||
Keys: keys,
|
Keys: keys,
|
||||||
// TODO(tom): Actually plumb a real disablement value.
|
// TODO(tom): s/tka.State.DisablementSecrets/tka.State.DisablementValues
|
||||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)},
|
// 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)
|
}, b.nlPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("tka.Create: %v", err)
|
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()
|
ourNodeKey := b.prefs.Persist().PublicNodeKey()
|
||||||
|
head := b.tka.authority.Head()
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true)
|
resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true)
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -474,6 +492,42 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
|||||||
return nil
|
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) {
|
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||||
if err != nil {
|
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))
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
a := new(tailcfg.TKAInitBeginResponse)
|
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()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
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))
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
a := new(tailcfg.TKAInitFinishResponse)
|
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()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
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))
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
a := new(tailcfg.TKABootstrapResponse)
|
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()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
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))
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
a := new(tailcfg.TKASyncOfferResponse)
|
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()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
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
|
// tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
|
||||||
// over noise. This is the second of two RPCs implementing tka synchronization.
|
// 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{
|
sendReq := tailcfg.TKASyncSendRequest{
|
||||||
Version: tailcfg.CurrentCapabilityVersion,
|
Version: tailcfg.CurrentCapabilityVersion,
|
||||||
NodeKey: ourNodeKey,
|
NodeKey: ourNodeKey,
|
||||||
|
Head: string(headBytes),
|
||||||
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
|
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
|
||||||
Interactive: interactive,
|
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))
|
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
a := new(tailcfg.TKASyncSendResponse)
|
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()
|
res.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||||
|
@ -261,6 +261,8 @@ func TestTKASync(t *testing.T) {
|
|||||||
someKeyPriv := key.NewNLPrivate()
|
someKeyPriv := key.NewNLPrivate()
|
||||||
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
|
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
|
||||||
|
|
||||||
|
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||||
|
|
||||||
type tkaSyncScenario struct {
|
type tkaSyncScenario struct {
|
||||||
name string
|
name string
|
||||||
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
|
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
|
||||||
@ -342,7 +344,7 @@ type tkaSyncScenario struct {
|
|||||||
controlStorage := &tka.Mem{}
|
controlStorage := &tka.Mem{}
|
||||||
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
|
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
|
||||||
Keys: []tka.Key{key, someKey},
|
Keys: []tka.Key{key, someKey},
|
||||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||||
}, nlPriv)
|
}, nlPriv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("tka.Create() failed: %v", err)
|
t.Fatalf("tka.Create() failed: %v", err)
|
||||||
@ -416,6 +418,11 @@ type tkaSyncScenario struct {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Logf("got sync send:\n%+v", body)
|
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))
|
toApply := make([]tka.AUM, len(body.MissingAUMs))
|
||||||
for i, a := range body.MissingAUMs {
|
for i, a := range body.MissingAUMs {
|
||||||
if err := toApply[i].Unserialize(a); err != nil {
|
if err := toApply[i].Unserialize(a); err != nil {
|
||||||
@ -434,7 +441,9 @@ type tkaSyncScenario struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(200)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,3 +545,87 @@ func TestTKAFilterNetmap(t *testing.T) {
|
|||||||
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
|
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")
|
cc.called("SetNetInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cc *mockControl) SetTKAHead(head string) {
|
||||||
|
cc.logf("SetTKAHead: %s", head)
|
||||||
|
}
|
||||||
|
|
||||||
func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
||||||
// validate endpoint information here?
|
// validate endpoint information here?
|
||||||
cc.logf("UpdateEndpoints: ep=%v", endpoints)
|
cc.logf("UpdateEndpoints: ep=%v", endpoints)
|
||||||
|
@ -932,7 +932,8 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type initRequest struct {
|
type initRequest struct {
|
||||||
Keys []tka.Key
|
Keys []tka.Key
|
||||||
|
DisablementValues [][]byte
|
||||||
}
|
}
|
||||||
var req initRequest
|
var req initRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
@ -940,7 +941,7 @@ type initRequest struct {
|
|||||||
return
|
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)
|
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -947,6 +947,11 @@ type MapRequest struct {
|
|||||||
// EndpointTypes are the types of the corresponding endpoints in Endpoints.
|
// EndpointTypes are the types of the corresponding endpoints in Endpoints.
|
||||||
EndpointTypes []EndpointType `json:",omitempty"`
|
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
|
// ReadOnly is whether the client just wants to fetch the
|
||||||
// MapResponse, without updating their Endpoints. The
|
// MapResponse, without updating their Endpoints. The
|
||||||
// Endpoints field will be ignored and LastSeen will not be
|
// 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
|
// If the Head state differs to that known locally, the node should perform
|
||||||
// synchronization via a separate RPC.
|
// 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"`
|
Head string `json:",omitempty"`
|
||||||
|
|
||||||
// Disabled indicates the control plane believes TKA should be disabled,
|
// Disabled indicates the control plane believes TKA should be disabled,
|
||||||
@ -97,9 +94,6 @@ type TKAInfo struct {
|
|||||||
// disable TKA locally.
|
// disable TKA locally.
|
||||||
// This field exists to disambiguate a nil TKAInfo in a delta mapresponse
|
// This field exists to disambiguate a nil TKAInfo in a delta mapresponse
|
||||||
// from a nil TKAInfo indicating TKA should be disabled.
|
// 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"`
|
Disabled bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +156,8 @@ type TKASyncOfferResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TKASyncSendRequest encodes AUMs that a node believes the control plane
|
// 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 {
|
type TKASyncSendRequest struct {
|
||||||
// Version is the client's capabilities.
|
// Version is the client's capabilities.
|
||||||
Version CapabilityVersion
|
Version CapabilityVersion
|
||||||
@ -170,9 +165,15 @@ type TKASyncSendRequest struct {
|
|||||||
// NodeKey is the client's current node key.
|
// NodeKey is the client's current node key.
|
||||||
NodeKey key.NodePublic
|
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
|
// MissingAUMs encodes AUMs that the node believes the control plane
|
||||||
// is missing.
|
// is missing.
|
||||||
MissingAUMs []tkatype.MarshaledAUM
|
MissingAUMs []tkatype.MarshaledAUM
|
||||||
|
|
||||||
// Interactive is true if additional error checking should be performed as
|
// Interactive is true if additional error checking should be performed as
|
||||||
// the request is on behalf of an interactive operation (e.g., an
|
// the request is on behalf of an interactive operation (e.g., an
|
||||||
// administrator publishing new changes) as opposed to an automatic
|
// administrator publishing new changes) as opposed to an automatic
|
||||||
@ -187,3 +188,29 @@ type TKASyncSendResponse struct {
|
|||||||
// after applying the missing AUMs.
|
// after applying the missing AUMs.
|
||||||
Head string
|
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