Issuing the token sends the hub its new peer, and the hub applies it on its own time; a bus dialled before then timed out naming the bus. The first tunnel now waits for a handshake with the hub, and says so in the tunnel's words when there is none (novox/hq ADR 0169).
140 lines
4.8 KiB
Go
140 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
)
|
|
|
|
// `key` makes the tunnel key once and prints its public half; asked again it prints the same one,
|
|
// because a token may already have been issued for it (novox/hq ADR 0169).
|
|
func TestKeyMakesTheTunnelKeyOnceAndKeepsIt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
first := captureStdout(t, func() {
|
|
if err := keyCommand(opts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
second := captureStdout(t, func() {
|
|
if err := keyCommand(opts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
if strings.TrimSpace(first) == "" || strings.TrimSpace(first) != strings.TrimSpace(second) {
|
|
t.Fatalf("the key changed between two asks: %q then %q", first, second)
|
|
}
|
|
info, err := os.Stat(identity.OverlayKeyPath(opts.state))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Errorf("the private half is readable beyond root: %v", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// A token through the tunnel takes the key it was issued for, and says so when this machine has none
|
|
// or another.
|
|
func TestATokenThroughTheTunnelTakesItsOwnKey(t *testing.T) {
|
|
dir := t.TempDir()
|
|
state := filepath.Join(dir, "state.json")
|
|
tt := &identity.TokenTunnel{Key: "x", Address: "10.42.0.9/32", Range: "10.42.0.0/16", HubKey: "h", HubEndpoint: "198.51.100.1:51820"}
|
|
if _, err := tunnelKeyFor(tt, state); err == nil || !strings.Contains(err.Error(), "nox-mesh-host key") {
|
|
t.Fatalf("a machine with no key was not told to make one: %v", err)
|
|
}
|
|
captureStdout(t, func() { _ = keyCommand(options{state: state}) })
|
|
if _, err := tunnelKeyFor(tt, state); err == nil || !strings.Contains(err.Error(), "issued for the tunnel key x") {
|
|
t.Fatalf("another machine's token was taken: %v", err)
|
|
}
|
|
mine, _ := identity.LoadOverlayKey(identity.OverlayKeyPath(state))
|
|
tt.Key = mine.Public
|
|
if got, err := tunnelKeyFor(tt, state); err != nil || got.Public != mine.Public {
|
|
t.Fatalf("this machine's own token was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// The first tunnel is the mesh's interface and unit, with the hub as its one peer and no secret in
|
|
// the file — the same shape the mesh's declaration replaces it with.
|
|
func TestTheFirstTunnelIsTheMeshsInterfaceWithTheHubAsItsPeer(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tunnelConfigPath = filepath.Join(dir, "wireguard", "mesh0.conf")
|
|
lookPath = func(string) (string, error) { return "/usr/bin/wg-quick", nil }
|
|
t.Cleanup(func() { tunnelConfigPath = "/etc/wireguard/mesh0.conf" })
|
|
var ran []string
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
if name == "wg" {
|
|
return "HUBKEY\t1759400000\n", nil
|
|
}
|
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
|
return "", nil
|
|
}
|
|
tt := &identity.TokenTunnel{Key: "k", Address: "10.42.0.9/32", Range: "10.42.0.0/16", HubKey: "HUBKEY", HubEndpoint: "198.51.100.1:51820"}
|
|
captureStdout(t, func() {
|
|
if err := bringTheTunnelUp(context.Background(), tt, "/var/lib/mesh-host/overlay.key", run); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
raw, err := os.ReadFile(tunnelConfigPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := string(raw)
|
|
for _, want := range []string{"Address = 10.42.0.9/32", "PostUp = wg set %i private-key /var/lib/mesh-host/overlay.key",
|
|
"PublicKey = HUBKEY", "Endpoint = 198.51.100.1:51820", "AllowedIPs = 10.42.0.0/16", "PersistentKeepalive = 25"} {
|
|
if !strings.Contains(conf, want) {
|
|
t.Errorf("the first tunnel lacks %q:\n%s", want, conf)
|
|
}
|
|
}
|
|
if strings.Contains(conf, "PrivateKey") {
|
|
t.Error("the first tunnel's file holds the private key")
|
|
}
|
|
if len(ran) != 1 || ran[0] != "systemctl restart wg-quick@mesh0" {
|
|
t.Errorf("the tunnel was started as %v", ran)
|
|
}
|
|
}
|
|
|
|
// captureStdout is what fn printed to standard output.
|
|
func captureStdout(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
was := os.Stdout
|
|
os.Stdout = w
|
|
fn()
|
|
os.Stdout = was
|
|
w.Close()
|
|
out, _ := io.ReadAll(r)
|
|
return string(out)
|
|
}
|
|
|
|
// The bus is dialled only once the hub has answered the tunnel, and a hub that never does is said
|
|
// as the tunnel's fault rather than the bus's.
|
|
func TestTheBusWaitsForTheHubToAnswer(t *testing.T) {
|
|
asked := 0
|
|
answersOnThird := func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked++
|
|
if asked < 3 {
|
|
return "HUBKEY\t0\n", nil
|
|
}
|
|
return "HUBKEY\t1759400000\n", nil
|
|
}
|
|
captureStdout(t, func() {
|
|
if err := waitForTheHub(context.Background(), answersOnThird, time.Minute); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
never := func(context.Context, string, ...string) (string, error) { return "HUBKEY\t0\n", nil }
|
|
err := waitForTheHub(context.Background(), never, 0)
|
|
if err == nil || !strings.Contains(err.Error(), "has not answered the tunnel") {
|
|
t.Fatalf("a hub that never answered was not said: %v", err)
|
|
}
|
|
}
|