package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/cookiejar" "net/http/httptest" "net/netip" "os" "path/filepath" "strings" "testing" "time" ) func TestMapIPv6(t *testing.T) { got := mapIPv6(netip.MustParsePrefix("fd11:5ee:bad:c0de::/64"), netip.MustParseAddr("10.84.12.8")) if got.String() != "fd11:5ee:bad:c0de::a54:c08" { t.Fatalf("got %s", got) } } func testConfig(t *testing.T) *Config { t.Helper() c := &Config{} c.applyDefaults() c.Server.IPv4 = "10.84.12.0/24" if _, err := c.initServer(); err != nil { t.Fatal(err) } c.Server.Endpoint = "vpn.example.net" return c } func TestNextFreeIPv4(t *testing.T) { c := testConfig(t) ip, err := nextFreeIPv4(c) if err != nil || ip.String() != "10.84.12.2" { t.Fatalf("first free: %v %v", ip, err) } c.Peers = []Peer{{IPv4: "10.84.12.2"}, {IPv4: "10.84.12.4"}} ip, _ = nextFreeIPv4(c) if ip.String() != "10.84.12.3" { t.Fatalf("gap not reused: %v", ip) } c.Server.IPv4 = "10.84.12.0/30" // .1 server, .2 the only peer address c.Peers = []Peer{{IPv4: "10.84.12.2"}} if _, err := nextFreeIPv4(c); err == nil { t.Fatal("expected full network") } } func TestValidate(t *testing.T) { c := testConfig(t) key, _ := newPrivateKey() ok := Peer{ID: "a", Name: "phone", IPv4: "10.84.12.2", PublicKey: key.PublicKey().String()} c.Peers = []Peer{ok} if err := c.validate(); err != nil { t.Fatalf("valid config rejected: %v", err) } for name, mutate := range map[string]func(c *Config){ "duplicate name": func(c *Config) { p := ok p.ID, p.IPv4, p.PublicKey = "b", "10.84.12.3", "x" c.Peers = append(c.Peers, p) }, "duplicate ip": func(c *Config) { p := ok; p.ID, p.Name, p.PublicKey = "b", "other", "x"; c.Peers = append(c.Peers, p) }, "server address": func(c *Config) { c.Peers[0].IPv4 = "10.84.12.1" }, "outside network": func(c *Config) { c.Peers[0].IPv4 = "10.84.13.2" }, "bad name": func(c *Config) { c.Peers[0].Name = "has space" }, "digits only": func(c *Config) { c.Peers[0].Name = "1234" }, "bad dns": func(c *Config) { c.Peers[0].DNS = []string{"dns.example"} }, "bad port": func(c *Config) { c.Server.ListenPort = 70000 }, "unmasked net": func(c *Config) { c.Server.IPv4 = "10.84.12.5/24" }, } { cc := c.clone() mutate(cc) if err := cc.validate(); err == nil { t.Errorf("%s: expected an error", name) } } } func TestClientConfig(t *testing.T) { c := testConfig(t) c.Server.IPv6Enabled = true c.Server.ClientDefaults.Keepalive = 25 p := Peer{Name: "phone", IPv4: "10.84.12.8", PresharedKey: "psk="} out := clientConfig(c, &p, "priv=") for _, want := range []string{ "PrivateKey = priv=", "Address = 10.84.12.8/24,fd11:5ee:bad:c0de::a54:c08/64", "DNS = 9.9.9.9, 149.112.112.112", "PresharedKey = psk=", "Endpoint = vpn.example.net:51820", "AllowedIPs = 0.0.0.0/0, ::/0", "PersistentKeepalive = 25", "PublicKey = " + serverPublicKey(c), } { if !strings.Contains(out, want) { t.Errorf("config lacks %q:\n%s", want, out) } } if strings.Contains(out, "MTU") { t.Error("client config must not set an MTU") } zero := 0 p.Keepalive = &zero if strings.Contains(clientConfig(c, &p, ""), "PersistentKeepalive") { t.Error("keepalive override 0 should remove the line") } } func TestPassword(t *testing.T) { h, err := hashPassword("correct horse battery") if err != nil { t.Fatal(err) } if !verifyPassword(h, "correct horse battery") || verifyPassword(h, "wrong password!") { t.Fatal("password verification is wrong") } } func TestRenumber(t *testing.T) { c := testConfig(t) c.Peers = []Peer{{IPv4: "10.84.12.7"}} c.Server.IPv4 = "172.20.5.0/24" if err := renumberPeers(c, "10.84.12.0/24"); err != nil || c.Peers[0].IPv4 != "172.20.5.7" { t.Fatalf("renumber: %v %v", c.Peers[0].IPv4, err) } c.Server.IPv4 = "172.20.5.0/30" if err := renumberPeers(c, "172.20.5.0/24"); err == nil { t.Fatal("expected too-small error") } } func TestRandomSubnet(t *testing.T) { for range 50 { n, err := randomSubnet(24) if err != nil { t.Fatal(err) } if !n.Addr().IsPrivate() || overlapsAny(n, avoidedSubnets) { t.Fatalf("bad subnet %s", n) } } } // fakeKernel records applies and returns scripted counters. type fakeKernel struct { applied int samples []PeerSample } func (k *fakeKernel) Apply(*Config) error { k.applied++; return nil } func (k *fakeKernel) Sample(string) ([]PeerSample, error) { return k.samples, nil } func (k *fakeKernel) Checks(*Config) []Check { return []Check{{"fake", true, ""}} } func (k *fakeKernel) Uplink(*Config, bool) string { return "eth0" } func (k *fakeKernel) Down(*Config) error { return nil } func (k *fakeKernel) Close() error { return nil } func (k *fakeKernel) Ping(dsts []netip.Addr, _ time.Duration) (map[netip.Addr]time.Duration, error) { out := map[netip.Addr]time.Duration{} for _, d := range dsts { out[d] = 20 * time.Millisecond } return out, nil } func TestStatsDeltas(t *testing.T) { dir := t.TempDir() store, err := openStore(filepath.Join(dir, "config.json")) if err != nil { t.Fatal(err) } key, _ := newPrivateKey() pub := key.PublicKey().String() if err := store.Update(func(c *Config) error { c.Peers = append(c.Peers, Peer{ID: "p1", Name: "phone", IPv4: serverIPv4(netip.MustParsePrefix(c.Server.IPv4)).Next().String(), PublicKey: pub, Enabled: true}) return nil }); err != nil { t.Fatal(err) } k := &fakeKernel{} st, _ := openStats(filepath.Join(dir, "stats.json"), store, k) hs := time.Now() step := func(rx, tx int64) { k.samples = []PeerSample{{PublicKey: pub, RxBytes: rx, TxBytes: tx, LastHandshake: hs}} st.sample() } step(100, 1000) step(150, 1500) // +50 / +500 step(20, 30) // counter reset: counts 20 / 30 s := st.Summary("p1") if s.UpTotal != 170 || s.DownTotal != 1530 { t.Fatalf("totals: up %d down %d", s.UpTotal, s.DownTotal) } if s.Down24h != 1530 || !s.Online { t.Fatalf("24h %d online %v", s.Down24h, s.Online) } st.save() st2, _ := openStats(filepath.Join(dir, "stats.json"), store, k) if st2.Summary("p1").DownTotal != 1530 { t.Fatal("stats not persisted") } } // TestAPI runs a full flow over HTTP: login, create, list, patch, issue, // disable, delete, tokens. func TestAPI(t *testing.T) { dir := t.TempDir() store, err := openStore(filepath.Join(dir, "config.json")) if err != nil { t.Fatal(err) } hash, _ := hashPassword("a long test password") _ = store.Update(func(c *Config) error { c.Users[0].PasswordHash = hash c.Server.Endpoint = "vpn.example.net" return nil }) k := &fakeKernel{} st, _ := openStats(filepath.Join(dir, "stats.json"), store, k) app := &App{store: store, kernel: k, recon: newReconciler(k, store), stats: st, auth: newAuth(store), tls: &webTLS{}, logPath: filepath.Join(dir, "log.jsonl"), started: time.Now(), shutdown: func() {}} srv := httptest.NewServer(app.routes()) defer srv.Close() jar, _ := cookiejar.New(nil) cl := &http.Client{Jar: jar} call := func(method, path string, body any, want int) map[string]any { t.Helper() var rd io.Reader if body != nil { b, _ := json.Marshal(body) rd = bytes.NewReader(b) } req, _ := http.NewRequest(method, srv.URL+"/api/v1"+path, rd) req.Header.Set("Content-Type", "application/json") resp, err := cl.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var out map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) if resp.StatusCode != want { t.Fatalf("%s %s: status %d, want %d: %v", method, path, resp.StatusCode, want, out) } return out } call("GET", "/peers", nil, 401) call("POST", "/auth/login", map[string]string{"username": "admin", "password": "wrong"}, 401) call("POST", "/auth/login", map[string]string{"username": "admin", "password": "a long test password"}, 200) created := call("POST", "/peers", map[string]any{"name": "phone-alex", "note": "iPhone"}, 201) peer := created["peer"].(map[string]any) id := peer["id"].(string) if peer["ipv4"] != serverIPv4(netip.MustParsePrefix(store.Get().Server.IPv4)).Next().String() { t.Fatalf("unexpected address %v", peer["ipv4"]) } conf := created["config"].(string) if !strings.Contains(conf, "PrivateKey = ") || strings.Contains(conf, "127.0.0.1

") || h.Get("Server") != "" { t.Fatalf("soon decoy: %q", b) } set(func(c *Config) { c.Decoy.Page = "blank" }) if b, _ := get("/", 200); b != "" { t.Fatalf("blank decoy: %q", b) } if b, _ := get("/app.js", 404); b != "" { t.Fatalf("blank 404: %q", b) } set(func(c *Config) { c.Decoy.Page = "forbidden" }) if b, _ := get("/", 403); !strings.Contains(b, "Forbidden") { t.Fatalf("forbidden decoy: %q", b) } set(func(c *Config) { c.Decoy.Page = "private" }) if b, _ := get("/", 200); !strings.Contains(b, "Private server") { t.Fatalf("private decoy: %q", b) } if err := store.Update(func(c *Config) error { c.Decoy.Page = "iis"; return nil }); err == nil { t.Fatal("unknown decoy page accepted") } } func TestTOTPCode(t *testing.T) { // RFC 6238, appendix B (SHA-1), cut to 6 digits. key := []byte("12345678901234567890") for _, c := range []struct { unix int64 want string }{{59, "287082"}, {1111111109, "081804"}, {1234567890, "005924"}, {2000000000, "279037"}} { if got := totpCode(key, uint64(c.unix/30)); got != c.want { t.Errorf("time %d: %s, want %s", c.unix, got, c.want) } } secret := b32.EncodeToString(key) now := time.Unix(1111111109, 0) if _, ok := totpMatch(secret, "081 804", now); !ok { t.Error("code with a space refused") } if _, ok := totpMatch(secret, "081804", now.Add(90*time.Second)); ok { t.Error("code three steps late accepted") } } // TestMFA signs in with an authenticator code and a recovery code, and // checks the "require" switch and the admin reset. func TestMFA(t *testing.T) { dir := t.TempDir() store, err := openStore(filepath.Join(dir, "config.json")) if err != nil { t.Fatal(err) } hash, _ := hashPassword("a long test password") _ = store.Update(func(c *Config) error { c.Users[0].PasswordHash = hash; return nil }) k := &fakeKernel{} st, _ := openStats(filepath.Join(dir, "stats.json"), store, k) app := &App{store: store, kernel: k, recon: newReconciler(k, store), stats: st, auth: newAuth(store), tls: &webTLS{}, logPath: filepath.Join(dir, "log.jsonl"), started: time.Now(), shutdown: func() {}} srv := httptest.NewServer(app.routes()) defer srv.Close() client := func() func(method, path string, body any, want int) map[string]any { jar, _ := cookiejar.New(nil) cl := &http.Client{Jar: jar} return func(method, path string, body any, want int) map[string]any { t.Helper() var rd io.Reader if body != nil { b, _ := json.Marshal(body) rd = bytes.NewReader(b) } req, _ := http.NewRequest(method, srv.URL+"/api/v1"+path, rd) req.Header.Set("Content-Type", "application/json") resp, err := cl.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var out map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) if resp.StatusCode != want { t.Fatalf("%s %s: status %d, want %d: %v", method, path, resp.StatusCode, want, out) } return out } } login := map[string]string{"username": "admin", "password": "a long test password"} adm := client() adm("POST", "/auth/login", login, 200) if o := adm("GET", "/auth/options", nil, 200); o["passkeys"] != false { t.Fatalf("passkeys offered on an IP address: %v", o) } adm("POST", "/auth/mfa/keys/begin", nil, 400) // Turn on the authenticator app; the first method brings recovery codes. setup := adm("POST", "/auth/mfa/totp/setup", nil, 200) secret := setup["secret"].(string) if !strings.HasPrefix(setup["uri"].(string), "otpauth://totp/") || setup["qr"] == "" { t.Fatalf("setup: %v", setup) } adm("POST", "/auth/mfa/totp/confirm", map[string]string{"code": "000000"}, 400) key, _ := b32.DecodeString(secret) code := func(offset int) string { return totpCode(key, uint64(time.Now().Unix()/30)+uint64(offset)) } conf := adm("POST", "/auth/mfa/totp/confirm", map[string]string{"code": code(0)}, 200) codes := conf["recoveryCodes"].([]any) if len(codes) != recoveryCount { t.Fatalf("recovery codes: %v", conf) } if s := adm("GET", "/auth/mfa", nil, 200); s["totp"] != true || s["recoveryLeft"] != float64(recoveryCount) { t.Fatalf("status: %v", s) } // A password alone now gives a ticket, not a session. c := client() r := c("POST", "/auth/login", login, 200) ticket, _ := r["ticket"].(string) if r["mfa"] != true || ticket == "" { t.Fatalf("login without second step: %v", r) } c("GET", "/peers", nil, 401) c("POST", "/auth/login/totp", map[string]string{"ticket": ticket, "code": "123456"}, 401) c("POST", "/auth/login/totp", map[string]string{"ticket": ticket, "code": code(0)}, 401) // used during setup c("POST", "/auth/login/totp", map[string]string{"ticket": ticket, "code": code(1)}, 200) c("GET", "/peers", nil, 200) // A recovery code works once. c2 := client() ticket = c2("POST", "/auth/login", login, 200)["ticket"].(string) c2("POST", "/auth/login/recovery", map[string]string{"ticket": ticket, "code": strings.ToLower(codes[0].(string))}, 200) c3 := client() ticket = c3("POST", "/auth/login", login, 200)["ticket"].(string) c3("POST", "/auth/login/recovery", map[string]string{"ticket": ticket, "code": codes[0].(string)}, 401) c3("POST", "/auth/login/recovery", map[string]string{"ticket": ticket, "code": codes[1].(string)}, 200) // Required for everyone: a user without it can only set it up. adm("PATCH", "/settings", map[string]any{"signin": map[string]bool{"requireMfa": true}}, 200) u := adm("POST", "/users", map[string]any{"username": "eve", "password": "eve's password 1", "mustChangePassword": false}, 201)["user"].(map[string]any) e := client() e("POST", "/auth/login", map[string]string{"username": "eve", "password": "eve's password 1"}, 200) if me := e("GET", "/auth/me", nil, 200); me["mfaSetupRequired"] != true { t.Fatalf("me: %v", me) } e("GET", "/peers", nil, 403) e("GET", "/auth/mfa", nil, 200) // The last method cannot be removed while it is required. adm("DELETE", "/auth/mfa/totp", nil, 400) // An admin resets another user's two-step sign-in, not their own. _ = store.Update(func(c *Config) error { _, eu := c.userByID(u["id"].(string)) eu.MFA = &UserMFA{TOTPSecret: newTOTPSecret(), RecoveryCodes: []string{"x"}} return nil }) if l := adm("GET", "/users", nil, 200)["users"].([]any); l[1].(map[string]any)["mfa"].(map[string]any)["totp"] != true { t.Fatalf("users list: %v", l) } me := adm("GET", "/auth/me", nil, 200) adm("POST", "/users/"+me["id"].(string)+"/reset-mfa", nil, 400) adm("POST", "/users/"+u["id"].(string)+"/reset-mfa", nil, 200) if _, eu := store.Get().userByID(u["id"].(string)); eu.hasMFA() || len(eu.MFA.RecoveryCodes) != 0 { t.Fatal("reset left methods behind") } }