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