You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/client/local/tailnetlock_test.go

602 lines
13 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package local
import (
"bytes"
"context"
"encoding/json"
"testing"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
// TestNetworkLockInit_RequestEncoding tests the JSON encoding of init requests
func TestNetworkLockInit_RequestEncoding(t *testing.T) {
type initRequest struct {
Keys []tka.Key
DisablementValues [][]byte
SupportDisablement []byte
}
tests := []struct {
name string
keys []tka.Key
disablementValues [][]byte
supportDisablement []byte
wantErr bool
}{
{
name: "empty_all",
keys: []tka.Key{},
disablementValues: [][]byte{},
supportDisablement: []byte{},
wantErr: false,
},
{
name: "with_disablement",
keys: []tka.Key{},
disablementValues: [][]byte{[]byte("secret1"), []byte("secret2")},
supportDisablement: []byte("support-data"),
wantErr: false,
},
{
name: "nil_slices",
keys: nil,
disablementValues: nil,
supportDisablement: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := initRequest{
Keys: tt.keys,
DisablementValues: tt.disablementValues,
SupportDisablement: tt.supportDisablement,
}
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(req)
if tt.wantErr && err == nil {
t.Error("expected error encoding request")
}
if !tt.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
if !tt.wantErr && b.Len() == 0 {
t.Error("encoded buffer should not be empty")
}
})
}
}
// TestNetworkLockWrapPreauthKey_RequestStructure tests the request format
func TestNetworkLockWrapPreauthKey_RequestStructure(t *testing.T) {
type wrapRequest struct {
TSKey string
TKAKey string
}
tests := []struct {
name string
tsKey string
tkaKey string
wantTSKey string
wantTKAKey string
}{
{
name: "simple_keys",
tsKey: "tskey-auth-xxxx",
tkaKey: "nlpriv:xxxxx",
wantTSKey: "tskey-auth-xxxx",
wantTKAKey: "nlpriv:xxxxx",
},
{
name: "empty_keys",
tsKey: "",
tkaKey: "",
wantTSKey: "",
wantTKAKey: "",
},
{
name: "long_keys",
tsKey: "tskey-auth-" + string(make([]byte, 100)),
tkaKey: "nlpriv:" + string(make([]byte, 100)),
wantTSKey: "tskey-auth-" + string(make([]byte, 100)),
wantTKAKey: "nlpriv:" + string(make([]byte, 100)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := wrapRequest{
TSKey: tt.tsKey,
TKAKey: tt.tkaKey,
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(req); err != nil {
t.Fatalf("encoding error: %v", err)
}
// Decode to verify
var decoded wrapRequest
if err := json.NewDecoder(&b).Decode(&decoded); err != nil {
t.Fatalf("decoding error: %v", err)
}
if decoded.TSKey != tt.wantTSKey {
t.Errorf("TSKey = %q, want %q", decoded.TSKey, tt.wantTSKey)
}
if decoded.TKAKey != tt.wantTKAKey {
t.Errorf("TKAKey = %q, want %q", decoded.TKAKey, tt.wantTKAKey)
}
})
}
}
// TestNetworkLockModify_RequestEncoding tests modify request structure
func TestNetworkLockModify_RequestEncoding(t *testing.T) {
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
tests := []struct {
name string
addKeys []tka.Key
removeKeys []tka.Key
wantAdd int
wantRemove int
}{
{
name: "add_only",
addKeys: []tka.Key{{}},
removeKeys: []tka.Key{},
wantAdd: 1,
wantRemove: 0,
},
{
name: "remove_only",
addKeys: []tka.Key{},
removeKeys: []tka.Key{{}, {}},
wantAdd: 0,
wantRemove: 2,
},
{
name: "add_and_remove",
addKeys: []tka.Key{{}, {}, {}},
removeKeys: []tka.Key{{}, {}},
wantAdd: 3,
wantRemove: 2,
},
{
name: "empty_both",
addKeys: []tka.Key{},
removeKeys: []tka.Key{},
wantAdd: 0,
wantRemove: 0,
},
{
name: "nil_slices",
addKeys: nil,
removeKeys: nil,
wantAdd: 0,
wantRemove: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := modifyRequest{
AddKeys: tt.addKeys,
RemoveKeys: tt.removeKeys,
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(req); err != nil {
t.Fatalf("encoding error: %v", err)
}
// Verify encoded data is valid JSON
var decoded modifyRequest
if err := json.NewDecoder(&b).Decode(&decoded); err != nil {
t.Fatalf("decoding error: %v", err)
}
gotAdd := len(decoded.AddKeys)
gotRemove := len(decoded.RemoveKeys)
if gotAdd != tt.wantAdd {
t.Errorf("AddKeys length = %d, want %d", gotAdd, tt.wantAdd)
}
if gotRemove != tt.wantRemove {
t.Errorf("RemoveKeys length = %d, want %d", gotRemove, tt.wantRemove)
}
})
}
}
// TestNetworkLockSign_RequestEncoding tests sign request structure
func TestNetworkLockSign_RequestEncoding(t *testing.T) {
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
tests := []struct {
name string
rotationPublic []byte
wantRotLen int
}{
{
name: "no_rotation",
rotationPublic: nil,
wantRotLen: 0,
},
{
name: "with_rotation",
rotationPublic: []byte("rotation-key-data"),
wantRotLen: 17,
},
{
name: "ed25519_size",
rotationPublic: make([]byte, 32), // ed25519 public key size
wantRotLen: 32,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := signRequest{
NodeKey: key.NodePublic{},
RotationPublic: tt.rotationPublic,
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(req); err != nil {
t.Fatalf("encoding error: %v", err)
}
// Verify it's valid JSON
var decoded signRequest
if err := json.NewDecoder(&b).Decode(&decoded); err != nil {
t.Fatalf("decoding error: %v", err)
}
if len(decoded.RotationPublic) != tt.wantRotLen {
t.Errorf("RotationPublic length = %d, want %d", len(decoded.RotationPublic), tt.wantRotLen)
}
})
}
}
// TestNetworkLockLog_URLFormatting tests log request URL parameters
func TestNetworkLockLog_URLFormatting(t *testing.T) {
tests := []struct {
name string
maxEntries int
wantQuery string
}{
{
name: "default_limit",
maxEntries: 50,
wantQuery: "limit=50",
},
{
name: "zero_limit",
maxEntries: 0,
wantQuery: "limit=0",
},
{
name: "large_limit",
maxEntries: 1000,
wantQuery: "limit=1000",
},
{
name: "negative_limit",
maxEntries: -1,
wantQuery: "limit=-1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test that the query parameter formats correctly
query := "limit=" + string([]byte{byte('0' + tt.maxEntries/10), byte('0' + tt.maxEntries%10)})
if tt.maxEntries >= 10 {
// For multi-digit numbers, just check the format exists
if tt.wantQuery == "" {
t.Error("wantQuery should not be empty")
}
}
})
}
}
// TestNetworkLockForceLocalDisable_EmptyJSON tests empty JSON payload
func TestNetworkLockForceLocalDisable_EmptyJSON(t *testing.T) {
// The endpoint expects an empty JSON stanza: {}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
t.Fatalf("encoding error: %v", err)
}
// Should produce "{}\n"
got := b.String()
if got != "{}\n" {
t.Errorf("encoded JSON = %q, want %q", got, "{}\n")
}
// Verify it's valid JSON
var decoded struct{}
if err := json.NewDecoder(&b).Decode(&decoded); err != nil {
t.Errorf("should be valid JSON: %v", err)
}
}
// TestNetworkLockVerifySigningDeeplink_RequestFormat tests deeplink verification
func TestNetworkLockVerifySigningDeeplink_RequestFormat(t *testing.T) {
tests := []struct {
name string
url string
wantURL string
}{
{
name: "standard_deeplink",
url: "https://login.tailscale.com/admin/machines/sign/...",
wantURL: "https://login.tailscale.com/admin/machines/sign/...",
},
{
name: "empty_url",
url: "",
wantURL: "",
},
{
name: "local_url",
url: "http://localhost/sign",
wantURL: "http://localhost/sign",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vr := struct {
URL string
}{tt.url}
// Verify it encodes correctly
data, err := json.Marshal(vr)
if err != nil {
t.Fatalf("marshal error: %v", err)
}
// Decode to verify
var decoded struct{ URL string }
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if decoded.URL != tt.wantURL {
t.Errorf("URL = %q, want %q", decoded.URL, tt.wantURL)
}
})
}
}
// TestNetworkLockGenRecoveryAUM_RequestFormat tests recovery AUM generation
func TestNetworkLockGenRecoveryAUM_RequestFormat(t *testing.T) {
tests := []struct {
name string
numKeys int
forkString string
}{
{
name: "single_key",
numKeys: 1,
forkString: "abc123",
},
{
name: "multiple_keys",
numKeys: 5,
forkString: "def456",
},
{
name: "no_keys",
numKeys: 0,
forkString: "ghi789",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keys := make([]tkatype.KeyID, tt.numKeys)
for i := range keys {
keys[i] = tkatype.KeyID([]byte{byte(i)})
}
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
}{keys, tt.forkString}
// Verify it encodes
data, err := json.Marshal(vr)
if err != nil {
t.Fatalf("marshal error: %v", err)
}
// Decode to verify
var decoded struct {
Keys []tkatype.KeyID
ForkFrom string
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if len(decoded.Keys) != tt.numKeys {
t.Errorf("Keys length = %d, want %d", len(decoded.Keys), tt.numKeys)
}
if decoded.ForkFrom != tt.forkString {
t.Errorf("ForkFrom = %q, want %q", decoded.ForkFrom, tt.forkString)
}
})
}
}
// TestNetworkLockAffectedSigs_KeyIDFormat tests keyID handling
func TestNetworkLockAffectedSigs_KeyIDFormat(t *testing.T) {
tests := []struct {
name string
keyID tkatype.KeyID
}{
{
name: "short_keyid",
keyID: tkatype.KeyID([]byte{1, 2, 3}),
},
{
name: "empty_keyid",
keyID: tkatype.KeyID([]byte{}),
},
{
name: "long_keyid",
keyID: tkatype.KeyID(make([]byte, 32)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test that KeyID can be used as bytes.Reader input
r := bytes.NewReader(tt.keyID)
data, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read error: %v", err)
}
if len(data) != len(tt.keyID) {
t.Errorf("read length = %d, want %d", len(data), len(tt.keyID))
}
})
}
}
// TestNetworkLockCosignRecoveryAUM_Serialization tests AUM serialization
func TestNetworkLockCosignRecoveryAUM_Serialization(t *testing.T) {
// Create a minimal AUM for testing
aum := tka.AUM{}
// Serialize
serialized := aum.Serialize()
// Should be able to create reader
r := bytes.NewReader(serialized)
if r.Len() == 0 {
t.Error("serialized AUM should not be empty")
}
// Should be readable
data, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read error: %v", err)
}
if len(data) != len(serialized) {
t.Errorf("read length = %d, want %d", len(data), len(serialized))
}
}
// TestNetworkLockDisable_SecretHandling tests secret byte handling
func TestNetworkLockDisable_SecretHandling(t *testing.T) {
tests := []struct {
name string
secret []byte
}{
{
name: "short_secret",
secret: []byte("secret123"),
},
{
name: "empty_secret",
secret: []byte{},
},
{
name: "nil_secret",
secret: nil,
},
{
name: "long_secret",
secret: make([]byte, 256),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test that secret can be used with bytes.NewReader
r := bytes.NewReader(tt.secret)
data, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read error: %v", err)
}
if len(data) != len(tt.secret) {
t.Errorf("read length = %d, want %d", len(data), len(tt.secret))
}
})
}
}
// TestDecodeJSON_NetworkLockTypes tests JSON decoding for various response types
func TestDecodeJSON_NetworkLockTypes(t *testing.T) {
t.Run("NetworkLockStatus", func(t *testing.T) {
status := &ipnstate.NetworkLockStatus{
Enabled: true,
}
data, err := json.Marshal(status)
if err != nil {
t.Fatalf("marshal error: %v", err)
}
var decoded ipnstate.NetworkLockStatus
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if decoded.Enabled != status.Enabled {
t.Errorf("Enabled = %v, want %v", decoded.Enabled, status.Enabled)
}
})
t.Run("NetworkLockUpdate_slice", func(t *testing.T) {
updates := []ipnstate.NetworkLockUpdate{
{},
{},
}
data, err := json.Marshal(updates)
if err != nil {
t.Fatalf("marshal error: %v", err)
}
var decoded []ipnstate.NetworkLockUpdate
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if len(decoded) != len(updates) {
t.Errorf("decoded length = %d, want %d", len(decoded), len(updates))
}
})
}