Files
jochen b462f461c6 A taken tunnel's found configuration is retired once the take is proven (hq ADR 0119)
Kept on disk it was the take's fallback; once the mesh's interface is up in its place and a peer
has handshaken with it, it is an unmaintained way back onto the network, held for ever. It is now
removed from where its unit reads it, its kept original verified first and left as it is, and the
hold ends. Until proven — no handshake, or wg not answering — it is kept and the report says why.
The retirement is recorded apart from holds, so later applies, an undeclare, and a reassignment
find it retired rather than missing, and nothing writes it back.
2026-09-27 00:47:57 +02:00

243 lines
8.6 KiB
Go

package tunnel
import (
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"testing"
)
// novox/hq ADR 0105: the host reads the predecessor's tunnel — key, port, address and range, every
// peer — and the private key becomes the node's, never printed and never sent.
// aKey is a real WireGuard keypair, made here so a key that stopped being a key is caught.
func aKey(t *testing.T) (private, public string) {
t.Helper()
k, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
return base64.StdEncoding.EncodeToString(k.Bytes()),
base64.StdEncoding.EncodeToString(k.PublicKey().Bytes())
}
func aConfig(private string, peers ...string) string {
var b strings.Builder
fmt.Fprintf(&b, "# the predecessor's hub\n[Interface]\nPrivateKey = %s\nListenPort = 51900\n"+
"Address = 192.0.2.1/24\n", private)
for i, key := range peers {
fmt.Fprintf(&b, "\n[Peer]\nPublicKey = %s\nAllowedIPs = 192.0.2.%d/32\n", key, i+2)
}
return b.String()
}
func TestTheConfigurationIsReadWhole(t *testing.T) {
private, public := aKey(t)
_, peerA := aKey(t)
_, peerB := aKey(t)
found, err := Parse([]byte(aConfig(private, peerA, peerB) + "PersistentKeepalive = 25 ; a comment\n"))
if err != nil {
t.Fatal(err)
}
if found.Port != 51900 || found.Address != "192.0.2.1/24" || found.Range != "192.0.2.0/24" {
t.Errorf("port, address or range misread: %+v", found)
}
if found.PublicKey != public {
t.Errorf("the public key is not the one derived from the file's private key")
}
if found.PrivateKey() != private {
t.Error("the private key was not read")
}
if len(found.Peers) != 2 || found.Peers[0].PublicKey != peerA || found.Peers[0].Address != "192.0.2.2/32" ||
found.Peers[1].PublicKey != peerB || found.Peers[1].Address != "192.0.2.3/32" {
t.Errorf("the peers were misread: %+v", found.Peers)
}
}
func TestThePrivateKeyNeverPrintsAndNeverTravels(t *testing.T) {
private, _ := aKey(t)
found, err := Parse([]byte(aConfig(private)))
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(found)
if err != nil {
t.Fatal(err)
}
for what, said := range map[string]string{
"JSON": string(raw),
"String": found.String(),
"%v": fmt.Sprintf("%v", found),
"%+v": fmt.Sprintf("%+v", found),
"%#v via %v": fmt.Sprintf("%v", []Found{found}),
} {
if strings.Contains(said, private) {
t.Errorf("the private key appears in %s: %s", what, said)
}
}
if !strings.Contains(string(raw), found.PublicKey) {
t.Error("the public key does not travel, so the mesh could not know the tunnel's key")
}
}
func TestATunnelWithoutWhatTheMeshNeedsIsRefused(t *testing.T) {
private, _ := aKey(t)
_, peer := aKey(t)
for name, conf := range map[string]string{
"no key": "[Interface]\nListenPort = 51900\nAddress = 192.0.2.1/24\n",
"no address": fmt.Sprintf("[Interface]\nPrivateKey = %s\nListenPort = 51900\n", private),
"bare address": fmt.Sprintf("[Interface]\nPrivateKey = %s\nListenPort = 51900\nAddress = 192.0.2.1\n", private),
"no port": fmt.Sprintf("[Interface]\nPrivateKey = %s\nAddress = 192.0.2.1/24\n", private),
"peer no route": aConfig(private) + "\n[Peer]\nPublicKey = " + peer + "\n",
"peer no key": aConfig(private) + "\n[Peer]\nAllowedIPs = 192.0.2.9/32\n",
"not a key": "[Interface]\nPrivateKey = not-base64!\nListenPort = 1\nAddress = 192.0.2.1/24\n",
} {
if _, err := Parse([]byte(conf)); err == nil {
t.Errorf("%s was accepted", name)
}
}
}
// aMachine answers `wg show interfaces` and reads configurations from a map.
type aMachine struct {
up string
files map[string]string
asked []string
}
func (m *aMachine) run(_ context.Context, name string, args ...string) (string, error) {
m.asked = append(m.asked, name+" "+strings.Join(args, " "))
if name == "wg" && len(args) == 2 && args[0] == "show" && args[1] == "interfaces" {
return m.up, nil
}
return "", errors.New("unexpected: " + name)
}
func (m *aMachine) read(path string) ([]byte, error) {
if raw, ok := m.files[path]; ok {
return []byte(raw), nil
}
return nil, errors.New("no such file: " + path)
}
func TestTheOneTunnelUpBesidesTheMeshsIsFound(t *testing.T) {
private, public := aKey(t)
m := &aMachine{up: "mesh0 wg0\n", files: map[string]string{ConfigDir + "/wg0.conf": aConfig(private)}}
ReadFile = m.read
t.Cleanup(func() { ReadFile = os.ReadFile })
found, err := Find(context.Background(), m.run, "")
if err != nil {
t.Fatal(err)
}
if found.Interface != "wg0" || found.Unit != "wg-quick@wg0" || found.Config != ConfigDir+"/wg0.conf" ||
found.PublicKey != public {
t.Errorf("the wrong tunnel, or misnamed: %+v", found)
}
for _, asked := range m.asked {
if strings.HasPrefix(asked, "wg set") || strings.Contains(asked, "private-key") {
t.Errorf("finding a tunnel ran %q; reading is reading", asked)
}
}
}
func TestNoneUpIsAnOrdinaryAnswerAndSeveralIsAQuestion(t *testing.T) {
private, _ := aKey(t)
m := &aMachine{up: "mesh0\n", files: map[string]string{
ConfigDir + "/wg0.conf": aConfig(private), ConfigDir + "/wg1.conf": aConfig(private)}}
ReadFile = m.read
t.Cleanup(func() { ReadFile = os.ReadFile })
if _, err := Find(context.Background(), m.run, ""); !errors.Is(err, ErrNone) {
t.Errorf("a machine with only the mesh's interface up was not an ordinary none: %v", err)
}
m.up = "wg1 mesh0 wg0\n"
_, err := Find(context.Background(), m.run, "")
if !errors.Is(err, ErrSeveral) || !strings.Contains(err.Error(), "wg0, wg1") || strings.Contains(err.Error(), "mesh0") {
t.Errorf("two tunnels up were not refused naming both and only them: %v", err)
}
found, err := Find(context.Background(), m.run, "wg1")
if err != nil || found.Interface != "wg1" {
t.Errorf("naming one of two did not find it: %+v %v", found, err)
}
if _, err := Find(context.Background(), m.run, "wg9"); err == nil || !strings.Contains(err.Error(), "wg9") {
t.Errorf("naming a tunnel that is not up was not refused: %v", err)
}
}
func TestAFoundTunnelReadsItsMTU(t *testing.T) {
// A tuned path sets MTU in [Interface]; the mesh must carry it or the tunnel regresses to the
// default silently (novox/hq: a taken tunnel carries its MTU).
private, public := aKey(t)
withMTU := "# tuned\n[Interface]\nPrivateKey = " + private +
"\nListenPort = 51820\nAddress = 10.10.0.3/24\nMTU = 1380\n" +
"[Peer]\nPublicKey = " + public + "\nAllowedIPs = 10.10.0.1/32\n"
f, err := Parse([]byte(withMTU))
if err != nil {
t.Fatal(err)
}
if f.MTU != 1380 {
t.Fatalf("MTU 1380 was not read; got %d", f.MTU)
}
// And a config with none leaves MTU zero, so the mesh writes no MTU line.
f2, err := Parse([]byte(aConfig(private, public)))
if err != nil {
t.Fatal(err)
}
if f2.MTU != 0 {
t.Fatalf("a config with no MTU must leave it zero; got %d", f2.MTU)
}
}
// novox/hq ADR 0119: a take is proven by a handshake on the mesh's interface, read from `wg show
// <interface> latest-handshakes` — as wg prints it, a key and a Unix time per peer, zero for never.
func TestAHandshakeIsAPeerWithATime(t *testing.T) {
cases := map[string]struct {
out string
want int
}{
"two peers, one handshaken": {"PEER-A=\t1790000000\nPEER-B=\t0\n", 1},
"every peer handshaken": {"PEER-A=\t1790000000\nPEER-B=\t1790000042\n", 2},
"no peer ever": {"PEER-A=\t0\nPEER-B=\t0\n", 0},
"an interface with no peer": {"", 0},
"spaces, a trailing line": {"PEER-A= 1790000000\n\n", 1},
}
for name, c := range cases {
got, err := ParseHandshakes(c.out)
if err != nil || got != c.want {
t.Errorf("%s: %d peer(s) handshaken (%v), want %d", name, got, err, c.want)
}
}
// Output that is not a key and a time is not evidence of anything, and not a zero either.
for _, nonsense := range []string{"PEER-A=\n", "PEER-A=\tyesterday\n", "PEER-A=\t-1\n", "a b c\n"} {
if _, err := ParseHandshakes(nonsense); err == nil {
t.Errorf("%q was read as handshakes", nonsense)
}
}
}
func TestHandshakesThatCannotBeAskedAreAnErrorNotAZero(t *testing.T) {
var asked string
ok := func(_ context.Context, name string, args ...string) (string, error) {
asked = name + " " + strings.Join(args, " ")
return "PEER-A=\t1790000000\n", nil
}
if n, err := Handshaken(context.Background(), ok, "mesh0"); err != nil || n != 1 ||
asked != "wg show mesh0 latest-handshakes" {
t.Fatalf("asked %q and read %d (%v)", asked, n, err)
}
missing := func(context.Context, string, ...string) (string, error) {
return "", errors.New(`exec: "wg": executable file not found in $PATH`)
}
if _, err := Handshaken(context.Background(), missing, "mesh0"); err == nil ||
!strings.Contains(err.Error(), "mesh0") {
t.Fatalf("a machine with no wg was read as one with no handshake: %v", err)
}
}