mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
244 lines
6.6 KiB
Go
244 lines
6.6 KiB
Go
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package ipnlocal
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"testing"
|
||
|
|
||
|
"tailscale.com/control/controlclient"
|
||
|
"tailscale.com/hostinfo"
|
||
|
"tailscale.com/tailcfg"
|
||
|
"tailscale.com/tka"
|
||
|
"tailscale.com/types/key"
|
||
|
"tailscale.com/types/netmap"
|
||
|
)
|
||
|
|
||
|
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||
|
hi := hostinfo.New()
|
||
|
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||
|
hi.NetInfo = &ni
|
||
|
|
||
|
k := key.NewMachine()
|
||
|
opts := controlclient.Options{
|
||
|
ServerURL: "https://example.com",
|
||
|
Hostinfo: hi,
|
||
|
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||
|
return k, nil
|
||
|
},
|
||
|
HTTPTestClient: c,
|
||
|
NoiseTestClient: c,
|
||
|
Status: func(controlclient.Status) {},
|
||
|
}
|
||
|
|
||
|
cc, err := controlclient.NewNoStart(opts)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return cc
|
||
|
}
|
||
|
|
||
|
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
|
||
|
// httptest plumbing, despite the domain being unused in the actual noise request transport.
|
||
|
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
|
||
|
ts := httptest.NewUnstartedServer(handler)
|
||
|
ts.StartTLS()
|
||
|
client := ts.Client()
|
||
|
client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||
|
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||
|
return (&net.Dialer{}).DialContext(ctx, network, ts.Listener.Addr().String())
|
||
|
}
|
||
|
return ts, client
|
||
|
}
|
||
|
|
||
|
func TestTKAEnablementFlow(t *testing.T) {
|
||
|
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||
|
|
||
|
// Make a fake TKA authority, getting a usable genesis AUM which
|
||
|
// our mock server can communicate.
|
||
|
nlPriv := key.NewNLPrivate()
|
||
|
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||
|
a1, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
||
|
Keys: []tka.Key{key},
|
||
|
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
||
|
}, nlPriv)
|
||
|
if err != nil {
|
||
|
t.Fatalf("tka.Create() failed: %v", err)
|
||
|
}
|
||
|
|
||
|
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
defer r.Body.Close()
|
||
|
switch r.URL.Path {
|
||
|
case "/machine/tka/bootstrap":
|
||
|
body := new(tailcfg.TKABootstrapRequest)
|
||
|
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if body.NodeID != 420 {
|
||
|
t.Errorf("bootstrap nodeID=%v, want 420", body.NodeID)
|
||
|
}
|
||
|
if body.Head != "" {
|
||
|
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
|
||
|
}
|
||
|
|
||
|
w.WriteHeader(200)
|
||
|
out := tailcfg.TKABootstrapResponse{
|
||
|
GenesisAUM: genesisAUM.Serialize(),
|
||
|
}
|
||
|
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||
|
w.WriteHeader(404)
|
||
|
}
|
||
|
}))
|
||
|
defer ts.Close()
|
||
|
temp := t.TempDir()
|
||
|
|
||
|
cc := fakeControlClient(t, client)
|
||
|
b := LocalBackend{
|
||
|
varRoot: temp,
|
||
|
cc: cc,
|
||
|
ccAuto: cc,
|
||
|
logf: t.Logf,
|
||
|
}
|
||
|
|
||
|
b.mu.Lock()
|
||
|
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||
|
SelfNode: &tailcfg.Node{ID: 420},
|
||
|
TKAEnabled: true,
|
||
|
TKAHead: tka.AUMHash{},
|
||
|
})
|
||
|
b.mu.Unlock()
|
||
|
if err != nil {
|
||
|
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||
|
}
|
||
|
if b.tka == nil {
|
||
|
t.Fatal("tka was not initialized")
|
||
|
}
|
||
|
if b.tka.authority.Head() != a1.Head() {
|
||
|
t.Errorf("authority.Head() = %x, want %x", b.tka.authority.Head(), a1.Head())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestTKADisablementFlow(t *testing.T) {
|
||
|
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||
|
temp := t.TempDir()
|
||
|
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||
|
|
||
|
// Make a fake TKA authority, to seed local state.
|
||
|
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||
|
nlPriv := key.NewNLPrivate()
|
||
|
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||
|
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
authority, _, err := tka.Create(chonk, tka.State{
|
||
|
Keys: []tka.Key{key},
|
||
|
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||
|
}, nlPriv)
|
||
|
if err != nil {
|
||
|
t.Fatalf("tka.Create() failed: %v", err)
|
||
|
}
|
||
|
|
||
|
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
defer r.Body.Close()
|
||
|
switch r.URL.Path {
|
||
|
case "/machine/tka/bootstrap":
|
||
|
body := new(tailcfg.TKABootstrapRequest)
|
||
|
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
var disablement []byte
|
||
|
switch body.NodeID {
|
||
|
case 42:
|
||
|
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
|
||
|
case 420:
|
||
|
disablement = disablementSecret
|
||
|
default:
|
||
|
t.Errorf("bootstrap nodeID=%v, wanted 42 or 420", body.NodeID)
|
||
|
}
|
||
|
var head tka.AUMHash
|
||
|
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
|
||
|
t.Fatalf("failed unmarshal of body.Head: %v", err)
|
||
|
}
|
||
|
if head != authority.Head() {
|
||
|
t.Errorf("reported head = %x, want %x", head, authority.Head())
|
||
|
}
|
||
|
|
||
|
w.WriteHeader(200)
|
||
|
out := tailcfg.TKABootstrapResponse{
|
||
|
DisablementSecret: disablement,
|
||
|
}
|
||
|
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||
|
w.WriteHeader(404)
|
||
|
}
|
||
|
}))
|
||
|
defer ts.Close()
|
||
|
|
||
|
cc := fakeControlClient(t, client)
|
||
|
b := LocalBackend{
|
||
|
varRoot: temp,
|
||
|
cc: cc,
|
||
|
ccAuto: cc,
|
||
|
logf: t.Logf,
|
||
|
tka: &tkaState{
|
||
|
authority: authority,
|
||
|
storage: chonk,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// Test that the wrong disablement secret does not shut down the authority.
|
||
|
// NodeID == 42 indicates this scenario to our mock server.
|
||
|
b.mu.Lock()
|
||
|
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||
|
SelfNode: &tailcfg.Node{ID: 42},
|
||
|
TKAEnabled: false,
|
||
|
TKAHead: authority.Head(),
|
||
|
})
|
||
|
b.mu.Unlock()
|
||
|
if err != nil {
|
||
|
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||
|
}
|
||
|
if b.tka == nil {
|
||
|
t.Error("TKA was disabled despite incorrect disablement secret")
|
||
|
}
|
||
|
|
||
|
// Test the correct disablement secret shuts down the authority.
|
||
|
// NodeID == 420 indicates this scenario to our mock server.
|
||
|
b.mu.Lock()
|
||
|
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||
|
SelfNode: &tailcfg.Node{ID: 420},
|
||
|
TKAEnabled: false,
|
||
|
TKAHead: authority.Head(),
|
||
|
})
|
||
|
b.mu.Unlock()
|
||
|
if err != nil {
|
||
|
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||
|
}
|
||
|
|
||
|
if b.tka != nil {
|
||
|
t.Fatal("tka was not shut down")
|
||
|
}
|
||
|
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
|
||
|
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
|
||
|
}
|
||
|
}
|