You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

514 lines
16 KiB

package app
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/httpauth"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
)
// WireGuardConfigResponse is returned by the /api/wireguard/config endpoint.
type WireGuardConfigResponse struct {
ConfigText string `json:"config_text"`
Interface WGInterface `json:"interface"`
Peer WGPeer `json:"peer"`
}
// WGInterface represents the [Interface] section of a WireGuard config.
type WGInterface struct {
Address string `json:"address"`
PrivateKey string `json:"private_key"`
}
// WGPeer represents the [Peer] section of a WireGuard config.
type WGPeer struct {
PublicKey string `json:"public_key"`
Endpoint string `json:"endpoint"`
AllowedIPs string `json:"allowed_ips"`
}
// BunkerURLResponse is returned by the /api/bunker/url endpoint.
type BunkerURLResponse struct {
URL string `json:"url"`
RelayNpub string `json:"relay_npub"`
RelayPubkey string `json:"relay_pubkey"`
InternalIP string `json:"internal_ip"`
}
// handleWireGuardConfig returns the user's WireGuard configuration.
// Requires NIP-98 authentication and write+ access.
func (s *Server) handleWireGuardConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if WireGuard is enabled
if !s.Config.WGEnabled {
http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
return
}
// Check if ACL mode supports WireGuard
if s.Config.ACLMode == "none" {
http.Error(w, "WireGuard requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return
}
// Check user has write+ access
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
http.Error(w, "Write access required for WireGuard", http.StatusForbidden)
return
}
// Type assert to Badger database for WireGuard methods
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
return
}
// Check subnet pool is available
if s.subnetPool == nil {
http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
return
}
// Get or create WireGuard peer for this user
peer, err := badgerDB.GetOrCreateWireGuardPeer(pubkey, s.subnetPool)
if chk.E(err) {
log.E.F("failed to get/create WireGuard peer: %v", err)
http.Error(w, "Failed to create WireGuard configuration", http.StatusInternalServerError)
return
}
// Derive subnet IPs from sequence
subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
clientIP := subnet.ClientIP.String()
serverIP := subnet.ServerIP.String()
// Get server public key
serverKey, err := badgerDB.GetOrCreateWireGuardServerKey()
if chk.E(err) {
log.E.F("failed to get WireGuard server key: %v", err)
http.Error(w, "WireGuard server not configured", http.StatusInternalServerError)
return
}
serverPubKey, err := deriveWGPublicKey(serverKey)
if chk.E(err) {
log.E.F("failed to derive server public key: %v", err)
http.Error(w, "WireGuard server error", http.StatusInternalServerError)
return
}
// Build endpoint
endpoint := fmt.Sprintf("%s:%d", s.Config.WGEndpoint, s.Config.WGPort)
// Build response
resp := WireGuardConfigResponse{
Interface: WGInterface{
Address: clientIP + "/32",
PrivateKey: base64.StdEncoding.EncodeToString(peer.WGPrivateKey),
},
Peer: WGPeer{
PublicKey: base64.StdEncoding.EncodeToString(serverPubKey),
Endpoint: endpoint,
AllowedIPs: serverIP + "/32", // Only route bunker traffic to this peer's server IP
},
}
// Generate config text
resp.ConfigText = fmt.Sprintf(`[Interface]
Address = %s
PrivateKey = %s
[Peer]
PublicKey = %s
Endpoint = %s
AllowedIPs = %s
PersistentKeepalive = 25
`, resp.Interface.Address, resp.Interface.PrivateKey,
resp.Peer.PublicKey, resp.Peer.Endpoint, resp.Peer.AllowedIPs)
// If WireGuard server is running, add the peer
if s.wireguardServer != nil && s.wireguardServer.IsRunning() {
if err := s.wireguardServer.AddPeer(pubkey, peer.WGPublicKey, clientIP); chk.E(err) {
log.W.F("failed to add peer to running WireGuard server: %v", err)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// handleWireGuardRegenerate generates a new WireGuard keypair for the user.
// Requires NIP-98 authentication and write+ access.
func (s *Server) handleWireGuardRegenerate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if WireGuard is enabled
if !s.Config.WGEnabled {
http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
return
}
// Check if ACL mode supports WireGuard
if s.Config.ACLMode == "none" {
http.Error(w, "WireGuard requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return
}
// Check user has write+ access
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
http.Error(w, "Write access required for WireGuard", http.StatusForbidden)
return
}
// Type assert to Badger database for WireGuard methods
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
return
}
// Check subnet pool is available
if s.subnetPool == nil {
http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
return
}
// Remove old peer from running server if exists
oldPeer, err := badgerDB.GetWireGuardPeer(pubkey)
if err == nil && oldPeer != nil && s.wireguardServer != nil && s.wireguardServer.IsRunning() {
s.wireguardServer.RemovePeer(oldPeer.WGPublicKey)
}
// Regenerate keypair
peer, err := badgerDB.RegenerateWireGuardPeer(pubkey, s.subnetPool)
if chk.E(err) {
log.E.F("failed to regenerate WireGuard peer: %v", err)
http.Error(w, "Failed to regenerate WireGuard configuration", http.StatusInternalServerError)
return
}
// Derive subnet IPs from sequence (same sequence as before)
subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
clientIP := subnet.ClientIP.String()
log.I.F("regenerated WireGuard keypair for user: %s", hex.Enc(pubkey[:8]))
// Return success with IP (same subnet as before)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "regenerated",
"assigned_ip": clientIP,
})
}
// handleBunkerURL returns the bunker connection URL.
// Requires NIP-98 authentication and write+ access.
func (s *Server) handleBunkerURL(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if bunker is enabled
if !s.Config.BunkerEnabled {
http.Error(w, "Bunker is not enabled on this relay", http.StatusNotFound)
return
}
// Check if WireGuard is enabled (required for bunker)
if !s.Config.WGEnabled {
http.Error(w, "WireGuard is required for bunker access", http.StatusNotFound)
return
}
// Check if ACL mode supports WireGuard
if s.Config.ACLMode == "none" {
http.Error(w, "Bunker requires ACL mode 'follows' or 'managed'", http.StatusForbidden)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return
}
// Check user has write+ access
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
http.Error(w, "Write access required for bunker", http.StatusForbidden)
return
}
// Type assert to Badger database for WireGuard methods
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "Bunker requires Badger database backend", http.StatusInternalServerError)
return
}
// Check subnet pool is available
if s.subnetPool == nil {
http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
return
}
// Get or create WireGuard peer to get their subnet
peer, err := badgerDB.GetOrCreateWireGuardPeer(pubkey, s.subnetPool)
if chk.E(err) {
log.E.F("failed to get/create WireGuard peer for bunker: %v", err)
http.Error(w, "Failed to get WireGuard configuration", http.StatusInternalServerError)
return
}
// Derive server IP for this peer's subnet
subnet := s.subnetPool.SubnetForSequence(peer.Sequence)
serverIP := subnet.ServerIP.String()
// Get relay identity
relaySecret, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
log.E.F("failed to get relay identity: %v", err)
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, err := deriveNostrPublicKey(relaySecret)
if chk.E(err) {
log.E.F("failed to derive relay public key: %v", err)
http.Error(w, "Failed to derive relay public key", http.StatusInternalServerError)
return
}
// Encode as npub
relayNpubBytes, err := bech32encoding.BinToNpub(relayPubkey)
relayNpub := string(relayNpubBytes)
if chk.E(err) {
relayNpub = hex.Enc(relayPubkey) // Fallback to hex
}
// Build bunker URL using this peer's server IP
// Format: bunker://<relay-pubkey-hex>?relay=ws://<server-ip>:3335
relayPubkeyHex := hex.Enc(relayPubkey)
bunkerURL := fmt.Sprintf("bunker://%s?relay=ws://%s:%d",
relayPubkeyHex,
serverIP,
s.Config.BunkerPort,
)
resp := BunkerURLResponse{
URL: bunkerURL,
RelayNpub: relayNpub,
RelayPubkey: relayPubkeyHex,
InternalIP: serverIP,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// handleWireGuardStatus returns whether WireGuard/Bunker are available.
func (s *Server) handleWireGuardStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
resp := map[string]interface{}{
"wireguard_enabled": s.Config.WGEnabled,
"bunker_enabled": s.Config.BunkerEnabled,
"acl_mode": s.Config.ACLMode,
"available": s.Config.WGEnabled && s.Config.ACLMode != "none",
}
if s.wireguardServer != nil {
resp["wireguard_running"] = s.wireguardServer.IsRunning()
resp["peer_count"] = s.wireguardServer.PeerCount()
}
if s.bunkerServer != nil {
resp["bunker_sessions"] = s.bunkerServer.SessionCount()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// RevokedKeyResponse is the JSON response for revoked keys.
type RevokedKeyResponse struct {
NostrPubkey string `json:"nostr_pubkey"`
WGPublicKey string `json:"wg_public_key"`
Sequence uint32 `json:"sequence"`
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
CreatedAt int64 `json:"created_at"`
RevokedAt int64 `json:"revoked_at"`
AccessCount int `json:"access_count"`
LastAccessAt int64 `json:"last_access_at"`
}
// AccessLogResponse is the JSON response for access logs.
type AccessLogResponse struct {
NostrPubkey string `json:"nostr_pubkey"`
WGPublicKey string `json:"wg_public_key"`
Sequence uint32 `json:"sequence"`
ClientIP string `json:"client_ip"`
Timestamp int64 `json:"timestamp"`
RemoteAddr string `json:"remote_addr"`
}
// handleWireGuardAudit returns the user's own revoked keys and access logs.
// This lets users see if their old WireGuard keys are still being used,
// which could indicate they left something on or someone copied their credentials.
// Requires NIP-98 authentication and write+ access.
func (s *Server) handleWireGuardAudit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if WireGuard is enabled
if !s.Config.WGEnabled {
http.Error(w, "WireGuard is not enabled on this relay", http.StatusNotFound)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
http.Error(w, "NIP-98 authentication required", http.StatusUnauthorized)
return
}
// Check user has write+ access (same as other WireGuard endpoints)
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "write" && accessLevel != "admin" && accessLevel != "owner" {
http.Error(w, "Write access required", http.StatusForbidden)
return
}
// Type assert to Badger database for WireGuard methods
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "WireGuard requires Badger database backend", http.StatusInternalServerError)
return
}
// Check subnet pool is available
if s.subnetPool == nil {
http.Error(w, "WireGuard subnet pool not initialized", http.StatusInternalServerError)
return
}
// Get this user's revoked keys only
revokedKeys, err := badgerDB.GetRevokedKeys(pubkey)
if chk.E(err) {
log.E.F("failed to get revoked keys: %v", err)
http.Error(w, "Failed to get revoked keys", http.StatusInternalServerError)
return
}
// Get this user's access logs only
accessLogs, err := badgerDB.GetAccessLogs(pubkey)
if chk.E(err) {
log.E.F("failed to get access logs: %v", err)
http.Error(w, "Failed to get access logs", http.StatusInternalServerError)
return
}
// Convert to response format
var revokedResp []RevokedKeyResponse
for _, key := range revokedKeys {
subnet := s.subnetPool.SubnetForSequence(key.Sequence)
revokedResp = append(revokedResp, RevokedKeyResponse{
NostrPubkey: hex.Enc(key.NostrPubkey),
WGPublicKey: hex.Enc(key.WGPublicKey),
Sequence: key.Sequence,
ClientIP: subnet.ClientIP.String(),
ServerIP: subnet.ServerIP.String(),
CreatedAt: key.CreatedAt,
RevokedAt: key.RevokedAt,
AccessCount: key.AccessCount,
LastAccessAt: key.LastAccessAt,
})
}
var accessResp []AccessLogResponse
for _, logEntry := range accessLogs {
subnet := s.subnetPool.SubnetForSequence(logEntry.Sequence)
accessResp = append(accessResp, AccessLogResponse{
NostrPubkey: hex.Enc(logEntry.NostrPubkey),
WGPublicKey: hex.Enc(logEntry.WGPublicKey),
Sequence: logEntry.Sequence,
ClientIP: subnet.ClientIP.String(),
Timestamp: logEntry.Timestamp,
RemoteAddr: logEntry.RemoteAddr,
})
}
resp := map[string]interface{}{
"revoked_keys": revokedResp,
"access_logs": accessResp,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// deriveWGPublicKey derives a Curve25519 public key from a private key.
func deriveWGPublicKey(privateKey []byte) ([]byte, error) {
if len(privateKey) != 32 {
return nil, fmt.Errorf("invalid private key length: %d", len(privateKey))
}
// Use wireguard package
return derivePublicKey(privateKey)
}
// deriveNostrPublicKey derives a secp256k1 public key from a secret key.
func deriveNostrPublicKey(secretKey []byte) ([]byte, error) {
if len(secretKey) != 32 {
return nil, fmt.Errorf("invalid secret key length: %d", len(secretKey))
}
// Use nostr library's key derivation
pk, err := deriveSecp256k1PublicKey(secretKey)
if err != nil {
return nil, err
}
return pk, nil
}