handle-wireguard.go raw
1 package app
2
3 import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "net/http"
8
9 "next.orly.dev/pkg/nostr/encoders/bech32encoding"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 "next.orly.dev/pkg/nostr/httpauth"
12 "next.orly.dev/pkg/lol/chk"
13 "next.orly.dev/pkg/lol/log"
14
15 "next.orly.dev/pkg/acl"
16 "next.orly.dev/pkg/database"
17 )
18
19 // WireGuardConfigResponse is returned by the /api/wireguard/config endpoint.
20 type WireGuardConfigResponse struct {
21 ConfigText string `json:"config_text"`
22 Interface WGInterface `json:"interface"`
23 Peer WGPeer `json:"peer"`
24 }
25
26 // WGInterface represents the [Interface] section of a WireGuard config.
27 type WGInterface struct {
28 Address string `json:"address"`
29 PrivateKey string `json:"private_key"`
30 }
31
32 // WGPeer represents the [Peer] section of a WireGuard config.
33 type WGPeer struct {
34 PublicKey string `json:"public_key"`
35 Endpoint string `json:"endpoint"`
36 AllowedIPs string `json:"allowed_ips"`
37 }
38
39 // BunkerURLResponse is returned by the /api/bunker/url endpoint.
40 type BunkerURLResponse struct {
41 URL string `json:"url"`
42 RelayNpub string `json:"relay_npub"`
43 RelayPubkey string `json:"relay_pubkey"`
44 InternalIP string `json:"internal_ip"`
45 }
46
47 // handleWireGuardConfig returns the user's WireGuard configuration.
48 // Requires NIP-98 authentication and write+ access.
49 func (s *Server) handleWireGuardConfig(w http.ResponseWriter, r *http.Request) {
50 if r.Method != http.MethodGet {
51 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
52 return
53 }
54
55 // Check if WireGuard is enabled
56 if !s.Config.WGEnabled {
57 http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
58 return
59 }
60
61 // Check if ACL mode supports WireGuard
62 if s.Config.ACLMode == "none" {
63 http.Error(w, "WireGuard requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
64 return
65 }
66
67 // Validate NIP-98 authentication
68 valid, pubkey, err := httpauth.CheckAuth(r)
69 if chk.E(err) || !valid {
70 http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
71 return
72 }
73
74 // Check user has write+ access
75 accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
76 if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
77 http.Error(w, "Write access required for WireGuard", http.StatusForbidden)
78 return
79 }
80
81 // Type assert to Badger database for WireGuard methods
82 badgerDB, ok := s.DB.(*database.D)
83 if !ok {
84 http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
85 return
86 }
87
88 // Check subnet pool is available
89 if s.subnetPool == nil {
90 http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
91 return
92 }
93
94 // Get or create WireGuard peer for this user
95 peer, err := badgerDB.GetOrCreateWireGuardPeer(pubkey, s.subnetPool)
96 if chk.E(err) {
97 log.E.F("failed to get/create WireGuard peer: %v", err)
98 http.Error(w, "Failed to create WireGuard configuration", http.StatusInternalServerError)
99 return
100 }
101
102 // Derive subnet IPs from sequence
103 subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
104 clientIP := subnet.ClientIP.String()
105 serverIP := subnet.ServerIP.String()
106
107 // Get server public key
108 serverKey, err := badgerDB.GetOrCreateWireGuardServerKey()
109 if chk.E(err) {
110 log.E.F("failed to get WireGuard server key: %v", err)
111 http.Error(w, "WireGuard server not configured", http.StatusInternalServerError)
112 return
113 }
114
115 serverPubKey, err := deriveWGPublicKey(serverKey)
116 if chk.E(err) {
117 log.E.F("failed to derive server public key: %v", err)
118 http.Error(w, "WireGuard server error", http.StatusInternalServerError)
119 return
120 }
121
122 // Build endpoint
123 endpoint := fmt.Sprintf("%s:%d", s.Config.WGEndpoint, s.Config.WGPort)
124
125 // Build response
126 resp := WireGuardConfigResponse{
127 Interface: WGInterface{
128 Address: clientIP + "/32",
129 PrivateKey: base64.StdEncoding.EncodeToString(peer.WGPrivateKey),
130 },
131 Peer: WGPeer{
132 PublicKey: base64.StdEncoding.EncodeToString(serverPubKey),
133 Endpoint: endpoint,
134 AllowedIPs: serverIP + "/32", // Only route bunker traffic to this peer's server IP
135 },
136 }
137
138 // Generate config text
139 resp.ConfigText = fmt.Sprintf(`[Interface]
140 Address = %s
141 PrivateKey = %s
142
143 [Peer]
144 PublicKey = %s
145 Endpoint = %s
146 AllowedIPs = %s
147 PersistentKeepalive = 25
148 `, resp.Interface.Address, resp.Interface.PrivateKey,
149 resp.Peer.PublicKey, resp.Peer.Endpoint, resp.Peer.AllowedIPs)
150
151 // If WireGuard server is running, add the peer
152 if s.wireguardServer != nil && s.wireguardServer.IsRunning() {
153 if err := s.wireguardServer.AddPeer(pubkey, peer.WGPublicKey, clientIP); chk.E(err) {
154 log.W.F("failed to add peer to running WireGuard server: %v", err)
155 }
156 }
157
158 w.Header().Set("Content-Type", "application/json")
159 json.NewEncoder(w).Encode(resp)
160 }
161
162 // handleWireGuardRegenerate generates a new WireGuard keypair for the user.
163 // Requires NIP-98 authentication and write+ access.
164 func (s *Server) handleWireGuardRegenerate(w http.ResponseWriter, r *http.Request) {
165 if r.Method != http.MethodPost {
166 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
167 return
168 }
169
170 // Check if WireGuard is enabled
171 if !s.Config.WGEnabled {
172 http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
173 return
174 }
175
176 // Check if ACL mode supports WireGuard
177 if s.Config.ACLMode == "none" {
178 http.Error(w, "WireGuard requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
179 return
180 }
181
182 // Validate NIP-98 authentication
183 valid, pubkey, err := httpauth.CheckAuth(r)
184 if chk.E(err) || !valid {
185 http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
186 return
187 }
188
189 // Check user has write+ access
190 accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
191 if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
192 http.Error(w, "Write access required for WireGuard", http.StatusForbidden)
193 return
194 }
195
196 // Type assert to Badger database for WireGuard methods
197 badgerDB, ok := s.DB.(*database.D)
198 if !ok {
199 http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
200 return
201 }
202
203 // Check subnet pool is available
204 if s.subnetPool == nil {
205 http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
206 return
207 }
208
209 // Remove old peer from running server if exists
210 oldPeer, err := badgerDB.GetWireGuardPeer(pubkey)
211 if err == nil && oldPeer != nil && s.wireguardServer != nil && s.wireguardServer.IsRunning() {
212 s.wireguardServer.RemovePeer(oldPeer.WGPublicKey)
213 }
214
215 // Regenerate keypair
216 peer, err := badgerDB.RegenerateWireGuardPeer(pubkey, s.subnetPool)
217 if chk.E(err) {
218 log.E.F("failed to regenerate WireGuard peer: %v", err)
219 http.Error(w, "Failed to regenerate WireGuard configuration", http.StatusInternalServerError)
220 return
221 }
222
223 // Derive subnet IPs from sequence (same sequence as before)
224 subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
225 clientIP := subnet.ClientIP.String()
226
227 log.I.F("regenerated WireGuard keypair for user: %s", hex.Enc(pubkey[:8]))
228
229 // Return success with IP (same subnet as before)
230 w.Header().Set("Content-Type", "application/json")
231 json.NewEncoder(w).Encode(map[string]string{
232 "status": "regenerated",
233 "assigned_ip": clientIP,
234 })
235 }
236
237 // handleBunkerURL returns the bunker connection URL.
238 // Requires NIP-98 authentication and write+ access.
239 func (s *Server) handleBunkerURL(w http.ResponseWriter, r *http.Request) {
240 if r.Method != http.MethodGet {
241 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
242 return
243 }
244
245 // Check if bunker is enabled
246 if !s.Config.BunkerEnabled {
247 http.Error(w, "Bunker is not enabled on this relay", http.StatusNotFound)
248 return
249 }
250
251 // Check if WireGuard is enabled (required for bunker)
252 if !s.Config.WGEnabled {
253 http.Error(w, "WireGuard is required for bunker access", http.StatusNotFound)
254 return
255 }
256
257 // Check if ACL mode supports WireGuard
258 if s.Config.ACLMode == "none" {
259 http.Error(w, "Bunker requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
260 return
261 }
262
263 // Validate NIP-98 authentication
264 valid, pubkey, err := httpauth.CheckAuth(r)
265 if chk.E(err) || !valid {
266 http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
267 return
268 }
269
270 // Check user has write+ access
271 accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
272 if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
273 http.Error(w, "Write access required for bunker", http.StatusForbidden)
274 return
275 }
276
277 // Type assert to Badger database for WireGuard methods
278 badgerDB, ok := s.DB.(*database.D)
279 if !ok {
280 http.Error(w, "Bunker requires Badger database backend", http.StatusInternalServerError)
281 return
282 }
283
284 // Check subnet pool is available
285 if s.subnetPool == nil {
286 http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
287 return
288 }
289
290 // Get or create WireGuard peer to get their subnet
291 peer, err := badgerDB.GetOrCreateWireGuardPeer(pubkey, s.subnetPool)
292 if chk.E(err) {
293 log.E.F("failed to get/create WireGuard peer for bunker: %v", err)
294 http.Error(w, "Failed to get WireGuard configuration", http.StatusInternalServerError)
295 return
296 }
297
298 // Derive server IP for this peer's subnet
299 subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
300 serverIP := subnet.ServerIP.String()
301
302 // Get relay identity
303 relaySecret, err := s.DB.GetOrCreateRelayIdentitySecret()
304 if chk.E(err) {
305 log.E.F("failed to get relay identity: %v", err)
306 http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
307 return
308 }
309
310 relayPubkey, err := deriveNostrPublicKey(relaySecret)
311 if chk.E(err) {
312 log.E.F("failed to derive relay public key: %v", err)
313 http.Error(w, "Failed to derive relay public key", http.StatusInternalServerError)
314 return
315 }
316
317 // Encode as npub
318 relayNpubBytes, err := bech32encoding.BinToNpub(relayPubkey)
319 relayNpub := string(relayNpubBytes)
320 if chk.E(err) {
321 relayNpub = hex.Enc(relayPubkey) // Fallback to hex
322 }
323
324 // Build bunker URL using this peer's server IP
325 // Format: bunker://<relay-pubkey-hex>?relay=ws://<server-ip>:3335
326 relayPubkeyHex := hex.Enc(relayPubkey)
327 bunkerURL := fmt.Sprintf("bunker://%s?relay=ws://%s:%d",
328 relayPubkeyHex,
329 serverIP,
330 s.Config.BunkerPort,
331 )
332
333 resp := BunkerURLResponse{
334 URL: bunkerURL,
335 RelayNpub: relayNpub,
336 RelayPubkey: relayPubkeyHex,
337 InternalIP: serverIP,
338 }
339
340 w.Header().Set("Content-Type", "application/json")
341 json.NewEncoder(w).Encode(resp)
342 }
343
344 // handleWireGuardStatus returns whether WireGuard/Bunker are available.
345 func (s *Server) handleWireGuardStatus(w http.ResponseWriter, r *http.Request) {
346 if r.Method != http.MethodGet {
347 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
348 return
349 }
350
351 resp := map[string]interface{}{
352 "wireguard_enabled": s.Config.WGEnabled,
353 "bunker_enabled": s.Config.BunkerEnabled,
354 "acl_mode": s.Config.ACLMode,
355 "available": s.Config.WGEnabled && s.Config.ACLMode != "none",
356 }
357
358 if s.wireguardServer != nil {
359 resp["wireguard_running"] = s.wireguardServer.IsRunning()
360 resp["peer_count"] = s.wireguardServer.PeerCount()
361 }
362
363 if s.bunkerServer != nil {
364 resp["bunker_sessions"] = s.bunkerServer.SessionCount()
365 }
366
367 w.Header().Set("Content-Type", "application/json")
368 json.NewEncoder(w).Encode(resp)
369 }
370
371 // RevokedKeyResponse is the JSON response for revoked keys.
372 type RevokedKeyResponse struct {
373 NostrPubkey string `json:"nostr_pubkey"`
374 WGPublicKey string `json:"wg_public_key"`
375 Sequence uint32 `json:"sequence"`
376 ClientIP string `json:"client_ip"`
377 ServerIP string `json:"server_ip"`
378 CreatedAt int64 `json:"created_at"`
379 RevokedAt int64 `json:"revoked_at"`
380 AccessCount int `json:"access_count"`
381 LastAccessAt int64 `json:"last_access_at"`
382 }
383
384 // AccessLogResponse is the JSON response for access logs.
385 type AccessLogResponse struct {
386 NostrPubkey string `json:"nostr_pubkey"`
387 WGPublicKey string `json:"wg_public_key"`
388 Sequence uint32 `json:"sequence"`
389 ClientIP string `json:"client_ip"`
390 Timestamp int64 `json:"timestamp"`
391 RemoteAddr string `json:"remote_addr"`
392 }
393
394 // handleWireGuardAudit returns the user's own revoked keys and access logs.
395 // This lets users see if their old WireGuard keys are still being used,
396 // which could indicate they left something on or someone copied their credentials.
397 // Requires NIP-98 authentication and write+ access.
398 func (s *Server) handleWireGuardAudit(w http.ResponseWriter, r *http.Request) {
399 if r.Method != http.MethodGet {
400 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
401 return
402 }
403
404 // Check if WireGuard is enabled
405 if !s.Config.WGEnabled {
406 http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
407 return
408 }
409
410 // Validate NIP-98 authentication
411 valid, pubkey, err := httpauth.CheckAuth(r)
412 if chk.E(err) || !valid {
413 http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
414 return
415 }
416
417 // Check user has write+ access (same as other WireGuard endpoints)
418 accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
419 if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
420 http.Error(w, "Write access required", http.StatusForbidden)
421 return
422 }
423
424 // Type assert to Badger database for WireGuard methods
425 badgerDB, ok := s.DB.(*database.D)
426 if !ok {
427 http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
428 return
429 }
430
431 // Check subnet pool is available
432 if s.subnetPool == nil {
433 http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
434 return
435 }
436
437 // Get this user's revoked keys only
438 revokedKeys, err := badgerDB.GetRevokedKeys(pubkey)
439 if chk.E(err) {
440 log.E.F("failed to get revoked keys: %v", err)
441 http.Error(w, "Failed to get revoked keys", http.StatusInternalServerError)
442 return
443 }
444
445 // Get this user's access logs only
446 accessLogs, err := badgerDB.GetAccessLogs(pubkey)
447 if chk.E(err) {
448 log.E.F("failed to get access logs: %v", err)
449 http.Error(w, "Failed to get access logs", http.StatusInternalServerError)
450 return
451 }
452
453 // Convert to response format
454 var revokedResp []RevokedKeyResponse
455 for _, key := range revokedKeys {
456 subnet := s.subnetPool.SubnetForSequence(key.Sequence)
457 revokedResp = append(revokedResp, RevokedKeyResponse{
458 NostrPubkey: hex.Enc(key.NostrPubkey),
459 WGPublicKey: hex.Enc(key.WGPublicKey),
460 Sequence: key.Sequence,
461 ClientIP: subnet.ClientIP.String(),
462 ServerIP: subnet.ServerIP.String(),
463 CreatedAt: key.CreatedAt,
464 RevokedAt: key.RevokedAt,
465 AccessCount: key.AccessCount,
466 LastAccessAt: key.LastAccessAt,
467 })
468 }
469
470 var accessResp []AccessLogResponse
471 for _, logEntry := range accessLogs {
472 subnet := s.subnetPool.SubnetForSequence(logEntry.Sequence)
473 accessResp = append(accessResp, AccessLogResponse{
474 NostrPubkey: hex.Enc(logEntry.NostrPubkey),
475 WGPublicKey: hex.Enc(logEntry.WGPublicKey),
476 Sequence: logEntry.Sequence,
477 ClientIP: subnet.ClientIP.String(),
478 Timestamp: logEntry.Timestamp,
479 RemoteAddr: logEntry.RemoteAddr,
480 })
481 }
482
483 resp := map[string]interface{}{
484 "revoked_keys": revokedResp,
485 "access_logs": accessResp,
486 }
487
488 w.Header().Set("Content-Type", "application/json")
489 json.NewEncoder(w).Encode(resp)
490 }
491
492 // deriveWGPublicKey derives a Curve25519 public key from a private key.
493 func deriveWGPublicKey(privateKey []byte) ([]byte, error) {
494 if len(privateKey) != 32 {
495 return nil, fmt.Errorf("invalid private key length: %d", len(privateKey))
496 }
497
498 // Use wireguard package
499 return derivePublicKey(privateKey)
500 }
501
502 // deriveNostrPublicKey derives a secp256k1 public key from a secret key.
503 func deriveNostrPublicKey(secretKey []byte) ([]byte, error) {
504 if len(secretKey) != 32 {
505 return nil, fmt.Errorf("invalid secret key length: %d", len(secretKey))
506 }
507
508 // Use nostr library's key derivation
509 pk, err := deriveSecp256k1PublicKey(secretKey)
510 if err != nil {
511 return nil, err
512 }
513 return pk, nil
514 }
515