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