mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-02 07:52:05 +01:00
cmd/tailscale/cli: stabilise the output of tailscale lock log --json
This patch changes the behaviour of `tailscale lock log --json` to make
it more useful for users. It also introduces versioning of our JSON output.
## Changes to `tailscale lock log --json`
Previously this command would print the hash and base64-encoded bytes of
each AUM, and users would need their own CBOR decoder to interpret it in
a useful way:
```json
[
{
"Hash": [
80,
136,
151,
…
],
"Change": "checkpoint",
"Raw": "pAEFAvYFpQH2AopYIAkPN+8V3cJpkoC5ZY2+RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm+NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl/D93I1M56/rowU+UIlYIPZ/SxT9EA2Idy9kaCbsFzjX/s3Ms7584wWGbWd/f/QAWCBHYZzYiAPpQ+NXN+1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK/RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz+dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav/k6dDF4GiAVgg5Eh00epI7PPW2sjKCc/nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc+m45ay5PB/OB4AA9Fdki4KJq9Ll+PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
```
Now we print the AUM in an expanded form that can be easily read by scripts,
although we include the raw bytes for verification and auditing.
```json
{
"SchemaVersion": "1",
"Messages": [
{
"Hash": "KCEJPRKNSXJG2TPH3EHQRLJNLIIK2DV53FUNPADWA7BZJWBDRXZQ",
"AUM": {
"MessageKind": "checkpoint",
"PrevAUMHash": null,
"Key": null,
"KeyID": null,
"State": {
…
},
"Votes": null,
"Meta": null,
"Signatures": [
{
"KeyID": "tlpub:e44874d1ea48ecf3d6dac8ca09cfe70dc958ad83b656393432016c3ed229c8d6",
"Signature": "8yAKKHPpuOWsuTwfzgeAAPRXZIuCiavS5fjxeiCR2JTlYaU23NxNChezg7tVlEXdH+z151u9na/PQknxsSBwBQ=="
}
]
},
"Raw": "pAEFAvYFpQH2AopYIAkPN-8V3cJpkoC5ZY2-RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm-NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl_D93I1M56_rowU-UIlYIPZ_SxT9EA2Idy9kaCbsFzjX_s3Ms7584wWGbWd_f_QAWCBHYZzYiAPpQ-NXN-1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK_RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz-dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav_k6dDF4GiAVgg5Eh00epI7PPW2sjKCc_nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc-m45ay5PB_OB4AA9Fdki4KJq9Ll-PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
}
```
This output was previously marked as unstable, and it wasn't very useful,
so changing it should be fine.
## Versioning our JSON output
This patch introduces a way to version our JSON output on the CLI, so we
can make backwards-incompatible changes in future without breaking existing
scripts or integrations.
You can run this command in two ways:
```
tailscale lock log --json
tailscale lock log --json=1
```
Passing an explicit version number allows you to pick a specific JSON schema.
If we ever want to change the schema, we increment the version number and
users must opt-in to the new output.
A bare `--json` flag will always return schema version 1, for compatibility
with existing scripts.
Updates https://github.com/tailscale/tailscale/issues/17613
Updates https://github.com/tailscale/corp/issues/23258
Signed-off-by: Alex Chan <alexc@tailscale.com>
Change-Id: I897f78521cc1a81651f5476228c0882d7b723606
This commit is contained in:
parent
99b06eac49
commit
139c395d7d
84
cmd/tailscale/cli/jsonoutput/jsonoutput.go
Normal file
84
cmd/tailscale/cli/jsonoutput/jsonoutput.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Package jsonoutput provides stable and versioned JSON serialisation for CLI output.
|
||||||
|
// This allows us to provide stable output to scripts/clients, but also make
|
||||||
|
// breaking changes to the output when it's useful.
|
||||||
|
//
|
||||||
|
// Historically we only used `--json` as a boolean flag, so changing the output
|
||||||
|
// could break scripts that rely on the existing format.
|
||||||
|
//
|
||||||
|
// This package allows callers to pass a version number to `--json` and get
|
||||||
|
// a consistent output. We'll bump the version when we make a breaking change
|
||||||
|
// that's likely to break scripts that rely on the existing output, e.g. if
|
||||||
|
// we remove a field or change the type/format.
|
||||||
|
//
|
||||||
|
// Passing just the boolean flag `--json` will always return v1, to preserve
|
||||||
|
// compatibility with scripts written before we versioned our output.
|
||||||
|
package jsonoutput
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONSchemaVersion implements flag.Value, and tracks whether the CLI has
|
||||||
|
// been called with `--json`, and if so, with what value.
|
||||||
|
type JSONSchemaVersion struct {
|
||||||
|
// IsSet tracks if the flag was provided at all.
|
||||||
|
IsSet bool
|
||||||
|
|
||||||
|
// Value tracks the desired schema version, which defaults to 1 if
|
||||||
|
// the user passes `--json` without an argument.
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the default value which is printed in the CLI help text.
|
||||||
|
func (v *JSONSchemaVersion) String() string {
|
||||||
|
if v.IsSet {
|
||||||
|
return strconv.Itoa(v.Value)
|
||||||
|
} else {
|
||||||
|
return "(not set)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is called when the user passes the flag as a command-line argument.
|
||||||
|
func (v *JSONSchemaVersion) Set(s string) error {
|
||||||
|
if v.IsSet {
|
||||||
|
return errors.New("received multiple instances of --json; only pass it once")
|
||||||
|
}
|
||||||
|
|
||||||
|
v.IsSet = true
|
||||||
|
|
||||||
|
// If the user doesn't supply a schema version, default to 1.
|
||||||
|
// This ensures that any existing scripts will continue to get their
|
||||||
|
// current output.
|
||||||
|
if s == "true" {
|
||||||
|
v.Value = 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid integer value passed to --json: %q", s)
|
||||||
|
}
|
||||||
|
v.Value = version
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBoolFlag tells the flag package that JSONSchemaVersion can be set
|
||||||
|
// without an argument.
|
||||||
|
func (v *JSONSchemaVersion) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseEnvelope is a set of fields common to all versioned JSON output.
|
||||||
|
type ResponseEnvelope struct {
|
||||||
|
// SchemaVersion is the version of the JSON output, e.g. "1", "2", "3"
|
||||||
|
SchemaVersion string
|
||||||
|
|
||||||
|
// ResponseWarning tells a user if a newer version of the JSON output
|
||||||
|
// is available.
|
||||||
|
ResponseWarning string `json:"_WARNING,omitzero"`
|
||||||
|
}
|
||||||
203
cmd/tailscale/cli/jsonoutput/network-lock-v1.go
Normal file
203
cmd/tailscale/cli/jsonoutput/network-lock-v1.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package jsonoutput
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintNetworkLockJSONV1 prints the stored TKA state as a JSON object to the CLI,
|
||||||
|
// in a stable "v1" format.
|
||||||
|
//
|
||||||
|
// This format includes:
|
||||||
|
//
|
||||||
|
// - the AUM hash as a base32-encoded string
|
||||||
|
// - the raw AUM as base64-encoded bytes
|
||||||
|
// - the expanded AUM, which prints named fields for consumption by other tools
|
||||||
|
func PrintNetworkLockJSONV1(out io.Writer, updates []ipnstate.NetworkLockUpdate) error {
|
||||||
|
messages := make([]logMessageV1, len(updates))
|
||||||
|
|
||||||
|
for i, update := range updates {
|
||||||
|
var aum tka.AUM
|
||||||
|
if err := aum.Unserialize(update.Raw); err != nil {
|
||||||
|
return fmt.Errorf("decoding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := aum.Hash()
|
||||||
|
|
||||||
|
if !bytes.Equal(h[:], update.Hash[:]) {
|
||||||
|
return fmt.Errorf("incorrect AUM hash: got %v, want %v", h, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages[i] = toLogMessageV1(aum, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := struct {
|
||||||
|
ResponseEnvelope
|
||||||
|
Messages []logMessageV1
|
||||||
|
}{
|
||||||
|
ResponseEnvelope: ResponseEnvelope{
|
||||||
|
SchemaVersion: "1",
|
||||||
|
},
|
||||||
|
Messages: messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(out)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toLogMessageV1 converts a [tka.AUM] and [ipnstate.NetworkLockUpdate] to the
|
||||||
|
// JSON output returned by the CLI.
|
||||||
|
func toLogMessageV1(aum tka.AUM, update ipnstate.NetworkLockUpdate) logMessageV1 {
|
||||||
|
expandedAUM := expandedAUMV1{}
|
||||||
|
expandedAUM.MessageKind = aum.MessageKind.String()
|
||||||
|
if len(aum.PrevAUMHash) > 0 {
|
||||||
|
expandedAUM.PrevAUMHash = aum.PrevAUMHash.String()
|
||||||
|
}
|
||||||
|
if key := aum.Key; key != nil {
|
||||||
|
expandedAUM.Key = toExpandedKeyV1(key)
|
||||||
|
}
|
||||||
|
if keyID := aum.KeyID; keyID != nil {
|
||||||
|
expandedAUM.KeyID = fmt.Sprintf("tlpub:%x", keyID)
|
||||||
|
}
|
||||||
|
if state := aum.State; state != nil {
|
||||||
|
expandedState := expandedStateV1{}
|
||||||
|
if h := state.LastAUMHash; h != nil {
|
||||||
|
expandedState.LastAUMHash = h.String()
|
||||||
|
}
|
||||||
|
for _, secret := range state.DisablementSecrets {
|
||||||
|
expandedState.DisablementSecrets = append(expandedState.DisablementSecrets, fmt.Sprintf("%x", secret))
|
||||||
|
}
|
||||||
|
for _, key := range state.Keys {
|
||||||
|
expandedState.Keys = append(expandedState.Keys, toExpandedKeyV1(&key))
|
||||||
|
}
|
||||||
|
expandedState.StateID1 = state.StateID1
|
||||||
|
expandedState.StateID2 = state.StateID2
|
||||||
|
expandedAUM.State = expandedState
|
||||||
|
}
|
||||||
|
if votes := aum.Votes; votes != nil {
|
||||||
|
expandedAUM.Votes = *votes
|
||||||
|
}
|
||||||
|
expandedAUM.Meta = aum.Meta
|
||||||
|
for _, signature := range aum.Signatures {
|
||||||
|
expandedAUM.Signatures = append(expandedAUM.Signatures, expandedSignatureV1{
|
||||||
|
KeyID: fmt.Sprintf("tlpub:%x", signature.KeyID),
|
||||||
|
Signature: base64.URLEncoding.EncodeToString(signature.Signature),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return logMessageV1{
|
||||||
|
Hash: aum.Hash().String(),
|
||||||
|
AUM: expandedAUM,
|
||||||
|
Raw: base64.URLEncoding.EncodeToString(update.Raw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toExpandedKeyV1 converts a [tka.Key] to the JSON output returned
|
||||||
|
// by the CLI.
|
||||||
|
func toExpandedKeyV1(key *tka.Key) expandedKeyV1 {
|
||||||
|
return expandedKeyV1{
|
||||||
|
Kind: key.Kind.String(),
|
||||||
|
Votes: key.Votes,
|
||||||
|
Public: fmt.Sprintf("tlpub:%x", key.Public),
|
||||||
|
Meta: key.Meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logMessageV1 is the JSON representation of an AUM as both raw bytes and
|
||||||
|
// in its expanded form, and the CLI output is a list of these entries.
|
||||||
|
type logMessageV1 struct {
|
||||||
|
// The BLAKE2s digest of the CBOR-encoded AUM. This is printed as a
|
||||||
|
// base32-encoded string, e.g. KCE…XZQ
|
||||||
|
Hash string
|
||||||
|
|
||||||
|
// The expanded form of the AUM, which presents the fields in a more
|
||||||
|
// accessible format than doing a CBOR decoding.
|
||||||
|
AUM expandedAUMV1
|
||||||
|
|
||||||
|
// The raw bytes of the CBOR-encoded AUM, encoded as base64.
|
||||||
|
// This is useful for verifying the AUM hash.
|
||||||
|
Raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandedAUMV1 is the expanded version of a [tka.AUM], designed so external tools
|
||||||
|
// can read the AUM without knowing our CBOR definitions.
|
||||||
|
type expandedAUMV1 struct {
|
||||||
|
MessageKind string
|
||||||
|
PrevAUMHash string `json:"PrevAUMHash,omitzero"`
|
||||||
|
|
||||||
|
// Key encodes a public key to be added to the key authority.
|
||||||
|
// This field is used for AddKey AUMs.
|
||||||
|
Key expandedKeyV1 `json:"Key,omitzero"`
|
||||||
|
|
||||||
|
// KeyID references a public key which is part of the key authority.
|
||||||
|
// This field is used for RemoveKey and UpdateKey AUMs.
|
||||||
|
KeyID string `json:"KeyID,omitzero"`
|
||||||
|
|
||||||
|
// State describes the full state of the key authority.
|
||||||
|
// This field is used for Checkpoint AUMs.
|
||||||
|
State expandedStateV1 `json:"State,omitzero"`
|
||||||
|
|
||||||
|
// Votes and Meta describe properties of a key in the key authority.
|
||||||
|
// These fields are used for UpdateKey AUMs.
|
||||||
|
Votes uint `json:"Votes,omitzero"`
|
||||||
|
Meta map[string]string `json:"Meta,omitzero"`
|
||||||
|
|
||||||
|
// Signatures lists the signatures over this AUM.
|
||||||
|
Signatures []expandedSignatureV1 `json:"Signatures,omitzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandedAUMV1 is the expanded version of a [tka.Key], which describes
|
||||||
|
// the public components of a key known to network-lock.
|
||||||
|
type expandedKeyV1 struct {
|
||||||
|
Kind string
|
||||||
|
|
||||||
|
// Votes describes the weight applied to signatures using this key.
|
||||||
|
Votes uint
|
||||||
|
|
||||||
|
// Public encodes the public key of the key as a hex string.
|
||||||
|
Public string
|
||||||
|
|
||||||
|
// Meta describes arbitrary metadata about the key. This could be
|
||||||
|
// used to store the name of the key, for instance.
|
||||||
|
Meta map[string]string `json:"Meta,omitzero"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandedStateV1 is the expanded version of a [tka.State], which describes
|
||||||
|
// Tailnet Key Authority state at an instant in time.
|
||||||
|
type expandedStateV1 struct {
|
||||||
|
// LastAUMHash is the blake2s digest of the last-applied AUM.
|
||||||
|
LastAUMHash string `json:"LastAUMHash,omitzero"`
|
||||||
|
|
||||||
|
// DisablementSecrets are KDF-derived values which can be used
|
||||||
|
// to turn off the TKA in the event of a consensus-breaking bug.
|
||||||
|
DisablementSecrets []string
|
||||||
|
|
||||||
|
// Keys are the public keys of either:
|
||||||
|
//
|
||||||
|
// 1. The signing nodes currently trusted by the TKA.
|
||||||
|
// 2. Ephemeral keys that were used to generate pre-signed auth keys.
|
||||||
|
Keys []expandedKeyV1
|
||||||
|
|
||||||
|
// StateID's are nonce's, generated on enablement and fixed for
|
||||||
|
// the lifetime of the Tailnet Key Authority.
|
||||||
|
StateID1 uint64
|
||||||
|
StateID2 uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandedSignatureV1 is the expanded form of a [tka.Signature], which
|
||||||
|
// describes a signature over an AUM. This signature can be verified
|
||||||
|
// using the key referenced by KeyID.
|
||||||
|
type expandedSignatureV1 struct {
|
||||||
|
KeyID string
|
||||||
|
Signature string
|
||||||
|
}
|
||||||
@ -10,10 +10,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
jsonv1 "encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tka"
|
"tailscale.com/tka"
|
||||||
"tailscale.com/tsconst"
|
"tailscale.com/tsconst"
|
||||||
@ -219,7 +221,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nlStatusArgs.json {
|
if nlStatusArgs.json {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := jsonv1.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
return enc.Encode(st)
|
return enc.Encode(st)
|
||||||
}
|
}
|
||||||
@ -600,7 +602,7 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
|||||||
|
|
||||||
var nlLogArgs struct {
|
var nlLogArgs struct {
|
||||||
limit int
|
limit int
|
||||||
json bool
|
json jsonoutput.JSONSchemaVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
var nlLogCmd = &ffcli.Command{
|
var nlLogCmd = &ffcli.Command{
|
||||||
@ -612,7 +614,7 @@ var nlLogCmd = &ffcli.Command{
|
|||||||
FlagSet: (func() *flag.FlagSet {
|
FlagSet: (func() *flag.FlagSet {
|
||||||
fs := newFlagSet("lock log")
|
fs := newFlagSet("lock log")
|
||||||
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
||||||
fs.BoolVar(&nlLogArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
fs.Var(&nlLogArgs.json, "json", "output in JSON format")
|
||||||
return fs
|
return fs
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
@ -678,7 +680,7 @@ func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, er
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
// Print a JSON encoding of the AUM as a fallback.
|
// Print a JSON encoding of the AUM as a fallback.
|
||||||
e := json.NewEncoder(&stanza)
|
e := jsonv1.NewEncoder(&stanza)
|
||||||
e.SetIndent("", "\t")
|
e.SetIndent("", "\t")
|
||||||
if err := e.Encode(aum); err != nil {
|
if err := e.Encode(aum); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -702,14 +704,21 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fixTailscaledConnectError(err)
|
return fixTailscaledConnectError(err)
|
||||||
}
|
}
|
||||||
if nlLogArgs.json {
|
|
||||||
enc := json.NewEncoder(Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(updates)
|
|
||||||
}
|
|
||||||
|
|
||||||
out, useColor := colorableOutput()
|
out, useColor := colorableOutput()
|
||||||
|
|
||||||
|
return printNetworkLockLog(updates, out, nlLogArgs.json, useColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNetworkLockLog(updates []ipnstate.NetworkLockUpdate, out io.Writer, jsonSchema jsonoutput.JSONSchemaVersion, useColor bool) error {
|
||||||
|
if jsonSchema.IsSet {
|
||||||
|
if jsonSchema.Value == 1 {
|
||||||
|
return jsonoutput.PrintNetworkLockJSONV1(out, updates)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unrecognised version: %q", jsonSchema.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
stanza, err := nlDescribeUpdate(update, useColor)
|
stanza, err := nlDescribeUpdate(update, useColor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
204
cmd/tailscale/cli/network-lock_test.go
Normal file
204
cmd/tailscale/cli/network-lock_test.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tka"
|
||||||
|
"tailscale.com/types/tkatype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNetworkLockLogOutput(t *testing.T) {
|
||||||
|
votes := uint(1)
|
||||||
|
aum1 := tka.AUM{
|
||||||
|
MessageKind: tka.AUMAddKey,
|
||||||
|
Key: &tka.Key{
|
||||||
|
Kind: tka.Key25519,
|
||||||
|
Votes: 1,
|
||||||
|
Public: []byte{2, 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h1 := aum1.Hash()
|
||||||
|
aum2 := tka.AUM{
|
||||||
|
MessageKind: tka.AUMRemoveKey,
|
||||||
|
KeyID: []byte{3, 3},
|
||||||
|
PrevAUMHash: h1[:],
|
||||||
|
Signatures: []tkatype.Signature{
|
||||||
|
{
|
||||||
|
KeyID: []byte{3, 4},
|
||||||
|
Signature: []byte{4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Meta: map[string]string{"en": "three", "de": "drei", "es": "tres"},
|
||||||
|
}
|
||||||
|
h2 := aum2.Hash()
|
||||||
|
aum3 := tka.AUM{
|
||||||
|
MessageKind: tka.AUMCheckpoint,
|
||||||
|
PrevAUMHash: h2[:],
|
||||||
|
State: &tka.State{
|
||||||
|
Keys: []tka.Key{
|
||||||
|
{
|
||||||
|
Kind: tka.Key25519,
|
||||||
|
Votes: 1,
|
||||||
|
Public: []byte{1, 1},
|
||||||
|
Meta: map[string]string{"en": "one", "de": "eins", "es": "uno"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DisablementSecrets: [][]byte{
|
||||||
|
{1, 2, 3},
|
||||||
|
{4, 5, 6},
|
||||||
|
{7, 8, 9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Votes: &votes,
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := []ipnstate.NetworkLockUpdate{
|
||||||
|
{
|
||||||
|
Hash: aum3.Hash(),
|
||||||
|
Change: aum3.MessageKind.String(),
|
||||||
|
Raw: aum3.Serialize(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hash: aum2.Hash(),
|
||||||
|
Change: aum2.MessageKind.String(),
|
||||||
|
Raw: aum2.Serialize(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hash: aum1.Hash(),
|
||||||
|
Change: aum1.MessageKind.String(),
|
||||||
|
Raw: aum1.Serialize(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("human-readable", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var outBuf bytes.Buffer
|
||||||
|
json := jsonoutput.JSONSchemaVersion{}
|
||||||
|
useColor := false
|
||||||
|
|
||||||
|
printNetworkLockLog(updates, &outBuf, json, useColor)
|
||||||
|
|
||||||
|
t.Logf("%s", outBuf.String())
|
||||||
|
|
||||||
|
want := `update 4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA (checkpoint)
|
||||||
|
Disablement values:
|
||||||
|
- 010203
|
||||||
|
- 040506
|
||||||
|
- 070809
|
||||||
|
Keys:
|
||||||
|
Type: 25519
|
||||||
|
KeyID: tlpub:0101
|
||||||
|
Metadata: map[de:eins en:one es:uno]
|
||||||
|
|
||||||
|
update BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ (remove-key)
|
||||||
|
KeyID: tlpub:0303
|
||||||
|
|
||||||
|
update UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA (add-key)
|
||||||
|
Type: 25519
|
||||||
|
KeyID: tlpub:0202
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
if diff := cmp.Diff(outBuf.String(), want); diff != "" {
|
||||||
|
t.Fatalf("wrong output (-got, +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jsonV1 := `{
|
||||||
|
"SchemaVersion": "1",
|
||||||
|
"Messages": [
|
||||||
|
{
|
||||||
|
"Hash": "4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA",
|
||||||
|
"AUM": {
|
||||||
|
"MessageKind": "checkpoint",
|
||||||
|
"PrevAUMHash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
|
||||||
|
"State": {
|
||||||
|
"DisablementSecrets": [
|
||||||
|
"010203",
|
||||||
|
"040506",
|
||||||
|
"070809"
|
||||||
|
],
|
||||||
|
"Keys": [
|
||||||
|
{
|
||||||
|
"Kind": "25519",
|
||||||
|
"Votes": 1,
|
||||||
|
"Public": "tlpub:0101",
|
||||||
|
"Meta": {
|
||||||
|
"de": "eins",
|
||||||
|
"en": "one",
|
||||||
|
"es": "uno"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"StateID1": 0,
|
||||||
|
"StateID2": 0
|
||||||
|
},
|
||||||
|
"Votes": 1
|
||||||
|
},
|
||||||
|
"Raw": "pAEFAlggCqtbndUNv4_i-JrrVbGywbw5dNWNZYysEm02CCgf3q8FowH2AoNDAQIDQwQFBkMHCAkDgaQBAQIBA0IBAQyjYmRlZGVpbnNiZW5jb25lYmVzY3VubwYB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Hash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
|
||||||
|
"AUM": {
|
||||||
|
"MessageKind": "remove-key",
|
||||||
|
"PrevAUMHash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
|
||||||
|
"KeyID": "tlpub:0303",
|
||||||
|
"Meta": {
|
||||||
|
"de": "drei",
|
||||||
|
"en": "three",
|
||||||
|
"es": "tres"
|
||||||
|
},
|
||||||
|
"Signatures": [
|
||||||
|
{
|
||||||
|
"KeyID": "tlpub:0304",
|
||||||
|
"Signature": "BAU="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Raw": "pQECAlggopKFFOhcPaARv2QQU90-kWozQFAG3Hqja7Vez-_EZIAEQgMDB6NiZGVkZHJlaWJlbmV0aHJlZWJlc2R0cmVzF4GiAUIDBAJCBAU="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Hash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
|
||||||
|
"AUM": {
|
||||||
|
"MessageKind": "add-key",
|
||||||
|
"Key": {
|
||||||
|
"Kind": "25519",
|
||||||
|
"Votes": 1,
|
||||||
|
"Public": "tlpub:0202"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Raw": "owEBAvYDowEBAgEDQgIC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run("json-1", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Logf("BOOM")
|
||||||
|
|
||||||
|
var outBuf bytes.Buffer
|
||||||
|
json := jsonoutput.JSONSchemaVersion{
|
||||||
|
IsSet: true,
|
||||||
|
Value: 1,
|
||||||
|
}
|
||||||
|
useColor := false
|
||||||
|
|
||||||
|
printNetworkLockLog(updates, &outBuf, json, useColor)
|
||||||
|
|
||||||
|
want := jsonV1
|
||||||
|
t.Logf("%s", outBuf.String())
|
||||||
|
|
||||||
|
if diff := cmp.Diff(outBuf.String(), want); diff != "" {
|
||||||
|
t.Fatalf("wrong output (-got, +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -85,6 +85,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||||
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
||||||
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
||||||
|
tailscale.com/cmd/tailscale/cli/jsonoutput from tailscale.com/cmd/tailscale/cli
|
||||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||||
tailscale.com/control/controlhttp from tailscale.com/control/ts2021
|
tailscale.com/control/controlhttp from tailscale.com/control/ts2021
|
||||||
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
||||||
|
|||||||
13
tka/aum.go
13
tka/aum.go
@ -55,6 +55,17 @@ func (h AUMHash) IsZero() bool {
|
|||||||
return h == (AUMHash{})
|
return h == (AUMHash{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrevAUMHash represents the BLAKE2s digest of an Authority Update Message (AUM).
|
||||||
|
// Unlike an AUMHash, this can be empty if there is no previous AUM hash
|
||||||
|
// (which occurs in the genesis AUM).
|
||||||
|
type PrevAUMHash []byte
|
||||||
|
|
||||||
|
// String returns the PrevAUMHash encoded as base32.
|
||||||
|
// This is suitable for use as a filename, and for storing in text-preferred media.
|
||||||
|
func (h PrevAUMHash) String() string {
|
||||||
|
return base32StdNoPad.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
// AUMKind describes valid AUM types.
|
// AUMKind describes valid AUM types.
|
||||||
type AUMKind uint8
|
type AUMKind uint8
|
||||||
|
|
||||||
@ -120,7 +131,7 @@ func (k AUMKind) String() string {
|
|||||||
// - No floats!
|
// - No floats!
|
||||||
type AUM struct {
|
type AUM struct {
|
||||||
MessageKind AUMKind `cbor:"1,keyasint"`
|
MessageKind AUMKind `cbor:"1,keyasint"`
|
||||||
PrevAUMHash []byte `cbor:"2,keyasint"`
|
PrevAUMHash PrevAUMHash `cbor:"2,keyasint"`
|
||||||
|
|
||||||
// Key encodes a public key to be added to the key authority.
|
// Key encodes a public key to be added to the key authority.
|
||||||
// This field is used for AddKey AUMs.
|
// This field is used for AddKey AUMs.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user