aerotun_handlers_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package api
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. "proxmox-router/internal/aerotun"
  11. "proxmox-router/internal/auth"
  12. "proxmox-router/internal/vpn"
  13. )
  14. func aeroRequest(method, path, body string, role auth.Role) *http.Request {
  15. r := httptest.NewRequest(method, path, strings.NewReader(body))
  16. return r.WithContext(context.WithValue(r.Context(), sessionContextKey, &auth.Session{Role: role}))
  17. }
  18. func TestAeroTUNSecretRedactionAndEdit(t *testing.T) {
  19. m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json"))
  20. defer m.StopAll()
  21. key := strings.Repeat("12", 32)
  22. saved, err := m.Save(aerotun.TunnelInstance{Name: "atest", Mode: "server", LocalAddress: "10.99.0.1/24", PSK: key, Disabled: true})
  23. if err != nil {
  24. t.Fatal(err)
  25. }
  26. s := &Server{vpnMgr: &vpn.Manager{AeroTUN: m}}
  27. w := httptest.NewRecorder()
  28. s.handleListAeroTUNTunnels(w, aeroRequest("GET", "/api/vpn/aerotun/tunnels", "", auth.RoleRead))
  29. if strings.Contains(w.Body.String(), key) || strings.Contains(w.Body.String(), `"psk":`) {
  30. t.Fatal("list disclosed PSK")
  31. }
  32. var views []aerotun.TunnelInstance
  33. if err = json.Unmarshal(w.Body.Bytes(), &views); err != nil || !views[0].HasPSK {
  34. t.Fatal("missing safe key presence indicator", err)
  35. }
  36. saved.PSK = ""
  37. saved.Comment = "edited without revealing secret"
  38. body, _ := json.Marshal(saved)
  39. w = httptest.NewRecorder()
  40. s.handleSaveAeroTUNTunnel(w, aeroRequest("POST", "/api/vpn/aerotun/tunnels", string(body), auth.RoleFull))
  41. if w.Code != 200 || strings.Contains(w.Body.String(), key) {
  42. t.Fatalf("save failed or disclosed PSK: %d", w.Code)
  43. }
  44. got, _ := m.Get(saved.ID)
  45. if got.PSK != key {
  46. t.Fatal("redacted edit erased the stored key")
  47. }
  48. for _, role := range []auth.Role{auth.RoleRead, auth.RoleWrite} {
  49. w = httptest.NewRecorder()
  50. s.handleSaveAeroTUNTunnel(w, aeroRequest("POST", "/api/vpn/aerotun/tunnels", string(body), role))
  51. if w.Code != 403 {
  52. t.Fatal("non-administrator modified tunnel")
  53. }
  54. w = httptest.NewRecorder()
  55. s.handleGenerateAeroTUNKey(w, aeroRequest("GET", "/api/vpn/aerotun/genkey", "", role))
  56. if w.Code != 403 {
  57. t.Fatal("non-administrator generated key")
  58. }
  59. }
  60. }
  61. func TestAeroTUNPrivateIdentityRedacted(t *testing.T) {
  62. private, public, err := aerotun.GenerateIdentity()
  63. if err != nil {
  64. t.Fatal(err)
  65. }
  66. view := aerotun.TunnelInstance{PSK: strings.Repeat("ab", 32), PrivateKey: private, PublicKey: public}.PublicView()
  67. encoded, _ := json.Marshal(view)
  68. if strings.Contains(string(encoded), private) || !view.HasPrivateKey || view.PublicKey != public {
  69. t.Fatal("private identity exposed or public identity lost")
  70. }
  71. }
  72. func TestAeroTUNDraftIdentityIsSessionBoundAndSaved(t *testing.T) {
  73. m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json"))
  74. defer m.StopAll()
  75. s := &Server{vpnMgr: &vpn.Manager{AeroTUN: m}}
  76. request := func(method, path, body, session string, role auth.Role) *http.Request {
  77. r := httptest.NewRequest(method, path, strings.NewReader(body))
  78. return r.WithContext(context.WithValue(r.Context(), sessionContextKey, &auth.Session{Role: role, Token: session}))
  79. }
  80. w := httptest.NewRecorder()
  81. s.handleCreateAeroIdentityDraft(w, request("POST", "/", "", "owner", auth.RoleRead))
  82. if w.Code != http.StatusForbidden {
  83. t.Fatal("read-only user generated an identity")
  84. }
  85. w = httptest.NewRecorder()
  86. s.handleCreateAeroIdentityDraft(w, request("POST", "/", "", "owner", auth.RoleFull))
  87. var draft struct {
  88. ID string `json:"draft_id"`
  89. Public string `json:"public_key"`
  90. }
  91. if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &draft) != nil || draft.ID == "" || draft.Public == "" || strings.Contains(w.Body.String(), "private") || w.Header().Get("Cache-Control") != "no-store" {
  92. t.Fatal("invalid or unsafe identity response")
  93. }
  94. payload := map[string]any{"identity_draft_id": draft.ID, "protocol_version": 3, "name": "atest", "mode": "server", "local_address": "10.99.0.1/24", "allowed_ips": []string{"10.99.0.2/32"}, "psk": strings.Repeat("12", 32), "disabled": true}
  95. body, _ := json.Marshal(payload)
  96. w = httptest.NewRecorder()
  97. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "other", auth.RoleFull))
  98. if w.Code != 422 {
  99. t.Fatalf("other session used identity: %d", w.Code)
  100. }
  101. bad := make(map[string]any)
  102. for k, v := range payload {
  103. bad[k] = v
  104. }
  105. bad["local_address"] = "invalid"
  106. badBody, _ := json.Marshal(bad)
  107. w = httptest.NewRecorder()
  108. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(badBody), "owner", auth.RoleFull))
  109. if w.Code == 200 {
  110. t.Fatal("invalid draft configuration was saved")
  111. }
  112. w = httptest.NewRecorder()
  113. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "owner", auth.RoleFull))
  114. var saved aerotun.TunnelInstance
  115. if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &saved) != nil || saved.PublicKey != draft.Public || strings.Contains(w.Body.String(), `"private_key":`) {
  116. t.Fatalf("identity not preserved on save: %d", w.Code)
  117. }
  118. stored, ok := m.Get(saved.ID)
  119. if !ok || stored.PublicKey != draft.Public || stored.PrivateKey == "" {
  120. t.Fatal("identity was not stored")
  121. }
  122. w = httptest.NewRecorder()
  123. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "owner", auth.RoleFull))
  124. if w.Code != 422 {
  125. t.Fatalf("identity draft reused: %d", w.Code)
  126. }
  127. }