package apply import ( "crypto/ecdh" "crypto/rand" "encoding/base64" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // novox/hq ADR 0105: the host raises the mesh's interface with the found key and peers, stops the // found interface without flushing it, and keeps its configuration. // foundConf is the predecessor's configuration, with a real key made once per run: the key is // what the takeover must never print or copy, so it had better be one. var foundConf = func() string { k, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { panic(err) } return "[Interface]\nPrivateKey = " + base64.StdEncoding.EncodeToString(k.Bytes()) + "\n" + "ListenPort = 51900\nAddress = 192.0.2.1/24\n\n[Peer]\nPublicKey = PEER-A=\nAllowedIPs = 192.0.2.2/32\n" + "\n[Peer]\nPublicKey = PEER-B=\nAllowedIPs = 192.0.2.3/32\n" }() // aTakeover is the private network's declaration for an adopted hub whose interface takes over // the found tunnel: the mesh's configuration — with the found key set from the node's own key file // and the found peers in its list — and the interface's service naming what it replaces. func aTakeover(t *testing.T, config, mesh string) *declaration.Declaration { t.Helper() return adopted(t, `{"taken":[],"untaken":{"mesh-wireguard":["mesh-wireguard.overlay-config","mesh-wireguard.overlay-up"]}}`, `{"id":"mesh-wireguard.overlay-config","type":"file","path":"`+mesh+`","mode":"0600", "content":"[Interface]\nAddress = 192.0.2.1/32\nListenPort = 51900\nPostUp = wg set %i private-key /var/lib/mesh-host/overlay.key\n\n[Peer]\nPublicKey = PEER-A=\nAllowedIPs = 192.0.2.2/32\n"}, {"id":"mesh-wireguard.overlay-up","type":"service","unit":"wg-quick@mesh0","state":"running","boot":"enabled", "restart-on":["mesh-wireguard.overlay-config"], "takes-over":{"interface":"wg0","unit":"wg-quick@wg0","config":"`+config+`"}}`) } // aHubInUse is a machine with the predecessor's tunnel up and the mesh's not yet. func aHubInUse(t *testing.T) (dir, config, mesh string, m *machine) { t.Helper() dir = t.TempDir() config = filepath.Join(dir, "wg0.conf") mesh = filepath.Join(dir, "mesh0.conf") if err := os.WriteFile(config, []byte(foundConf), 0o600); err != nil { t.Fatal(err) } m = &machine{containers: map[string]*fakeContainer{}, units: map[string]*fakeUnit{ "wg-quick@wg0": {active: "active", enabled: "enabled"}, "wg-quick@mesh0": {active: "inactive", enabled: "disabled", fragment: "/usr/lib/systemd/system/wg-quick@.service"}, }} return dir, config, mesh, m } func TestTheFoundTunnelIsStoppedNeverFlushedAndItsConfigurationKept(t *testing.T) { dir, config, mesh, m := aHubInUse(t) report, state := applyAdopted(t, aTakeover(t, config, mesh), store.State{}, m, dir) // The found interface: its unit stopped and disabled, and nothing else done to it. if u := m.units["wg-quick@wg0"]; u.active != "inactive" || u.enabled != "disabled" { t.Fatalf("the found unit was not stopped and disabled: %+v", u) } for _, asked := range m.asked { if strings.HasPrefix(asked, "wg ") && !strings.HasPrefix(asked, "wg show interfaces") { t.Errorf("the found interface was touched with %q; it is stopped, never flushed", asked) } if strings.HasPrefix(asked, "wg-quick") || strings.Contains(asked, "peer remove") { t.Errorf("the found interface was flushed: %q", asked) } } // Its configuration: on disk as it was, its original kept, held for the module. if got, _ := os.ReadFile(config); string(got) != foundConf { t.Fatalf("the found configuration was changed:\n%s", got) } held, ok := state.HeldAt("mesh-wireguard.overlay-up.takes-over") if !ok || held.Kind != "file" || held.Target != config || held.Kept == "" || held.Module != "mesh-wireguard" { t.Fatalf("the found configuration is not held: %+v", held) } if kept, _ := os.ReadFile(held.Kept); string(kept) != foundConf { t.Fatalf("the original was not kept as found: %q", kept) } // The mesh's interface: up, enabled, with the found peers in the file the mesh wrote. if u := m.units["wg-quick@mesh0"]; u.active != "active" || u.enabled != "enabled" { t.Fatalf("the mesh's interface was not raised: %+v", u) } if got, _ := os.ReadFile(mesh); !strings.Contains(string(got), "PEER-A=") || strings.Contains(string(got), "PrivateKey") { t.Fatalf("the mesh's configuration does not carry the found peer, or carries a key:\n%s", got) } // And the report says so, with what was found — port, range, peers — and never the key. if report.Tunnel == nil || !report.Tunnel.Taken || report.Tunnel.Port != 51900 || report.Tunnel.Range != "192.0.2.0/24" || report.Tunnel.Peers != 2 || report.Tunnel.Kept != held.Kept { t.Fatalf("the report does not say what was carried: %+v", report.Tunnel) } private := strings.TrimSpace(strings.SplitN(strings.SplitN(foundConf, "PrivateKey = ", 2)[1], "\n", 2)[0]) for _, o := range report.Outcomes { if strings.Contains(o.Detail, private) { t.Errorf("the found key was printed in an outcome: %+v", o) } } if o := outcomeOf(report, "mesh-wireguard.overlay-up.takes-over"); o.Action != "held" || !strings.Contains(o.Detail, "stopped wg-quick@wg0") || !strings.Contains(o.Detail, "never flushed") { t.Errorf("the takeover was not reported as a hold that stopped the found unit: %+v", o) } if _, recorded := state.Find("mesh-wireguard.overlay-up.takes-over"); recorded { t.Error("the found configuration was recorded as applied, so it would be removed as an orphan") } } func TestATakeoverIsSteadyAndTheFoundUnitStaysDown(t *testing.T) { dir, config, mesh, m := aHubInUse(t) d := aTakeover(t, config, mesh) _, state := applyAdopted(t, d, store.State{}, m, dir) m.asked = nil report, again := applyAdopted(t, d, state, m, dir) if report.Changed() { t.Errorf("a second apply moved the machine: %+v", report.Outcomes) } if _, still := again.HeldAt("mesh-wireguard.overlay-up.takes-over"); !still { t.Error("the hold on the found configuration was forgotten while the service still declares it") } if m.did("systemctl stop wg-quick@wg0") { t.Error("a found unit already down was stopped again") } // Somebody starts the found unit again: it would take the port back, so it is stopped again // — the one thing on an adopted node the mesh undoes, because the tunnel is the mesh's now. m.units["wg-quick@wg0"].active = "active" m.asked = nil _, _ = applyAdopted(t, d, again, m, dir) if m.units["wg-quick@wg0"].active != "inactive" || !m.did("systemctl stop wg-quick@wg0") { t.Error("a found unit started again was left holding the mesh's port") } } func TestAFoundInterfaceStillUpAfterItsUnitStoppedRefusesTheTakeover(t *testing.T) { dir, config, mesh, m := aHubInUse(t) m.wgUp = "wg0 mesh0\n" _, state, err := ApplyKeeping(t.Context(), archHost(t), aTakeover(t, config, mesh), store.State{}, store.OriginDeclared, m.run, nil, nil, KeepIn(dir)) if err == nil || !strings.Contains(err.Error(), "still up") || !strings.Contains(err.Error(), "Nothing was flushed") { t.Fatalf("an interface something else raises was taken over anyway: %v", err) } if m.units["wg-quick@mesh0"].active == "active" { t.Error("the mesh's interface was started on a port the found one still holds") } if _, held := state.HeldAt("mesh-wireguard.overlay-up.takes-over"); !held { t.Error("the found configuration was not kept before the refusal") } } func TestATakeoverIsRefusedOnAConvergedDeclaration(t *testing.T) { _, err := declaration.Parse([]byte(`{"declaration":1,"resources":[ {"id":"up","type":"service","unit":"wg-quick@mesh0","state":"running", "takes-over":{"interface":"wg0","unit":"wg-quick@wg0","config":"/etc/wireguard/wg0.conf"}}]}`)) if err == nil || !strings.Contains(err.Error(), "adopted") { t.Fatalf("a takeover on a converged node was accepted: %v", err) } }