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