mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
cmd/tailscale,ipn: support disablement args in lock cli, implement disable
* Support specifiying disablement values in lock init command * Support specifying rotation key in lock sign command * Implement lock disable command * Implement disablement-kdf command Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
fb392e34b5
commit
3271daf7a3
@ -827,6 +827,14 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetworkLockDisable shuts down network-lock across the tailnet.
|
||||||
|
func (lc *LocalClient) 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).
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"tailscale.com/health/healthmsg"
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tka"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/persist"
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/types/preftype"
|
"tailscale.com/types/preftype"
|
||||||
@ -1156,3 +1157,69 @@ func TestUpWorthWarning(t *testing.T) {
|
|||||||
t.Errorf("want false for other misc errors")
|
t.Errorf("want false for other misc errors")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseNLArgs(t *testing.T) {
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
parseKeys bool
|
||||||
|
parseDisablements bool
|
||||||
|
|
||||||
|
wantErr error
|
||||||
|
wantKeys []tka.Key
|
||||||
|
wantDisablements [][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: nil,
|
||||||
|
parseKeys: true,
|
||||||
|
parseDisablements: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key no votes",
|
||||||
|
input: []string{"nlpub:" + strings.Repeat("00", 32)},
|
||||||
|
parseKeys: true,
|
||||||
|
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key with votes",
|
||||||
|
input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
|
||||||
|
parseKeys: true,
|
||||||
|
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disablements",
|
||||||
|
input: []string{"disablement:" + strings.Repeat("02", 32), "disablement-secret:" + strings.Repeat("03", 32)},
|
||||||
|
parseDisablements: true,
|
||||||
|
wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disablements not allowed",
|
||||||
|
input: []string{"disablement:" + strings.Repeat("02", 32)},
|
||||||
|
parseKeys: true,
|
||||||
|
wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix nlpub:"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keys not allowed",
|
||||||
|
input: []string{"nlpub:" + strings.Repeat("02", 32)},
|
||||||
|
parseDisablements: true,
|
||||||
|
wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
keys, disablements, err := parseNLArgs(tc.input, tc.parseKeys, tc.parseDisablements)
|
||||||
|
if !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
|
t.Fatalf("parseNLArgs(%v).err = %v, want %v", tc.input, err, tc.wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(keys, tc.wantKeys) {
|
||||||
|
t.Errorf("keys = %v, want %v", keys, tc.wantKeys)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(disablements, tc.wantDisablements) {
|
||||||
|
t.Errorf("disablements = %v, want %v", disablements, tc.wantDisablements)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -27,6 +27,8 @@
|
|||||||
nlAddCmd,
|
nlAddCmd,
|
||||||
nlRemoveCmd,
|
nlRemoveCmd,
|
||||||
nlSignCmd,
|
nlSignCmd,
|
||||||
|
nlDisableCmd,
|
||||||
|
nlDisablementKDFCmd,
|
||||||
},
|
},
|
||||||
Exec: runNetworkLockStatus,
|
Exec: runNetworkLockStatus,
|
||||||
}
|
}
|
||||||
@ -47,15 +49,12 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
|||||||
return errors.New("network-lock is already enabled")
|
return errors.New("network-lock is already enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the set of initially-trusted keys.
|
// Parse initially-trusted keys & disablement values.
|
||||||
keys, err := parseNLKeyArgs(args)
|
keys, disablementValues, err := parseNLArgs(args, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
status, err := localClient.NetworkLockInit(ctx, keys, disablementValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -143,17 +142,34 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseNLKeyArgs converts a slice of strings into a slice of tka.Key. The keys
|
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
|
||||||
// should be specified using their key.NLPublic.MarshalText representation with
|
// values/secrets.
|
||||||
// an optional '?<votes>' suffix. If any of the keys encounters an error, a nil
|
// The keys encoded in args should be specified using their key.NLPublic.MarshalText
|
||||||
// slice is returned along with an appropriate error.
|
// representation with an optional '?<votes>' suffix.
|
||||||
func parseNLKeyArgs(args []string) ([]tka.Key, error) {
|
// Disablement values or secrets must be encoded in hex with a prefix of 'disablement:' or
|
||||||
var keys []tka.Key
|
// 'disablement-secret:'.
|
||||||
|
//
|
||||||
|
// If any element could not be parsed,
|
||||||
|
// a nil slice is returned along with an appropriate error.
|
||||||
|
func parseNLArgs(args []string, parseKeys, parseDisablements bool) (keys []tka.Key, disablements [][]byte, err error) {
|
||||||
for i, a := range args {
|
for i, a := range args {
|
||||||
|
if parseDisablements && (strings.HasPrefix(a, "disablement:") || strings.HasPrefix(a, "disablement-secret:")) {
|
||||||
|
b, err := hex.DecodeString(a[strings.Index(a, ":")+1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("parsing disablement %d: %v", i+1, err)
|
||||||
|
}
|
||||||
|
disablements = append(disablements, b)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parseKeys {
|
||||||
|
return nil, nil, fmt.Errorf("parsing argument %d: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", i+1, a)
|
||||||
|
}
|
||||||
|
|
||||||
var nlpk key.NLPublic
|
var nlpk key.NLPublic
|
||||||
spl := strings.SplitN(a, "?", 2)
|
spl := strings.SplitN(a, "?", 2)
|
||||||
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
|
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
|
||||||
return nil, fmt.Errorf("parsing key %d: %v", i+1, err)
|
return nil, nil, fmt.Errorf("parsing key %d: %v", i+1, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
k := tka.Key{
|
k := tka.Key{
|
||||||
@ -164,13 +180,13 @@ func parseNLKeyArgs(args []string) ([]tka.Key, error) {
|
|||||||
if len(spl) > 1 {
|
if len(spl) > 1 {
|
||||||
votes, err := strconv.Atoi(spl[1])
|
votes, err := strconv.Atoi(spl[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
return nil, nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
||||||
}
|
}
|
||||||
k.Votes = uint(votes)
|
k.Votes = uint(votes)
|
||||||
}
|
}
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
return keys, nil
|
return keys, disablements, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
|
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
|
||||||
@ -182,11 +198,11 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
|||||||
return errors.New("network-lock is not enabled")
|
return errors.New("network-lock is not enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
addKeys, err := parseNLKeyArgs(addArgs)
|
addKeys, _, err := parseNLArgs(addArgs, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
removeKeys, err := parseNLKeyArgs(removeArgs)
|
removeKeys, _, err := parseNLArgs(removeArgs, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -202,24 +218,65 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
|||||||
|
|
||||||
var nlSignCmd = &ffcli.Command{
|
var nlSignCmd = &ffcli.Command{
|
||||||
Name: "sign",
|
Name: "sign",
|
||||||
ShortUsage: "sign <node-key>",
|
ShortUsage: "sign <node-key> [<rotation-key>]",
|
||||||
ShortHelp: "Signs a node-key and transmits that signature to the control plane",
|
ShortHelp: "Signs a node-key and transmits that signature to the control plane",
|
||||||
Exec: runNetworkLockSign,
|
Exec: runNetworkLockSign,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(tom): Implement specifying the rotation key for the signature.
|
|
||||||
func runNetworkLockSign(ctx context.Context, args []string) error {
|
func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||||
switch len(args) {
|
var (
|
||||||
case 0:
|
nodeKey key.NodePublic
|
||||||
return errors.New("expected node-key as second argument")
|
rotationKey key.NLPublic
|
||||||
case 1:
|
)
|
||||||
var nodeKey key.NodePublic
|
|
||||||
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
|
|
||||||
return fmt.Errorf("decoding node-key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return localClient.NetworkLockSign(ctx, nodeKey, nil)
|
if len(args) == 0 || len(args) > 2 {
|
||||||
default:
|
return errors.New("usage: lock sign <node-key> [<rotation-key>]")
|
||||||
return errors.New("expected a single node-key as only argument")
|
|
||||||
}
|
}
|
||||||
|
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
|
||||||
|
return fmt.Errorf("decoding node-key: %w", err)
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
if err := rotationKey.UnmarshalText([]byte(args[1])); err != nil {
|
||||||
|
return fmt.Errorf("decoding rotation-key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var nlDisableCmd = &ffcli.Command{
|
||||||
|
Name: "disable",
|
||||||
|
ShortUsage: "disable <disablement-secret>",
|
||||||
|
ShortHelp: "Consumes a disablement secret to shut down network-lock across the tailnet",
|
||||||
|
Exec: runNetworkLockDisable,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkLockDisable(ctx context.Context, args []string) error {
|
||||||
|
_, secrets, err := parseNLArgs(args, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(secrets) != 1 {
|
||||||
|
return errors.New("usage: lock disable <disablement-secret>")
|
||||||
|
}
|
||||||
|
return localClient.NetworkLockDisable(ctx, secrets[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var nlDisablementKDFCmd = &ffcli.Command{
|
||||||
|
Name: "disablement-kdf",
|
||||||
|
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
|
||||||
|
ShortHelp: "Computes a disablement value from a disablement secret",
|
||||||
|
Exec: runNetworkLockDisablementKDF,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return errors.New("usage: lock disablement-kdf <hex-encoded-disablement-secret>")
|
||||||
|
}
|
||||||
|
secret, err := hex.DecodeString(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@ -80,6 +81,7 @@
|
|||||||
"tka/modify": (*Handler).serveTKAModify,
|
"tka/modify": (*Handler).serveTKAModify,
|
||||||
"tka/sign": (*Handler).serveTKASign,
|
"tka/sign": (*Handler).serveTKASign,
|
||||||
"tka/status": (*Handler).serveTKAStatus,
|
"tka/status": (*Handler).serveTKAStatus,
|
||||||
|
"tka/disable": (*Handler).serveTKADisable,
|
||||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||||
"whois": (*Handler).serveWhoIs,
|
"whois": (*Handler).serveWhoIs,
|
||||||
}
|
}
|
||||||
@ -1073,6 +1075,30 @@ type modifyRequest struct {
|
|||||||
w.Write(j)
|
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 != http.MethodPost {
|
||||||
|
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := io.LimitReader(r.Body, 1024*1024)
|
||||||
|
secret, err := ioutil.ReadAll(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "reading secret", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.b.NetworkLockDisable(secret); err != nil {
|
||||||
|
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
|
||||||
func defBool(a string, def bool) bool {
|
func defBool(a string, def bool) bool {
|
||||||
if a == "" {
|
if a == "" {
|
||||||
return def
|
return def
|
||||||
|
Loading…
Reference in New Issue
Block a user