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) } }