mirror of https://github.com/tailscale/tailscale/
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.
602 lines
13 KiB
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))
|
|
}
|
|
})
|
|
}
|