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.
182 lines
4.3 KiB
182 lines
4.3 KiB
// Package bunker provides a NIP-46 remote signing service that listens |
|
// only on the WireGuard VPN network for secure access. |
|
package bunker |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"net" |
|
"net/http" |
|
"sync" |
|
"time" |
|
|
|
"github.com/gorilla/websocket" |
|
"golang.zx2c4.com/wireguard/tun/netstack" |
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/log" |
|
|
|
"git.mleku.dev/mleku/nostr/interfaces/signer" |
|
) |
|
|
|
var upgrader = websocket.Upgrader{ |
|
ReadBufferSize: 4096, |
|
WriteBufferSize: 4096, |
|
CheckOrigin: func(r *http.Request) bool { return true }, |
|
} |
|
|
|
// Server is the NIP-46 bunker server. |
|
type Server struct { |
|
relaySigner signer.I // Relay's signer for signing events |
|
relayPubkey []byte // Relay's public key |
|
netstack *netstack.Net // WireGuard netstack for listening |
|
listenAddr string // e.g., "10.73.0.1:3335" |
|
|
|
sessions map[string]*Session // Connection ID -> Session |
|
sessionsMu sync.RWMutex |
|
|
|
server *http.Server |
|
ctx context.Context |
|
cancel context.CancelFunc |
|
wg sync.WaitGroup |
|
} |
|
|
|
// Config holds bunker server configuration. |
|
type Config struct { |
|
RelaySigner signer.I |
|
RelayPubkey []byte |
|
Netstack *netstack.Net |
|
ListenAddr string // IP:port on WireGuard network |
|
} |
|
|
|
// New creates a new bunker server. |
|
func New(cfg *Config) *Server { |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
|
|
return &Server{ |
|
relaySigner: cfg.RelaySigner, |
|
relayPubkey: cfg.RelayPubkey, |
|
netstack: cfg.Netstack, |
|
listenAddr: cfg.ListenAddr, |
|
sessions: make(map[string]*Session), |
|
ctx: ctx, |
|
cancel: cancel, |
|
} |
|
} |
|
|
|
// Start begins listening for bunker connections on the WireGuard network. |
|
func (s *Server) Start() error { |
|
// Parse listen address |
|
host, port, err := net.SplitHostPort(s.listenAddr) |
|
if err != nil { |
|
return fmt.Errorf("invalid listen address: %w", err) |
|
} |
|
|
|
ip := net.ParseIP(host) |
|
if ip == nil { |
|
return fmt.Errorf("invalid IP address: %s", host) |
|
} |
|
|
|
portNum := 0 |
|
if _, err := fmt.Sscanf(port, "%d", &portNum); err != nil { |
|
return fmt.Errorf("invalid port: %s", port) |
|
} |
|
|
|
// Create TCP listener on netstack (WireGuard network only) |
|
listener, err := s.netstack.ListenTCP(&net.TCPAddr{ |
|
IP: ip, |
|
Port: portNum, |
|
}) |
|
if err != nil { |
|
return fmt.Errorf("failed to listen on netstack: %w", err) |
|
} |
|
|
|
// Create HTTP server with WebSocket handler |
|
mux := http.NewServeMux() |
|
mux.HandleFunc("/", s.handleWebSocket) |
|
|
|
s.server = &http.Server{ |
|
Handler: mux, |
|
ReadTimeout: 30 * time.Second, |
|
WriteTimeout: 30 * time.Second, |
|
IdleTimeout: 120 * time.Second, |
|
} |
|
|
|
s.wg.Add(1) |
|
go func() { |
|
defer s.wg.Done() |
|
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed { |
|
log.E.F("bunker server error: %v", err) |
|
} |
|
}() |
|
|
|
log.I.F("NIP-46 bunker server started on %s (WireGuard only)", s.listenAddr) |
|
return nil |
|
} |
|
|
|
// Stop shuts down the bunker server. |
|
func (s *Server) Stop() error { |
|
s.cancel() |
|
|
|
if s.server != nil { |
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
defer cancel() |
|
if err := s.server.Shutdown(ctx); chk.E(err) { |
|
return err |
|
} |
|
} |
|
|
|
s.wg.Wait() |
|
log.I.F("NIP-46 bunker server stopped") |
|
return nil |
|
} |
|
|
|
// handleWebSocket handles WebSocket connections for NIP-46. |
|
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { |
|
conn, err := upgrader.Upgrade(w, r, nil) |
|
if err != nil { |
|
log.E.F("bunker websocket upgrade failed: %v", err) |
|
return |
|
} |
|
|
|
session := NewSession(s.ctx, conn, s.relaySigner, s.relayPubkey) |
|
|
|
// Register session |
|
s.sessionsMu.Lock() |
|
s.sessions[session.ID] = session |
|
s.sessionsMu.Unlock() |
|
|
|
// Handle session |
|
session.Handle() |
|
|
|
// Unregister session |
|
s.sessionsMu.Lock() |
|
delete(s.sessions, session.ID) |
|
s.sessionsMu.Unlock() |
|
} |
|
|
|
// SessionCount returns the number of active sessions. |
|
func (s *Server) SessionCount() int { |
|
s.sessionsMu.RLock() |
|
defer s.sessionsMu.RUnlock() |
|
return len(s.sessions) |
|
} |
|
|
|
// RelayPubkeyHex returns the relay's public key as hex. |
|
func (s *Server) RelayPubkeyHex() string { |
|
return fmt.Sprintf("%x", s.relayPubkey) |
|
} |
|
|
|
// NIP46Request represents a NIP-46 request from a client. |
|
type NIP46Request struct { |
|
ID string `json:"id"` |
|
Method string `json:"method"` |
|
Params json.RawMessage `json:"params"` |
|
} |
|
|
|
// NIP46Response represents a NIP-46 response to a client. |
|
type NIP46Response struct { |
|
ID string `json:"id"` |
|
Result any `json:"result,omitempty"` |
|
Error string `json:"error,omitempty"` |
|
}
|
|
|