mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
cmd/tailscale,ipn: implement lock log command
This commit implements `tailscale lock log [--limit N]`, which displays an ordered list of changes to network-lock state in a manner familiar to `git log`. Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
ed1fae6c73
commit
6708f9a93f
@ -829,6 +829,17 @@ type signRequest struct {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||||
|
func (lc *LocalClient) 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)
|
||||||
|
}
|
||||||
|
|
||||||
// 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 *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||||
|
@ -7,12 +7,18 @@
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tka"
|
"tailscale.com/tka"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
)
|
)
|
||||||
@ -29,6 +35,7 @@
|
|||||||
nlSignCmd,
|
nlSignCmd,
|
||||||
nlDisableCmd,
|
nlDisableCmd,
|
||||||
nlDisablementKDFCmd,
|
nlDisablementKDFCmd,
|
||||||
|
nlLogCmd,
|
||||||
},
|
},
|
||||||
Exec: runNetworkLockStatus,
|
Exec: runNetworkLockStatus,
|
||||||
}
|
}
|
||||||
@ -282,3 +289,100 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
|||||||
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
|
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nlLogArgs struct {
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
var nlLogCmd = &ffcli.Command{
|
||||||
|
Name: "log",
|
||||||
|
ShortUsage: "log [--limit N]",
|
||||||
|
ShortHelp: "List changes applied to network-lock",
|
||||||
|
Exec: runNetworkLockLog,
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("lock log")
|
||||||
|
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, error) {
|
||||||
|
terminalYellow := ""
|
||||||
|
terminalClear := ""
|
||||||
|
if color {
|
||||||
|
terminalYellow = "\x1b[33m"
|
||||||
|
terminalClear = "\x1b[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
var stanza strings.Builder
|
||||||
|
printKey := func(key *tka.Key, prefix string) {
|
||||||
|
fmt.Fprintf(&stanza, "%sType: %s\n", prefix, key.Kind.String())
|
||||||
|
fmt.Fprintf(&stanza, "%sKeyID: %x\n", prefix, key.ID())
|
||||||
|
fmt.Fprintf(&stanza, "%sVotes: %d\n", prefix, key.Votes)
|
||||||
|
if key.Meta != nil {
|
||||||
|
fmt.Fprintf(&stanza, "%sMetadata: %+v\n", prefix, key.Meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var aum tka.AUM
|
||||||
|
if err := aum.Unserialize(update.Raw); err != nil {
|
||||||
|
return "", fmt.Errorf("decoding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&stanza, "%supdate %x (%s)%s\n", terminalYellow, update.Hash, update.Change, terminalClear)
|
||||||
|
|
||||||
|
switch update.Change {
|
||||||
|
case tka.AUMAddKey.String():
|
||||||
|
printKey(aum.Key, "")
|
||||||
|
case tka.AUMRemoveKey.String():
|
||||||
|
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
|
||||||
|
|
||||||
|
case tka.AUMUpdateKey.String():
|
||||||
|
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
|
||||||
|
if aum.Votes != nil {
|
||||||
|
fmt.Fprintf(&stanza, "Votes: %d\n", aum.Votes)
|
||||||
|
}
|
||||||
|
if aum.Meta != nil {
|
||||||
|
fmt.Fprintf(&stanza, "Metadata: %+v\n", aum.Meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
case tka.AUMCheckpoint.String():
|
||||||
|
fmt.Fprintln(&stanza, "Disablement values:")
|
||||||
|
for _, v := range aum.State.DisablementSecrets {
|
||||||
|
fmt.Fprintf(&stanza, " - %x\n", v)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(&stanza, "Keys:")
|
||||||
|
for _, k := range aum.State.Keys {
|
||||||
|
printKey(&k, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Print a JSON encoding of the AUM as a fallback.
|
||||||
|
e := json.NewEncoder(&stanza)
|
||||||
|
e.SetIndent("", "\t")
|
||||||
|
if err := e.Encode(aum); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stanza.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return stanza.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||||
|
updates, err := localClient.NetworkLockLog(ctx, nlLogArgs.limit)
|
||||||
|
if err != nil {
|
||||||
|
return fixTailscaledConnectError(err)
|
||||||
|
}
|
||||||
|
useColor := isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
|
||||||
|
stdOut := colorable.NewColorableStdout()
|
||||||
|
for _, update := range updates {
|
||||||
|
stanza, err := nlDescribeUpdate(update, useColor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(stdOut, stanza)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -14,6 +14,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||||
|
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
|
||||||
|
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
|
||||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||||
|
4
go.mod
4
go.mod
@ -36,6 +36,8 @@ require (
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/klauspost/compress v1.15.4
|
github.com/klauspost/compress v1.15.4
|
||||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
|
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
|
||||||
|
github.com/mattn/go-colorable v0.1.12
|
||||||
|
github.com/mattn/go-isatty v0.0.14
|
||||||
github.com/mdlayher/genetlink v1.2.0
|
github.com/mdlayher/genetlink v1.2.0
|
||||||
github.com/mdlayher/netlink v1.6.0
|
github.com/mdlayher/netlink v1.6.0
|
||||||
github.com/mdlayher/sdnotify v1.0.0
|
github.com/mdlayher/sdnotify v1.0.0
|
||||||
@ -199,8 +201,6 @@ require (
|
|||||||
github.com/magiconair/properties v1.8.5 // indirect
|
github.com/magiconair/properties v1.8.5 // indirect
|
||||||
github.com/maratori/testpackage v1.0.1 // indirect
|
github.com/maratori/testpackage v1.0.1 // indirect
|
||||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||||
|
@ -647,6 +647,43 @@ func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetworkLockLog returns the changelog of TKA state up to maxEntries in size.
|
||||||
|
func (b *LocalBackend) NetworkLockLog(maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.tka == nil {
|
||||||
|
return nil, errNetworkLockNotActive
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []ipnstate.NetworkLockUpdate
|
||||||
|
cursor := b.tka.authority.Head()
|
||||||
|
for i := 0; i < maxEntries; i++ {
|
||||||
|
aum, err := b.tka.storage.AUM(cursor)
|
||||||
|
if err != nil {
|
||||||
|
if err == os.ErrNotExist {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return out, fmt.Errorf("reading AUM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := ipnstate.NetworkLockUpdate{
|
||||||
|
Hash: cursor,
|
||||||
|
Change: aum.MessageKind.String(),
|
||||||
|
Raw: aum.Serialize(),
|
||||||
|
}
|
||||||
|
out = append(out, update)
|
||||||
|
|
||||||
|
parent, hasParent := aum.Parent()
|
||||||
|
if !hasParent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -101,6 +101,16 @@ type NetworkLockStatus struct {
|
|||||||
TrustedKeys []TKAKey
|
TrustedKeys []TKAKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetworkLockUpdate describes a change to network-lock state.
|
||||||
|
type NetworkLockUpdate struct {
|
||||||
|
Hash [32]byte
|
||||||
|
Change string // values of tka.AUMKind.String()
|
||||||
|
|
||||||
|
// Raw contains the serialized AUM. The AUM is sent in serialized
|
||||||
|
// form to avoid transitive dependences bloating this package.
|
||||||
|
Raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
// TailnetStatus is information about a Tailscale network ("tailnet").
|
// TailnetStatus is information about a Tailscale network ("tailnet").
|
||||||
type TailnetStatus struct {
|
type TailnetStatus struct {
|
||||||
// Name is the name of the network that's currently in use.
|
// Name is the name of the network that's currently in use.
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||||
"status": (*Handler).serveStatus,
|
"status": (*Handler).serveStatus,
|
||||||
"tka/init": (*Handler).serveTKAInit,
|
"tka/init": (*Handler).serveTKAInit,
|
||||||
|
"tka/log": (*Handler).serveTKALog,
|
||||||
"tka/modify": (*Handler).serveTKAModify,
|
"tka/modify": (*Handler).serveTKAModify,
|
||||||
"tka/sign": (*Handler).serveTKASign,
|
"tka/sign": (*Handler).serveTKASign,
|
||||||
"tka/status": (*Handler).serveTKAStatus,
|
"tka/status": (*Handler).serveTKAStatus,
|
||||||
@ -1164,6 +1165,37 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
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", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user