package main import ( "html" "net/http" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "time" ) func TestKeys(t *testing.T) { // RFC 7748's X25519 test vector: what a WireGuard public key is. pub, err := publicKey("dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=") if err != nil || pub != "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=" { t.Fatalf("public key = %q %v", pub, err) } priv, pub, err := newPrivateKey() if err != nil || !b64KeyRe.MatchString(priv) || !b64KeyRe.MatchString(pub) { t.Fatalf("new key: %q %q %v", priv, pub, err) } if again, _ := publicKey(priv); again != pub { t.Error("the public key does not belong to the private key") } if _, err := publicKey("short"); err == nil { t.Error("a bad key was accepted") } } func TestParseWGDump(t *testing.T) { dump := "wg-a\tPRIV\tPUB\t51820\toff\n" + "wg-a\tPEERKEY=\t(none)\t203.0.113.7:51820\t10.0.0.2/32\t1700000000\t1234\t5678\t25\n" + "wg-a\tOTHER=\t(none)\t(none)\t10.0.0.3/32\t0\t0\t0\toff\n" got := parseWGDump(dump)["wg-a"] if p := got["PEERKEY="]; p.RX != 1234 || p.TX != 5678 || p.Endpoint != "203.0.113.7:51820" || !p.Handshake.Equal(time.Unix(1700000000, 0)) { t.Errorf("peer = %+v", p) } if p := got["OTHER="]; !p.Handshake.IsZero() || p.Endpoint != "" { t.Errorf("idle peer = %+v", p) } } const peerKey = "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=" func TestApplyTunnel(t *testing.T) { sys := System{StateDir: t.TempDir()} s := mustParse(t) files, err := s.applyTunnel(sys, WGTunnel{ Name: "family", ListenPort: "51820", Addresses: "10.100.0.1/24, fd42:100::1/64", DNS: true, KillSwitch: true, Peers: []WGPeer{ {Name: "gran", PublicKey: peerKey, Allowed: "10.100.0.2/32", Keepalive: "25", NewPSK: true}, {}, // the empty add row }, }) if err != nil { t.Fatal(err) } if len(files) != 2 || files[0].Path != sys.wgKeyPath("family") || files[1].Path != sys.wgPSKPath("family", "gran") { t.Fatalf("files = %+v", files) } if err := writeSecrets(sys.wgDir(), files); err != nil { t.Fatal(err) } if st, err := os.Stat(sys.wgKeyPath("family")); err != nil || st.Mode().Perm() != 0o600 { t.Errorf("key file: %v %v", st, err) } tunnels := s.tunnels(sys) if len(tunnels) != 2 || tunnels[0].Name != "family" || !tunnels[0].KeyPresent || tunnels[0].PublicKey == "" || !tunnels[0].DNS || len(tunnels[0].Peers) != 1 || !tunnels[0].Peers[0].PSKSet { t.Fatalf("tunnels = %+v", tunnels) } // What else is in the settings survives (the sample has a wireguard "vpn" too). if got := str(s, "router", "wireguard", "vpn", "listenPort"); got != "51820" { t.Errorf("the other tunnel changed: %q", got) } out, _ := s.marshal() if strings.Contains(string(out), tunnels[0].PublicKey+"x") || strings.Contains(string(out), "PRIVATE") { t.Error("secrets in the settings") } // Editing keeps the key; removing the peer row removes the peer. before, _ := os.ReadFile(sys.wgKeyPath("family")) edit := tunnels[0] edit.Peers[0].Remove = true files, err = s.applyTunnel(sys, edit) if err != nil || len(files) != 0 { t.Fatalf("edit: %v %+v", err, files) } if after, _ := os.ReadFile(sys.wgKeyPath("family")); string(after) != string(before) { t.Error("the key changed") } if len(s.tunnels(sys)[0].Peers) != 0 { t.Error("peer not removed") } // A new key on request. edit.NewKey = true edit.Peers = nil files, _ = s.applyTunnel(sys, edit) if len(files) != 1 || files[0].Content == strings.TrimSpace(string(before)) { t.Errorf("no new key: %+v", files) } if !s.deleteTunnel("family") || s.deleteTunnel("family") { t.Error("delete") } } func TestApplyTunnelValidates(t *testing.T) { sys := System{StateDir: t.TempDir()} for name, tun := range map[string]WGTunnel{ "name": {Name: "Bad Name"}, "listenPort": {Name: "a", ListenPort: "70000"}, "addresses": {Name: "a", Addresses: "10.0.0.1"}, "privateKey": {Name: "a", PrivateKey: "nope"}, "peers[0].publicKey": {Name: "a", Peers: []WGPeer{{Name: "p", PublicKey: "x", Allowed: "10.0.0.2/32"}}}, "peers[0].allowed": {Name: "a", Peers: []WGPeer{{Name: "p", PublicKey: peerKey, Allowed: "10.0.0.2"}}}, "peers[0].endpoint": {Name: "a", Peers: []WGPeer{{Name: "p", PublicKey: peerKey, Endpoint: "host", Allowed: "10.0.0.2/32"}}}, "peers[1].publicKey": {Name: "a", Peers: []WGPeer{{Name: "p", PublicKey: peerKey}, {Name: "q", PublicKey: peerKey}}}, } { s := mustParse(t) _, err := s.applyTunnel(sys, tun) fe, ok := err.(FormErrors) if !ok || fe[name] == "" { t.Errorf("%s: %v", name, err) } } // A pasted key is stored; an existing tunnel without a key file is refused. s := mustParse(t) files, err := s.applyTunnel(sys, WGTunnel{Name: "pasted", PrivateKey: "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo="}) if err != nil || len(files) != 1 || files[0].Content != "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=" { t.Fatalf("pasted: %v %+v", err, files) } if _, err := mustParse(t).applyTunnel(sys, WGTunnel{Name: "vpn"}); err == nil { t.Error("an existing tunnel without a key file was accepted") } } func TestNextClientAddress(t *testing.T) { tun := WGTunnel{Addresses: "10.100.0.1/24", Peers: []WGPeer{{Allowed: "10.100.0.2/32"}, {Allowed: "10.100.0.3/32"}}} if got := nextClientAddress(tun); got != "10.100.0.4" { t.Errorf("next = %q", got) } if got := nextClientAddress(WGTunnel{Addresses: "fd42::1/64"}); got != "" { t.Errorf("no IPv4 network: %q", got) } } func TestWireGuardPage(t *testing.T) { a, sys := newTestApp(t) h := a.routes() cookie := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil).Result().Cookies()[0] token := func(path string) string { body := do(h, "GET", path, nil, cookie).Body.String() return regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1] } if body := do(h, "GET", "/wireguard", nil, cookie).Body.String(); !strings.Contains(body, "New tunnel") { t.Fatalf("no page:\n%s", body) } // A new tunnel gets its key; a bad one is refused with the form kept. form := url.Values{"csrf": {token("/wireguard")}, "name": {"family"}, "listenPort": {"51820"}, "addresses": {"10.100.0.1/24"}, "killSwitch": {"on"}, "peers": {"1"}, "peer.0.name": {"gran"}, "peer.0.publicKey": {"bad"}, "peer.0.allowed": {"10.100.0.2/32"}} rec := do(h, "POST", "/wireguard/save", form, cookie) if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `value="gran"`) { t.Fatalf("bad peer: %d", rec.Code) } if _, err := os.Stat(sys.wgKeyPath("family")); err == nil { t.Error("a key was written for a refused form") } form.Set("peer.0.publicKey", peerKey) if rec := do(h, "POST", "/wireguard/save", form, cookie); rec.Code != http.StatusSeeOther { t.Fatalf("save: %d\n%s", rec.Code, rec.Body) } if _, err := os.Stat(sys.wgKeyPath("family")); err != nil { t.Fatal("no key file") } s, _ := loadSettings(sys.settingsPath()) if str(s, "router", "wireguard", "family", "privateKeyFile") != sys.wgKeyPath("family") { t.Fatalf("settings: %v", s) } body := html.UnescapeString(do(h, "GET", "/wireguard", nil, cookie).Body.String()) pub := s.tunnels(sys)[0].PublicKey priv, _ := os.ReadFile(sys.wgKeyPath("family")) if !strings.Contains(body, pub) || strings.Contains(body, strings.TrimSpace(string(priv))) { t.Error("the page lacks the public key (or shows the private one)") } // A phone: the router makes its keys; the config is shown once. client := url.Values{"csrf": {token("/wireguard")}, "tunnel": {"family"}, "name": {"phone"}, "address": {"10.100.0.3"}, "endpoint": {"vpn.example.org:51820"}, "allowed": {"10.100.0.0/24, 192.168.10.0/24"}, "dns": {"10.100.0.1"}} rec = do(h, "POST", "/wireguard/client", client, cookie) if rec.Code != http.StatusOK { t.Fatalf("client: %d\n%s", rec.Code, rec.Body) } shown := html.UnescapeString(rec.Body.String()) for _, want := range []string{"[Interface]", "PrivateKey = ", "Address = 10.100.0.3/32", "DNS = 10.100.0.1", "PublicKey = " + pub, "PresharedKey = ", "Endpoint = vpn.example.org:51820"} { if !strings.Contains(shown, want) { t.Errorf("config lacks %q", want) } } s, _ = loadSettings(sys.settingsPath()) peer := s.tunnels(sys)[0].Peers if len(peer) != 2 || peer[1].Name != "phone" || !peer[1].PSKSet || peer[1].Allowed != "10.100.0.3/32" { t.Fatalf("peers = %+v", peer) } psk, _ := os.ReadFile(sys.wgPSKPath("family", "phone")) if !strings.Contains(html.UnescapeString(rec.Body.String()), strings.TrimSpace(string(psk))) { t.Error("the config's preshared key is not the stored one") } if st, _ := os.Stat(filepath.Join(sys.wgDir(), "family-phone.psk")); st.Mode().Perm() != 0o600 { t.Error("psk permissions") } if _, err := exec.LookPath("qrencode"); err == nil && !strings.Contains(rec.Body.String(), "