mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 13:41:46 +02:00
all: add ts_omit_tailnetlock as a start of making it build-time modular
Updates #17115 Change-Id: I6b083c0db4c4d359e49eb129d626b7f128f0a9d2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
0e3d942e39
commit
3a49b7464c
@ -38,10 +38,8 @@ import (
|
|||||||
"tailscale.com/paths"
|
"tailscale.com/paths"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tka"
|
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/tkatype"
|
|
||||||
"tailscale.com/util/eventbus"
|
"tailscale.com/util/eventbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1219,183 +1217,6 @@ func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.Ping
|
|||||||
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
|
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
|
||||||
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
|
||||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockInit initializes the tailnet key authority.
|
|
||||||
//
|
|
||||||
// TODO(tom): Plumb through disablement secrets.
|
|
||||||
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
type initRequest struct {
|
|
||||||
Keys []tka.Key
|
|
||||||
DisablementValues [][]byte
|
|
||||||
SupportDisablement []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
|
||||||
// enable unattended bringup in the locked tailnet.
|
|
||||||
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
|
||||||
encodedPrivate, err := tkaKey.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
type wrapRequest struct {
|
|
||||||
TSKey string
|
|
||||||
TKAKey string // key.NLPrivate.MarshalText
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return string(body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
|
||||||
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
|
||||||
var b bytes.Buffer
|
|
||||||
type modifyRequest struct {
|
|
||||||
AddKeys []tka.Key
|
|
||||||
RemoveKeys []tka.Key
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
|
||||||
return fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
|
||||||
// rotationPublic, if specified, must be an ed25519 public key.
|
|
||||||
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
|
|
||||||
var b bytes.Buffer
|
|
||||||
type signRequest struct {
|
|
||||||
NodeKey key.NodePublic
|
|
||||||
RotationPublic []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
|
|
||||||
return fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
|
||||||
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
|
||||||
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("limit", fmt.Sprint(maxEntries))
|
|
||||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
|
||||||
}
|
|
||||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
|
||||||
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
|
|
||||||
// This endpoint expects an empty JSON stanza as the payload.
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
|
||||||
return fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
|
||||||
// in url and returns information extracted from it.
|
|
||||||
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
|
||||||
vr := struct {
|
|
||||||
URL string
|
|
||||||
}{url}
|
|
||||||
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
|
||||||
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
|
||||||
vr := struct {
|
|
||||||
Keys []tkatype.KeyID
|
|
||||||
ForkFrom string
|
|
||||||
}{removeKeys, forkFrom.String()}
|
|
||||||
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
|
||||||
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
|
||||||
r := bytes.NewReader(aum.Serialize())
|
|
||||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
|
||||||
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
|
||||||
r := bytes.NewReader(aum.Serialize())
|
|
||||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServeConfig sets or replaces the serving settings.
|
// SetServeConfig sets or replaces the serving settings.
|
||||||
// If config is nil, settings are cleared and serving is disabled.
|
// If config is nil, settings are cleared and serving is disabled.
|
||||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||||
@ -1421,14 +1242,6 @@ func (lc *Client) DisconnectControl(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkLockDisable shuts down network-lock across the tailnet.
|
|
||||||
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
|
|
||||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
|
|
||||||
return fmt.Errorf("error: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServeConfig return the current serve config.
|
// GetServeConfig return the current serve config.
|
||||||
//
|
//
|
||||||
// If the serve config is empty, it returns (nil, nil).
|
// If the serve config is empty, it returns (nil, nil).
|
||||||
|
204
client/local/tailnetlock.go
Normal file
204
client/local/tailnetlock.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/tkatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||||
|
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
|
||||||
|
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockInit initializes the tailnet key authority.
|
||||||
|
//
|
||||||
|
// TODO(tom): Plumb through disablement secrets.
|
||||||
|
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
type initRequest struct {
|
||||||
|
Keys []tka.Key
|
||||||
|
DisablementValues [][]byte
|
||||||
|
SupportDisablement []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||||
|
// enable unattended bringup in the locked tailnet.
|
||||||
|
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||||
|
encodedPrivate, err := tkaKey.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
type wrapRequest struct {
|
||||||
|
TSKey string
|
||||||
|
TKAKey string // key.NLPrivate.MarshalText
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||||
|
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||||
|
var b bytes.Buffer
|
||||||
|
type modifyRequest struct {
|
||||||
|
AddKeys []tka.Key
|
||||||
|
RemoveKeys []tka.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
|
||||||
|
return fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
|
||||||
|
// rotationPublic, if specified, must be an ed25519 public key.
|
||||||
|
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
|
||||||
|
var b bytes.Buffer
|
||||||
|
type signRequest struct {
|
||||||
|
NodeKey key.NodePublic
|
||||||
|
RotationPublic []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
|
||||||
|
return fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||||
|
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||||
|
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("limit", fmt.Sprint(maxEntries))
|
||||||
|
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||||
|
}
|
||||||
|
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||||
|
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
|
||||||
|
// This endpoint expects an empty JSON stanza as the payload.
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
|
||||||
|
return fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
||||||
|
// in url and returns information extracted from it.
|
||||||
|
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
|
||||||
|
vr := struct {
|
||||||
|
URL string
|
||||||
|
}{url}
|
||||||
|
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeJSON[*tka.DeeplinkValidationResult](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||||
|
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
|
||||||
|
vr := struct {
|
||||||
|
Keys []tkatype.KeyID
|
||||||
|
ForkFrom string
|
||||||
|
}{removeKeys, forkFrom.String()}
|
||||||
|
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||||
|
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
|
||||||
|
r := bytes.NewReader(aum.Serialize())
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||||
|
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
|
||||||
|
r := bytes.NewReader(aum.Serialize())
|
||||||
|
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkLockDisable shuts down network-lock across the tailnet.
|
||||||
|
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
|
||||||
|
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
|
||||||
|
return fmt.Errorf("error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -207,10 +207,14 @@ func noDupFlagify(c *ffcli.Command) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileCmd func() *ffcli.Command
|
var (
|
||||||
var sysPolicyCmd func() *ffcli.Command
|
fileCmd,
|
||||||
var maybeWebCmd func() *ffcli.Command
|
sysPolicyCmd,
|
||||||
var maybeDriveCmd func() *ffcli.Command
|
maybeWebCmd,
|
||||||
|
maybeDriveCmd,
|
||||||
|
maybeNetlockCmd,
|
||||||
|
_ func() *ffcli.Command
|
||||||
|
)
|
||||||
|
|
||||||
func newRootCmd() *ffcli.Command {
|
func newRootCmd() *ffcli.Command {
|
||||||
rootfs := newFlagSet("tailscale")
|
rootfs := newFlagSet("tailscale")
|
||||||
@ -257,7 +261,7 @@ change in the future.
|
|||||||
nilOrCall(fileCmd),
|
nilOrCall(fileCmd),
|
||||||
bugReportCmd,
|
bugReportCmd,
|
||||||
certCmd,
|
certCmd,
|
||||||
netlockCmd,
|
nilOrCall(maybeNetlockCmd),
|
||||||
licensesCmd,
|
licensesCmd,
|
||||||
exitNodeCmd(),
|
exitNodeCmd(),
|
||||||
updateCmd,
|
updateCmd,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -27,6 +29,10 @@ import (
|
|||||||
"tailscale.com/util/prompt"
|
"tailscale.com/util/prompt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
maybeNetlockCmd = func() *ffcli.Command { return netlockCmd }
|
||||||
|
}
|
||||||
|
|
||||||
var netlockCmd = &ffcli.Command{
|
var netlockCmd = &ffcli.Command{
|
||||||
Name: "lock",
|
Name: "lock",
|
||||||
ShortUsage: "tailscale lock <subcommand> [arguments...]",
|
ShortUsage: "tailscale lock <subcommand> [arguments...]",
|
||||||
|
@ -77,3 +77,16 @@ func TestOmitDrive(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}.Check(t)
|
}.Check(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOmitTailnetLock(t *testing.T) {
|
||||||
|
deptest.DepChecker{
|
||||||
|
GOOS: "linux",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Tags: "ts_omit_tailnetlock,ts_include_cli",
|
||||||
|
OnDep: func(dep string) {
|
||||||
|
if strings.Contains(dep, "cbor") {
|
||||||
|
t.Errorf("unexpected dep with ts_omit_tailnetlock: %q", dep)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}.Check(t)
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ var Features = map[string]string{
|
|||||||
"syspolicy": "System policy configuration (MDM) support",
|
"syspolicy": "System policy configuration (MDM) support",
|
||||||
"systray": "Linux system tray",
|
"systray": "Linux system tray",
|
||||||
"taildrop": "Taildrop (file sending) support",
|
"taildrop": "Taildrop (file sending) support",
|
||||||
|
"tailnetlock": "Tailnet Lock support",
|
||||||
"tap": "Experimental Layer 2 (ethernet) support",
|
"tap": "Experimental Layer 2 (ethernet) support",
|
||||||
"tka": "Tailnet Lock (TKA) support",
|
"tka": "Tailnet Lock (TKA) support",
|
||||||
"tpm": "TPM support",
|
"tpm": "TPM support",
|
||||||
|
@ -82,7 +82,6 @@ import (
|
|||||||
"tailscale.com/posture"
|
"tailscale.com/posture"
|
||||||
"tailscale.com/syncs"
|
"tailscale.com/syncs"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tka"
|
|
||||||
"tailscale.com/tsd"
|
"tailscale.com/tsd"
|
||||||
"tailscale.com/tstime"
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/appctype"
|
"tailscale.com/types/appctype"
|
||||||
@ -7179,53 +7178,6 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
|
|||||||
return b.resetForProfileChangeLockedOnEntry(unlock)
|
return b.resetForProfileChangeLockedOnEntry(unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) initTKALocked() error {
|
|
||||||
cp := b.pm.CurrentProfile()
|
|
||||||
if cp.ID() == "" {
|
|
||||||
b.tka = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if b.tka != nil {
|
|
||||||
if b.tka.profile == cp.ID() {
|
|
||||||
// Already initialized.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// As we're switching profiles, we need to reset the TKA to nil.
|
|
||||||
b.tka = nil
|
|
||||||
}
|
|
||||||
root := b.TailscaleVarRoot()
|
|
||||||
if root == "" {
|
|
||||||
b.tka = nil
|
|
||||||
b.logf("network-lock unavailable; no state directory")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
chonkDir := b.chonkPathLocked()
|
|
||||||
if _, err := os.Stat(chonkDir); err == nil {
|
|
||||||
// The directory exists, which means network-lock has been initialized.
|
|
||||||
storage, err := tka.ChonkDir(chonkDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening tailchonk: %v", err)
|
|
||||||
}
|
|
||||||
authority, err := tka.Open(storage)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initializing tka: %v", err)
|
|
||||||
}
|
|
||||||
if err := authority.Compact(storage, tkaCompactionDefaults); err != nil {
|
|
||||||
b.logf("tka compaction failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.tka = &tkaState{
|
|
||||||
profile: cp.ID(),
|
|
||||||
authority: authority,
|
|
||||||
storage: storage,
|
|
||||||
}
|
|
||||||
b.logf("tka initialized at head %x", authority.Head())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetDialPlan resets the dialPlan for this LocalBackend. It will log if
|
// resetDialPlan resets the dialPlan for this LocalBackend. It will log if
|
||||||
// anything is reset.
|
// anything is reset.
|
||||||
//
|
//
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -56,6 +58,53 @@ type tkaState struct {
|
|||||||
filtered []ipnstate.TKAPeer
|
filtered []ipnstate.TKAPeer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) initTKALocked() error {
|
||||||
|
cp := b.pm.CurrentProfile()
|
||||||
|
if cp.ID() == "" {
|
||||||
|
b.tka = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if b.tka != nil {
|
||||||
|
if b.tka.profile == cp.ID() {
|
||||||
|
// Already initialized.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// As we're switching profiles, we need to reset the TKA to nil.
|
||||||
|
b.tka = nil
|
||||||
|
}
|
||||||
|
root := b.TailscaleVarRoot()
|
||||||
|
if root == "" {
|
||||||
|
b.tka = nil
|
||||||
|
b.logf("network-lock unavailable; no state directory")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chonkDir := b.chonkPathLocked()
|
||||||
|
if _, err := os.Stat(chonkDir); err == nil {
|
||||||
|
// The directory exists, which means network-lock has been initialized.
|
||||||
|
storage, err := tka.ChonkDir(chonkDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening tailchonk: %v", err)
|
||||||
|
}
|
||||||
|
authority, err := tka.Open(storage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initializing tka: %v", err)
|
||||||
|
}
|
||||||
|
if err := authority.Compact(storage, tkaCompactionDefaults); err != nil {
|
||||||
|
b.logf("tka compaction failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.tka = &tkaState{
|
||||||
|
profile: cp.ID(),
|
||||||
|
authority: authority,
|
||||||
|
storage: storage,
|
||||||
|
}
|
||||||
|
b.logf("tka initialized at head %x", authority.Head())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||||
// nodes from the netmap whose signature does not verify.
|
// nodes from the netmap whose signature does not verify.
|
||||||
//
|
//
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package ipnlocal
|
package ipnlocal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
31
ipn/ipnlocal/tailnetlock_disabled.go
Normal file
31
ipn/ipnlocal/tailnetlock_disabled.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build ts_omit_tailnetlock
|
||||||
|
|
||||||
|
package ipnlocal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/netmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tkaState struct {
|
||||||
|
authority *tka.Authority
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) initTKALocked() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {}
|
||||||
|
|
||||||
|
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||||
|
return &ipnstate.NetworkLockStatus{Enabled: false}
|
||||||
|
}
|
@ -41,14 +41,12 @@ import (
|
|||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/net/portmapper"
|
"tailscale.com/net/portmapper"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tka"
|
|
||||||
"tailscale.com/tstime"
|
"tailscale.com/tstime"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/logid"
|
"tailscale.com/types/logid"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
"tailscale.com/types/tkatype"
|
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
"tailscale.com/util/eventbus"
|
"tailscale.com/util/eventbus"
|
||||||
"tailscale.com/util/httpm"
|
"tailscale.com/util/httpm"
|
||||||
@ -124,19 +122,6 @@ var handler = map[string]LocalAPIHandler{
|
|||||||
"start": (*Handler).serveStart,
|
"start": (*Handler).serveStart,
|
||||||
"status": (*Handler).serveStatus,
|
"status": (*Handler).serveStatus,
|
||||||
"suggest-exit-node": (*Handler).serveSuggestExitNode,
|
"suggest-exit-node": (*Handler).serveSuggestExitNode,
|
||||||
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
|
|
||||||
"tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM,
|
|
||||||
"tka/disable": (*Handler).serveTKADisable,
|
|
||||||
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
|
|
||||||
"tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM,
|
|
||||||
"tka/init": (*Handler).serveTKAInit,
|
|
||||||
"tka/log": (*Handler).serveTKALog,
|
|
||||||
"tka/modify": (*Handler).serveTKAModify,
|
|
||||||
"tka/sign": (*Handler).serveTKASign,
|
|
||||||
"tka/status": (*Handler).serveTKAStatus,
|
|
||||||
"tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM,
|
|
||||||
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
|
|
||||||
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
|
|
||||||
"update/check": (*Handler).serveUpdateCheck,
|
"update/check": (*Handler).serveUpdateCheck,
|
||||||
"update/install": (*Handler).serveUpdateInstall,
|
"update/install": (*Handler).serveUpdateInstall,
|
||||||
"update/progress": (*Handler).serveUpdateProgress,
|
"update/progress": (*Handler).serveUpdateProgress,
|
||||||
@ -1892,25 +1877,6 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
|||||||
json.NewEncoder(w).Encode(struct{}{})
|
json.NewEncoder(w).Encode(struct{}{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitRead {
|
|
||||||
http.Error(w, "lock status access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.GET {
|
|
||||||
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveSetGUIVisible(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveSetGUIVisible(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != httpm.POST {
|
if r.Method != httpm.POST {
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
@ -1958,366 +1924,6 @@ func (h *Handler) serveSetUseExitNodeEnabled(w http.ResponseWriter, r *http.Requ
|
|||||||
e.Encode(prefs)
|
e.Encode(prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "lock sign access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type signRequest struct {
|
|
||||||
NodeKey key.NodePublic
|
|
||||||
RotationPublic []byte
|
|
||||||
}
|
|
||||||
var req signRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
|
|
||||||
http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "lock init access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type initRequest struct {
|
|
||||||
Keys []tka.Key
|
|
||||||
DisablementValues [][]byte
|
|
||||||
SupportDisablement []byte
|
|
||||||
}
|
|
||||||
var req initRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.b.NetworkLockAllowed() {
|
|
||||||
http.Error(w, "Tailnet Lock is not supported on your pricing plan", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
|
|
||||||
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type modifyRequest struct {
|
|
||||||
AddKeys []tka.Key
|
|
||||||
RemoveKeys []tka.Key
|
|
||||||
}
|
|
||||||
var req modifyRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
|
|
||||||
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(204)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type wrapRequest struct {
|
|
||||||
TSKey string
|
|
||||||
TKAKey string // key.NLPrivate.MarshalText
|
|
||||||
}
|
|
||||||
var req wrapRequest
|
|
||||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 12*1024)).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var priv key.NLPrivate
|
|
||||||
if err := priv.UnmarshalText([]byte(req.TKAKey)); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(wrappedKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitRead {
|
|
||||||
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type verifyRequest struct {
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
var req verifyRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
|
||||||
j, err := json.MarshalIndent(res, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body := io.LimitReader(r.Body, 1024*1024)
|
|
||||||
secret, err := io.ReadAll(body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "reading secret", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockDisable(secret); err != nil {
|
|
||||||
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require a JSON stanza for the body as an additional CSRF protection.
|
|
||||||
var req struct{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockForceLocalDisable(); err != nil {
|
|
||||||
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != httpm.GET {
|
|
||||||
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := 50
|
|
||||||
if limitStr := r.FormValue("limit"); limitStr != "" {
|
|
||||||
l, err := strconv.Atoi(limitStr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
limit = int(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
updates, err := h.b.NetworkLockLog(limit)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(updates, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyID, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2048))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "reading body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sigs, err := h.b.NetworkLockAffectedSigs(keyID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(sigs, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type verifyRequest struct {
|
|
||||||
Keys []tkatype.KeyID
|
|
||||||
ForkFrom string
|
|
||||||
}
|
|
||||||
var req verifyRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var forkFrom tka.AUMHash
|
|
||||||
if req.ForkFrom != "" {
|
|
||||||
if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil {
|
|
||||||
http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Write(res.Serialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body := io.LimitReader(r.Body, 1024*1024)
|
|
||||||
aumBytes, err := io.ReadAll(body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "reading AUM", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var aum tka.AUM
|
|
||||||
if err := aum.Unserialize(aumBytes); err != nil {
|
|
||||||
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Write(res.Serialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body := io.LimitReader(r.Body, 1024*1024)
|
|
||||||
aumBytes, err := io.ReadAll(body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "reading AUM", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var aum tka.AUM
|
|
||||||
if err := aum.Unserialize(aumBytes); err != nil {
|
|
||||||
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveProfiles serves profile switching-related endpoints. Supported methods
|
// serveProfiles serves profile switching-related endpoints. Supported methods
|
||||||
// and paths are:
|
// and paths are:
|
||||||
// - GET /profiles/: list all profiles (JSON-encoded array of ipn.LoginProfiles)
|
// - GET /profiles/: list all profiles (JSON-encoded array of ipn.LoginProfiles)
|
||||||
|
413
ipn/localapi/tailnetlock.go
Normal file
413
ipn/localapi/tailnetlock.go
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
|
package localapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/tkatype"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
handler["tka/affected-sigs"] = (*Handler).serveTKAAffectedSigs
|
||||||
|
handler["tka/cosign-recovery-aum"] = (*Handler).serveTKACosignRecoveryAUM
|
||||||
|
handler["tka/disable"] = (*Handler).serveTKADisable
|
||||||
|
handler["tka/force-local-disable"] = (*Handler).serveTKALocalDisable
|
||||||
|
handler["tka/generate-recovery-aum"] = (*Handler).serveTKAGenerateRecoveryAUM
|
||||||
|
handler["tka/init"] = (*Handler).serveTKAInit
|
||||||
|
handler["tka/log"] = (*Handler).serveTKALog
|
||||||
|
handler["tka/modify"] = (*Handler).serveTKAModify
|
||||||
|
handler["tka/sign"] = (*Handler).serveTKASign
|
||||||
|
handler["tka/status"] = (*Handler).serveTKAStatus
|
||||||
|
handler["tka/submit-recovery-aum"] = (*Handler).serveTKASubmitRecoveryAUM
|
||||||
|
handler["tka/verify-deeplink"] = (*Handler).serveTKAVerifySigningDeeplink
|
||||||
|
handler["tka/wrap-preauth-key"] = (*Handler).serveTKAWrapPreauthKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitRead {
|
||||||
|
http.Error(w, "lock status access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.GET {
|
||||||
|
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "lock sign access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type signRequest struct {
|
||||||
|
NodeKey key.NodePublic
|
||||||
|
RotationPublic []byte
|
||||||
|
}
|
||||||
|
var req signRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
|
||||||
|
http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "lock init access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type initRequest struct {
|
||||||
|
Keys []tka.Key
|
||||||
|
DisablementValues [][]byte
|
||||||
|
SupportDisablement []byte
|
||||||
|
}
|
||||||
|
var req initRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.b.NetworkLockAllowed() {
|
||||||
|
http.Error(w, "Tailnet Lock is not supported on your pricing plan", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
|
||||||
|
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type modifyRequest struct {
|
||||||
|
AddKeys []tka.Key
|
||||||
|
RemoveKeys []tka.Key
|
||||||
|
}
|
||||||
|
var req modifyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
|
||||||
|
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrapRequest struct {
|
||||||
|
TSKey string
|
||||||
|
TKAKey string // key.NLPrivate.MarshalText
|
||||||
|
}
|
||||||
|
var req wrapRequest
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 12*1024)).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var priv key.NLPrivate
|
||||||
|
if err := priv.UnmarshalText([]byte(req.TKAKey)); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(wrappedKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitRead {
|
||||||
|
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type verifyRequest struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
var req verifyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||||
|
j, err := json.MarshalIndent(res, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := io.LimitReader(r.Body, 1024*1024)
|
||||||
|
secret, err := io.ReadAll(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading secret", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockDisable(secret); err != nil {
|
||||||
|
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require a JSON stanza for the body as an additional CSRF protection.
|
||||||
|
var req struct{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockForceLocalDisable(); err != nil {
|
||||||
|
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != httpm.GET {
|
||||||
|
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 50
|
||||||
|
if limitStr := r.FormValue("limit"); limitStr != "" {
|
||||||
|
l, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit = int(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates, err := h.b.NetworkLockLog(limit)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := json.MarshalIndent(updates, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyID, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2048))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sigs, err := h.b.NetworkLockAffectedSigs(keyID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j, err := json.MarshalIndent(sigs, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type verifyRequest struct {
|
||||||
|
Keys []tkatype.KeyID
|
||||||
|
ForkFrom string
|
||||||
|
}
|
||||||
|
var req verifyRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var forkFrom tka.AUMHash
|
||||||
|
if req.ForkFrom != "" {
|
||||||
|
if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil {
|
||||||
|
http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
w.Write(res.Serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := io.LimitReader(r.Body, 1024*1024)
|
||||||
|
aumBytes, err := io.ReadAll(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading AUM", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var aum tka.AUM
|
||||||
|
if err := aum.Unserialize(aumBytes); err != nil {
|
||||||
|
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
w.Write(res.Serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := io.LimitReader(r.Body, 1024*1024)
|
||||||
|
aumBytes, err := io.ReadAll(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading AUM", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var aum tka.AUM
|
||||||
|
if err := aum.Unserialize(aumBytes); err != nil {
|
||||||
|
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
149
tka/disabled_stub.go
Normal file
149
tka/disabled_stub.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build ts_omit_tailnetlock
|
||||||
|
|
||||||
|
package tka
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/tkatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authority struct {
|
||||||
|
head AUM
|
||||||
|
oldestAncestor AUM
|
||||||
|
state State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Authority) Head() AUMHash { return AUMHash{} }
|
||||||
|
|
||||||
|
func (AUMHash) MarshalText() ([]byte, error) { return nil, errNoTailnetLock }
|
||||||
|
|
||||||
|
type State struct{}
|
||||||
|
|
||||||
|
// AUMKind describes valid AUM types.
|
||||||
|
type AUMKind uint8
|
||||||
|
|
||||||
|
type AUMHash [32]byte
|
||||||
|
|
||||||
|
type AUM struct {
|
||||||
|
MessageKind AUMKind `cbor:"1,keyasint"`
|
||||||
|
PrevAUMHash []byte `cbor:"2,keyasint"`
|
||||||
|
|
||||||
|
// Key encodes a public key to be added to the key authority.
|
||||||
|
// This field is used for AddKey AUMs.
|
||||||
|
Key *Key `cbor:"3,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// KeyID references a public key which is part of the key authority.
|
||||||
|
// This field is used for RemoveKey and UpdateKey AUMs.
|
||||||
|
KeyID tkatype.KeyID `cbor:"4,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// State describes the full state of the key authority.
|
||||||
|
// This field is used for Checkpoint AUMs.
|
||||||
|
State *State `cbor:"5,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// Votes and Meta describe properties of a key in the key authority.
|
||||||
|
// These fields are used for UpdateKey AUMs.
|
||||||
|
Votes *uint `cbor:"6,keyasint,omitempty"`
|
||||||
|
Meta map[string]string `cbor:"7,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// Signatures lists the signatures over this AUM.
|
||||||
|
// CBOR key 23 is the last key which can be encoded as a single byte.
|
||||||
|
Signatures []tkatype.Signature `cbor:"23,keyasint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chonk interface {
|
||||||
|
// AUM returns the AUM with the specified digest.
|
||||||
|
//
|
||||||
|
// If the AUM does not exist, then os.ErrNotExist is returned.
|
||||||
|
AUM(hash AUMHash) (AUM, error)
|
||||||
|
|
||||||
|
// ChildAUMs returns all AUMs with a specified previous
|
||||||
|
// AUM hash.
|
||||||
|
ChildAUMs(prevAUMHash AUMHash) ([]AUM, error)
|
||||||
|
|
||||||
|
// CommitVerifiedAUMs durably stores the provided AUMs.
|
||||||
|
// Callers MUST ONLY provide AUMs which are verified (specifically,
|
||||||
|
// a call to aumVerify() must return a nil error).
|
||||||
|
// as the implementation assumes that only verified AUMs are stored.
|
||||||
|
CommitVerifiedAUMs(updates []AUM) error
|
||||||
|
|
||||||
|
// Heads returns AUMs for which there are no children. In other
|
||||||
|
// words, the latest AUM in all possible chains (the 'leaves').
|
||||||
|
Heads() ([]AUM, error)
|
||||||
|
|
||||||
|
// SetLastActiveAncestor is called to record the oldest-known AUM
|
||||||
|
// that contributed to the current state. This value is used as
|
||||||
|
// a hint on next startup to determine which chain to pick when computing
|
||||||
|
// the current state, if there are multiple distinct chains.
|
||||||
|
SetLastActiveAncestor(hash AUMHash) error
|
||||||
|
|
||||||
|
// LastActiveAncestor returns the oldest-known AUM that was (in a
|
||||||
|
// previous run) an ancestor of the current state. This is used
|
||||||
|
// as a hint to pick the correct chain in the event that the Chonk stores
|
||||||
|
// multiple distinct chains.
|
||||||
|
LastActiveAncestor() (*AUMHash, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SigKind describes valid NodeKeySignature types.
|
||||||
|
type SigKind uint8
|
||||||
|
|
||||||
|
type NodeKeySignature struct {
|
||||||
|
// SigKind identifies the variety of signature.
|
||||||
|
SigKind SigKind `cbor:"1,keyasint"`
|
||||||
|
// Pubkey identifies the key.NodePublic which is being authorized.
|
||||||
|
// SigCredential signatures do not use this field.
|
||||||
|
Pubkey []byte `cbor:"2,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// KeyID identifies which key in the tailnet key authority should
|
||||||
|
// be used to verify this signature. Only set for SigDirect and
|
||||||
|
// SigCredential signature kinds.
|
||||||
|
KeyID []byte `cbor:"3,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// Signature is the packed (R, S) ed25519 signature over all other
|
||||||
|
// fields of the structure.
|
||||||
|
Signature []byte `cbor:"4,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// Nested describes a NodeKeySignature which authorizes the node-key
|
||||||
|
// used as Pubkey. Only used for SigRotation signatures.
|
||||||
|
Nested *NodeKeySignature `cbor:"5,keyasint,omitempty"`
|
||||||
|
|
||||||
|
// WrappingPubkey specifies the ed25519 public key which must be used
|
||||||
|
// to sign a Signature which embeds this one.
|
||||||
|
//
|
||||||
|
// For SigRotation signatures multiple levels deep, intermediate
|
||||||
|
// signatures may omit this value, in which case the parent WrappingPubkey
|
||||||
|
// is used.
|
||||||
|
//
|
||||||
|
// SigCredential signatures use this field to specify the public key
|
||||||
|
// they are certifying, following the usual semanticsfor WrappingPubkey.
|
||||||
|
WrappingPubkey []byte `cbor:"6,keyasint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeeplinkValidationResult struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AUMHash) UnmarshalText(text []byte) error {
|
||||||
|
return errNoTailnetLock
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoTailnetLock = errors.New("tailnet lock is not enabled")
|
||||||
|
|
||||||
|
func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) {
|
||||||
|
return wrappedAuthKey, false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s NodeKeySignature) String() string { return "" }
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
// Package tka (WIP) implements the Tailnet Key Authority.
|
// Package tka (WIP) implements the Tailnet Key Authority.
|
||||||
package tka
|
package tka
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_tailnetlock
|
||||||
|
|
||||||
package netlogtype
|
package netlogtype
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user