server.go raw

   1  // Package bunker provides a NIP-46 remote signing service that listens
   2  // only on the WireGuard VPN network for secure access.
   3  package bunker
   4  
   5  import (
   6  	"context"
   7  	"encoding/json"
   8  	"fmt"
   9  	"net"
  10  	"net/http"
  11  	"sync"
  12  	"time"
  13  
  14  	"github.com/gorilla/websocket"
  15  	"golang.zx2c4.com/wireguard/tun/netstack"
  16  	"next.orly.dev/pkg/lol/chk"
  17  	"next.orly.dev/pkg/lol/log"
  18  
  19  	"next.orly.dev/pkg/nostr/interfaces/signer"
  20  )
  21  
  22  var upgrader = websocket.Upgrader{
  23  	ReadBufferSize:  4096,
  24  	WriteBufferSize: 4096,
  25  	CheckOrigin:     func(r *http.Request) bool { return true },
  26  }
  27  
  28  // Server is the NIP-46 bunker server.
  29  type Server struct {
  30  	relaySigner signer.I      // Relay's signer for signing events
  31  	relayPubkey []byte        // Relay's public key
  32  	netstack    *netstack.Net // WireGuard netstack for listening
  33  	listenAddr  string        // e.g., "10.73.0.1:3335"
  34  
  35  	sessions   map[string]*Session // Connection ID -> Session
  36  	sessionsMu sync.RWMutex
  37  
  38  	server *http.Server
  39  	ctx    context.Context
  40  	cancel context.CancelFunc
  41  	wg     sync.WaitGroup
  42  }
  43  
  44  // Config holds bunker server configuration.
  45  type Config struct {
  46  	RelaySigner signer.I
  47  	RelayPubkey []byte
  48  	Netstack    *netstack.Net
  49  	ListenAddr  string // IP:port on WireGuard network
  50  }
  51  
  52  // New creates a new bunker server.
  53  func New(cfg *Config) *Server {
  54  	ctx, cancel := context.WithCancel(context.Background())
  55  
  56  	return &Server{
  57  		relaySigner: cfg.RelaySigner,
  58  		relayPubkey: cfg.RelayPubkey,
  59  		netstack:    cfg.Netstack,
  60  		listenAddr:  cfg.ListenAddr,
  61  		sessions:    make(map[string]*Session),
  62  		ctx:         ctx,
  63  		cancel:      cancel,
  64  	}
  65  }
  66  
  67  // Start begins listening for bunker connections on the WireGuard network.
  68  func (s *Server) Start() error {
  69  	// Parse listen address
  70  	host, port, err := net.SplitHostPort(s.listenAddr)
  71  	if err != nil {
  72  		return fmt.Errorf("invalid listen address: %w", err)
  73  	}
  74  
  75  	ip := net.ParseIP(host)
  76  	if ip == nil {
  77  		return fmt.Errorf("invalid IP address: %s", host)
  78  	}
  79  
  80  	portNum := 0
  81  	if _, err := fmt.Sscanf(port, "%d", &portNum); err != nil {
  82  		return fmt.Errorf("invalid port: %s", port)
  83  	}
  84  
  85  	// Create TCP listener on netstack (WireGuard network only)
  86  	listener, err := s.netstack.ListenTCP(&net.TCPAddr{
  87  		IP:   ip,
  88  		Port: portNum,
  89  	})
  90  	if err != nil {
  91  		return fmt.Errorf("failed to listen on netstack: %w", err)
  92  	}
  93  
  94  	// Create HTTP server with WebSocket handler
  95  	mux := http.NewServeMux()
  96  	mux.HandleFunc("/", s.handleWebSocket)
  97  
  98  	s.server = &http.Server{
  99  		Handler:      mux,
 100  		ReadTimeout:  30 * time.Second,
 101  		WriteTimeout: 30 * time.Second,
 102  		IdleTimeout:  120 * time.Second,
 103  	}
 104  
 105  	s.wg.Add(1)
 106  	go func() {
 107  		defer s.wg.Done()
 108  		if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
 109  			log.E.F("bunker server error: %v", err)
 110  		}
 111  	}()
 112  
 113  	log.I.F("NIP-46 bunker server started on %s (WireGuard only)", s.listenAddr)
 114  	return nil
 115  }
 116  
 117  // Stop shuts down the bunker server.
 118  func (s *Server) Stop() error {
 119  	s.cancel()
 120  
 121  	if s.server != nil {
 122  		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 123  		defer cancel()
 124  		if err := s.server.Shutdown(ctx); chk.E(err) {
 125  			return err
 126  		}
 127  	}
 128  
 129  	s.wg.Wait()
 130  	log.I.F("NIP-46 bunker server stopped")
 131  	return nil
 132  }
 133  
 134  // handleWebSocket handles WebSocket connections for NIP-46.
 135  func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
 136  	conn, err := upgrader.Upgrade(w, r, nil)
 137  	if err != nil {
 138  		log.E.F("bunker websocket upgrade failed: %v", err)
 139  		return
 140  	}
 141  
 142  	session := NewSession(s.ctx, conn, s.relaySigner, s.relayPubkey)
 143  
 144  	// Register session
 145  	s.sessionsMu.Lock()
 146  	s.sessions[session.ID] = session
 147  	s.sessionsMu.Unlock()
 148  
 149  	// Handle session
 150  	session.Handle()
 151  
 152  	// Unregister session
 153  	s.sessionsMu.Lock()
 154  	delete(s.sessions, session.ID)
 155  	s.sessionsMu.Unlock()
 156  }
 157  
 158  // SessionCount returns the number of active sessions.
 159  func (s *Server) SessionCount() int {
 160  	s.sessionsMu.RLock()
 161  	defer s.sessionsMu.RUnlock()
 162  	return len(s.sessions)
 163  }
 164  
 165  // RelayPubkeyHex returns the relay's public key as hex.
 166  func (s *Server) RelayPubkeyHex() string {
 167  	return fmt.Sprintf("%x", s.relayPubkey)
 168  }
 169  
 170  // NIP46Request represents a NIP-46 request from a client.
 171  type NIP46Request struct {
 172  	ID     string          `json:"id"`
 173  	Method string          `json:"method"`
 174  	Params json.RawMessage `json:"params"`
 175  }
 176  
 177  // NIP46Response represents a NIP-46 response to a client.
 178  type NIP46Response struct {
 179  	ID     string `json:"id"`
 180  	Result any    `json:"result,omitempty"`
 181  	Error  string `json:"error,omitempty"`
 182  }
 183