Signed-off-by: Tom DNetto <tom@tailscale.com>main
parent
b9b0bf65a0
commit
e9b98dd2e1
@ -0,0 +1,243 @@ |
||||
// 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) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue