mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-04 19:56:35 +02:00
ipn/ipnlocal,tailcfg: add /debug/tka c2n endpoint (#19198)
Updates tailscale/corp#35015 Signed-off-by: James Sanderson <jsanderson@tailscale.com>
This commit is contained in:
parent
ec86f0ff93
commit
ffae275d4d
@ -259,6 +259,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/clientupdate from tailscale.com/feature/clientupdate
|
||||
LW tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscale/cli/jsonoutput from tailscale.com/feature/tailnetlock
|
||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/cmd/tailscaled/tailscaledhooks from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
@ -307,6 +308,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD tailscale.com/feature/ssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
||||
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/tailnetlock from tailscale.com/feature/condregister
|
||||
L tailscale.com/feature/tap from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/tpm from tailscale.com/feature/condregister
|
||||
L 💣 tailscale.com/feature/tundevstats from tailscale.com/feature/condregister
|
||||
|
||||
8
feature/condregister/maybe_tailnetlock.go
Normal file
8
feature/condregister/maybe_tailnetlock.go
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
package condregister
|
||||
|
||||
import _ "tailscale.com/feature/tailnetlock"
|
||||
54
feature/tailnetlock/tailnetlock.go
Normal file
54
feature/tailnetlock/tailnetlock.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// package tailnetlock registers the tailnet lock debug C2N handler. In the
|
||||
// future, all tailnet lock code should move here.
|
||||
package tailnetlock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.Register("tailnetlock")
|
||||
ipnlocal.RegisterC2N("/debug/tka/log", handleC2NDebugTKALog)
|
||||
}
|
||||
|
||||
const defaultC2NLogLimit = 50
|
||||
const maxC2NLogLimit = 1000
|
||||
|
||||
func handleC2NDebugTKALog(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasDebug {
|
||||
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
logf := b.Logger()
|
||||
logf("c2n: %s %s received", r.Method, r.URL)
|
||||
|
||||
limit := defaultC2NLogLimit
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if parsed, err := strconv.Atoi(limitStr); err == nil {
|
||||
limit = min(parsed, maxC2NLogLimit)
|
||||
}
|
||||
}
|
||||
|
||||
updates, err := b.NetworkLockLog(limit)
|
||||
if ipnlocal.IsNetworkLockNotActive(err) {
|
||||
http.Error(w, "tailnet lock not active", http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to get tailnet lock log: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
jsonoutput.PrintNetworkLockLogJSONV1(w, updates)
|
||||
}
|
||||
146
feature/tailnetlock/tailnetlock_test.go
Normal file
146
feature/tailnetlock/tailnetlock_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailnetlock
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestHandleC2NDebugTKA(t *testing.T) {
|
||||
makeTKA := func(length int) (tka.CompactableChonk, *tka.Authority) {
|
||||
if length == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
signerKey := key.NewNLPrivate()
|
||||
key1 := tka.Key{Kind: tka.Key25519, Public: signerKey.Public().Verifier(), Votes: 2}
|
||||
|
||||
chonk := tka.ChonkMem()
|
||||
authority, _, err := tka.Create(chonk, tka.State{
|
||||
Keys: []tka.Key{key1},
|
||||
DisablementValues: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, signerKey)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
for range length - 1 {
|
||||
updater := authority.NewUpdater(signerKey)
|
||||
key2 := tka.Key{Kind: tka.Key25519, Public: key.NewNLPrivate().Public().Verifier(), Votes: 2}
|
||||
updater.AddKey(key2)
|
||||
aums := must.Get(updater.Finalize(chonk))
|
||||
must.Do(authority.Inform(chonk, aums))
|
||||
}
|
||||
|
||||
return chonk, authority
|
||||
}
|
||||
|
||||
bodyHead := func(body *bytes.Buffer) string {
|
||||
count := 0
|
||||
var sb strings.Builder
|
||||
for line := range strings.Lines(body.String()) {
|
||||
if count == 10 {
|
||||
sb.WriteString("...")
|
||||
break
|
||||
}
|
||||
sb.WriteString(line)
|
||||
count++
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// matches [jsonoutput.PrintNetworkLockLogJSONV1]
|
||||
type response struct {
|
||||
SchemaVersion string
|
||||
Messages []any
|
||||
}
|
||||
|
||||
t.Run("tailnet-lock-disabled", func(t *testing.T) {
|
||||
b := ipnlocal.LocalBackendWithTKAForTest(nil, nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/debug/tka/log", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
b.HandleC2NForTest(rec, req)
|
||||
|
||||
if rec.Code != 400 {
|
||||
t.Fatalf("got status code: %v, want: 400\nBody: %s", rec.Code, rec.Body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tailnet-lock-enabled", func(t *testing.T) {
|
||||
chonk, authority := makeTKA(2)
|
||||
b := ipnlocal.LocalBackendWithTKAForTest(chonk, authority)
|
||||
|
||||
req := httptest.NewRequest("GET", "/debug/tka/log", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
b.HandleC2NForTest(rec, req)
|
||||
|
||||
if rec.Code != 200 {
|
||||
t.Fatalf("got status code: %v, want: 200\nBody: %s", rec.Code, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
var got response
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("couldn't parse JSON: %v\nbody: %s", err, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
if len(got.Messages) != 2 {
|
||||
t.Fatalf("got %d items, want 2", len(got.Messages))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default-limit", func(t *testing.T) {
|
||||
chonk, authority := makeTKA(60)
|
||||
b := ipnlocal.LocalBackendWithTKAForTest(chonk, authority)
|
||||
|
||||
req := httptest.NewRequest("GET", "/debug/tka/log", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
b.HandleC2NForTest(rec, req)
|
||||
|
||||
if rec.Code != 200 {
|
||||
t.Fatalf("got status code: %v, want: 200\nBody: %s", rec.Code, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
var got response
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("couldn't parse JSON: %v\nbody: %s", err, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
if len(got.Messages) != 50 {
|
||||
t.Fatalf("got %d items, want 50", len(got.Messages))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("override-limit", func(t *testing.T) {
|
||||
chonk, authority := makeTKA(65)
|
||||
b := ipnlocal.LocalBackendWithTKAForTest(chonk, authority)
|
||||
|
||||
req := httptest.NewRequest("GET", "/debug/tka/log?limit=60", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
b.HandleC2NForTest(rec, req)
|
||||
|
||||
if rec.Code != 200 {
|
||||
t.Fatalf("got status code: %v, want: 200\nBody: %s", rec.Code, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
var got response
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("couldn't parse JSON: %v\nbody: %s", err, bodyHead(rec.Body))
|
||||
}
|
||||
|
||||
if len(got.Messages) != 60 {
|
||||
t.Fatalf("got %d items, want 60", len(got.Messages))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"tailscale.com/util/goroutines"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@ -323,3 +324,10 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// HandleC2NForTest calls [handleC2N], for use by feature/ packages that
|
||||
// register C2N handlers and want to test them.
|
||||
func (b *LocalBackend) HandleC2NForTest(w http.ResponseWriter, r *http.Request) {
|
||||
testenv.AssertInTest()
|
||||
b.handleC2N(w, r)
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import (
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
@ -38,6 +39,7 @@ import (
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
|
||||
@ -47,6 +49,13 @@ var (
|
||||
errNetworkLockNotActive = errors.New("tailnet-lock is not active")
|
||||
)
|
||||
|
||||
// IsNetworkLockNotActive reports whether the given error indicates that
|
||||
// network-lock is not active. Stop-gap for feature/tailnetlock to check this
|
||||
// until all of this is code is moved to the feature.
|
||||
func IsNetworkLockNotActive(err error) bool {
|
||||
return errors.Is(err, errNetworkLockNotActive)
|
||||
}
|
||||
|
||||
type tkaState struct {
|
||||
profile ipn.ProfileID
|
||||
authority *tka.Authority
|
||||
@ -702,6 +711,7 @@ func (b *LocalBackend) NetworkLockAllowed() bool {
|
||||
|
||||
// Only use is in tests.
|
||||
func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSignature, nodeKey key.NodePublic) error {
|
||||
testenv.AssertInTest()
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
@ -712,6 +722,7 @@ func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSi
|
||||
|
||||
// Only use is in tests.
|
||||
func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
|
||||
testenv.AssertInTest()
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
@ -1481,3 +1492,24 @@ func (b *LocalBackend) tkaReadAffectedSigs(ourNodeKey key.NodePublic, key tkatyp
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// LocalBackendWithTKAForTest creates a LocalBackend with an initialized TKA
|
||||
// state for testing tailnet lock from the feature/tailnetlock package. Will be
|
||||
// removed when tailnet lock is fully moved to its own package. Do not use this
|
||||
// from any other package.
|
||||
func LocalBackendWithTKAForTest(chonk tka.CompactableChonk, tka *tka.Authority) *LocalBackend {
|
||||
testenv.AssertInTest()
|
||||
|
||||
var state *tkaState
|
||||
if tka != nil {
|
||||
state = &tkaState{
|
||||
authority: tka,
|
||||
storage: chonk,
|
||||
}
|
||||
}
|
||||
return &LocalBackend{
|
||||
store: &mem.Store{},
|
||||
logf: logger.Discard,
|
||||
tka: state,
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +184,8 @@ type CapabilityVersion int
|
||||
// - 135: 2026-03-30: Client understands [NodeAttrCacheNetworkMaps]
|
||||
// - 136: 2026-04-09: Client understands [NodeAttrDisableLinuxCGNATDropRule]
|
||||
// - 137: 2026-04-15: Client handles 429 responses to /machine/register.
|
||||
const CurrentCapabilityVersion CapabilityVersion = 137
|
||||
// - 138: 2026-03-31: can handle C2N /debug/tka.
|
||||
const CurrentCapabilityVersion CapabilityVersion = 138
|
||||
|
||||
// ID is an integer ID for a user, node, or login allocated by the
|
||||
// control plane.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user