aerotun_handlers_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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. "debtik/internal/aerotun"
  11. "debtik/internal/auth"
  12. "debtik/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 TestAeroTUNHealthAPIDoesNotDiscloseKeysAndRestrictsProbe(t *testing.T) {
  73. m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json"))
  74. defer m.StopAll()
  75. key := strings.Repeat("ab", 32)
  76. saved, err := m.Save(aerotun.TunnelInstance{Name: "health0", Mode: "server", LocalAddress: "10.99.0.1/24", PSK: key, Disabled: true})
  77. if err != nil {
  78. t.Fatal(err)
  79. }
  80. s := &Server{vpnMgr: &vpn.Manager{AeroTUN: m}}
  81. w := httptest.NewRecorder()
  82. s.handleAeroTUNHealth(w, aeroRequest("GET", "/", "", auth.RoleRead))
  83. if w.Code != 200 || strings.Contains(w.Body.String(), key) || strings.Contains(w.Body.String(), `"private_key"`) {
  84. t.Fatalf("unsafe health response: %d", w.Code)
  85. }
  86. r := aeroRequest("POST", "/", "", auth.RoleRead)
  87. r.SetPathValue("id", saved.ID)
  88. w = httptest.NewRecorder()
  89. s.handleAeroTUNHealthCheck(w, r)
  90. if w.Code != http.StatusForbidden {
  91. t.Fatalf("read-only user triggered probe: %d", w.Code)
  92. }
  93. r = aeroRequest("POST", "/", "", auth.RoleFull)
  94. r.SetPathValue("id", saved.ID)
  95. w = httptest.NewRecorder()
  96. s.handleAeroTUNHealthCheck(w, r)
  97. if w.Code != http.StatusUnprocessableEntity {
  98. t.Fatalf("disabled tunnel probe accepted: %d", w.Code)
  99. }
  100. }
  101. func TestAeroTUNDraftIdentityIsSessionBoundAndSaved(t *testing.T) {
  102. m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json"))
  103. defer m.StopAll()
  104. s := &Server{vpnMgr: &vpn.Manager{AeroTUN: m}}
  105. request := func(method, path, body, session string, role auth.Role) *http.Request {
  106. r := httptest.NewRequest(method, path, strings.NewReader(body))
  107. return r.WithContext(context.WithValue(r.Context(), sessionContextKey, &auth.Session{Role: role, Token: session}))
  108. }
  109. w := httptest.NewRecorder()
  110. s.handleCreateAeroIdentityDraft(w, request("POST", "/", "", "owner", auth.RoleRead))
  111. if w.Code != http.StatusForbidden {
  112. t.Fatal("read-only user generated an identity")
  113. }
  114. w = httptest.NewRecorder()
  115. s.handleCreateAeroIdentityDraft(w, request("POST", "/", "", "owner", auth.RoleFull))
  116. var draft struct {
  117. ID string `json:"draft_id"`
  118. Public string `json:"public_key"`
  119. }
  120. 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" {
  121. t.Fatal("invalid or unsafe identity response")
  122. }
  123. 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}
  124. body, _ := json.Marshal(payload)
  125. w = httptest.NewRecorder()
  126. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "other", auth.RoleFull))
  127. if w.Code != 422 {
  128. t.Fatalf("other session used identity: %d", w.Code)
  129. }
  130. bad := make(map[string]any)
  131. for k, v := range payload {
  132. bad[k] = v
  133. }
  134. bad["local_address"] = "invalid"
  135. badBody, _ := json.Marshal(bad)
  136. w = httptest.NewRecorder()
  137. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(badBody), "owner", auth.RoleFull))
  138. if w.Code == 200 {
  139. t.Fatal("invalid draft configuration was saved")
  140. }
  141. w = httptest.NewRecorder()
  142. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "owner", auth.RoleFull))
  143. var saved aerotun.TunnelInstance
  144. if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &saved) != nil || saved.PublicKey != draft.Public || strings.Contains(w.Body.String(), `"private_key":`) {
  145. t.Fatalf("identity not preserved on save: %d", w.Code)
  146. }
  147. stored, ok := m.Get(saved.ID)
  148. if !ok || stored.PublicKey != draft.Public || stored.PrivateKey == "" {
  149. t.Fatal("identity was not stored")
  150. }
  151. w = httptest.NewRecorder()
  152. s.handleSaveAeroTUNTunnel(w, request("POST", "/", string(body), "owner", auth.RoleFull))
  153. if w.Code != 422 {
  154. t.Fatalf("identity draft reused: %d", w.Code)
  155. }
  156. }