From b3e8aad8f07c9daca4d77bcbc811c249b244b97d Mon Sep 17 00:00:00 2001 From: woikos Date: Sat, 31 Jan 2026 13:46:05 +0100 Subject: [PATCH] Extract network transports into pluggable Transport module - Define Transport interface (Name, Start, Stop, Addresses) at pkg/interfaces/transport/ - Add Transport Manager with ordered startup/shutdown at pkg/transport/ - Create TCP transport (pkg/transport/tcp/) wrapping plain http.Server - Create TLS transport (pkg/transport/tls/) with ACME + manual certs - Create Tor transport (pkg/transport/tor/) wrapping existing pkg/tor service - Replace ~220 lines of inline transport code in app/main.go with manager - Simplify shutdown handler to single transportMgr.StopAll() call - Use transportMgr.Addresses() for NIP-11 relay info (replaces torService) Files modified: - pkg/interfaces/transport/transport.go: New transport interface - pkg/transport/manager.go: New transport manager - pkg/transport/tcp/tcp.go: New TCP transport - pkg/transport/tls/tls.go: New TLS/ACME transport (moved from app/tls.go) - pkg/transport/tor/tor.go: New Tor transport wrapper - app/main.go: Use transport manager for all networking - app/server.go: Replace torService field with transportMgr - app/handle-relayinfo.go: Use transportMgr.Addresses() - app/tls.go: Deleted (moved to pkg/transport/tls/) Co-Authored-By: Claude Opus 4.5 --- app/handle-relayinfo.go | 8 +- app/main.go | 151 ++++------------ app/server.go | 6 +- app/tls.go | 132 -------------- pkg/interfaces/transport/transport.go | 16 ++ pkg/transport/manager.go | 86 +++++++++ pkg/transport/tcp/tcp.go | 78 ++++++++ pkg/transport/tls/tls.go | 247 ++++++++++++++++++++++++++ pkg/transport/tor/tor.go | 91 ++++++++++ 9 files changed, 557 insertions(+), 258 deletions(-) delete mode 100644 app/tls.go create mode 100644 pkg/interfaces/transport/transport.go create mode 100644 pkg/transport/manager.go create mode 100644 pkg/transport/tcp/tcp.go create mode 100644 pkg/transport/tls/tls.go create mode 100644 pkg/transport/tor/tor.go diff --git a/app/handle-relayinfo.go b/app/handle-relayinfo.go index 24a8cba..7fe09e4 100644 --- a/app/handle-relayinfo.go +++ b/app/handle-relayinfo.go @@ -200,11 +200,9 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) { addresses = append(addresses, s.Config.RelayAddresses...) } - // Add Tor hidden service address if available - if s.torService != nil { - if onionAddr := s.torService.OnionWSAddress(); onionAddr != "" { - addresses = append(addresses, onionAddr) - } + // Add addresses from all transports (Tor .onion, etc.) + if s.transportMgr != nil { + addresses = append(addresses, s.transportMgr.Addresses()...) } // Build graph query config if enabled diff --git a/app/main.go b/app/main.go index e3ebfea..61c6b21 100644 --- a/app/main.go +++ b/app/main.go @@ -3,7 +3,6 @@ package app import ( "context" "fmt" - "net/http" "os" "path/filepath" "strings" @@ -11,7 +10,6 @@ import ( "time" "github.com/adrg/xdg" - "golang.org/x/crypto/acme/autocert" "lol.mleku.dev/chk" "lol.mleku.dev/log" "next.orly.dev/app/branding" @@ -32,9 +30,12 @@ import ( "next.orly.dev/pkg/spider" "next.orly.dev/pkg/storage" dsync "next.orly.dev/pkg/sync" + "next.orly.dev/pkg/transport" + "next.orly.dev/pkg/transport/tcp" + tlstransport "next.orly.dev/pkg/transport/tls" + tortransport "next.orly.dev/pkg/transport/tor" "next.orly.dev/pkg/wireguard" "next.orly.dev/pkg/archive" - "next.orly.dev/pkg/tor" "git.mleku.dev/mleku/nostr/interfaces/signer/p8k" ) @@ -616,32 +617,20 @@ func Run( log.I.F("archive relay manager initialized with %d relays", len(archiveRelays)) } - // Initialize Tor hidden service if enabled (spawns tor subprocess) + // Build transport manager + l.transportMgr = transport.NewManager() + + // Add Tor transport if enabled (can start before db is ready) torEnabled, torPort, torDataDir, torBinary, torSOCKSPort := cfg.GetTorConfigValues() if torEnabled { - torCfg := &tor.Config{ + tt := tortransport.New(&tortransport.Config{ Port: torPort, DataDir: torDataDir, Binary: torBinary, SOCKSPort: torSOCKSPort, Handler: l, - } - var err error - l.torService, err = tor.New(torCfg) - if err != nil { - log.W.F("Tor disabled: %v", err) - } else { - if err = l.torService.Start(); err != nil { - log.W.F("failed to start Tor service: %v", err) - l.torService = nil - } else { - if addr := l.torService.OnionWSAddress(); addr != "" { - log.I.F("Tor hidden service listening on port %d, address: %s", torPort, addr) - } else { - log.I.F("Tor hidden service listening on port %d (waiting for .onion address)", torPort) - } - } - } + }) + l.transportMgr.Add(tt) } // Start rate limiter if enabled @@ -653,81 +642,26 @@ func Run( // Wait for database to be ready before accepting requests log.I.F("waiting for database warmup to complete...") <-db.Ready() - log.I.F("database ready, starting HTTP servers") - - // Check if TLS is enabled - var tlsEnabled bool - var tlsServer *http.Server - var httpServer *http.Server + log.I.F("database ready, starting transports") + // Add TLS or plain TCP transport (mutually exclusive) if len(cfg.TLSDomains) > 0 { - // Validate TLS configuration - if err = ValidateTLSConfig(cfg.TLSDomains, cfg.Certs); chk.E(err) { - log.E.F("invalid TLS configuration: %v", err) - } else { - tlsEnabled = true - log.I.F("TLS enabled for domains: %v", cfg.TLSDomains) - - // Create cache directory for autocert - cacheDir := filepath.Join(cfg.DataDir, "autocert") - if err = os.MkdirAll(cacheDir, 0700); chk.E(err) { - log.E.F("failed to create autocert cache directory: %v", err) - tlsEnabled = false - } else { - // Set up autocert manager - m := &autocert.Manager{ - Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(cacheDir), - HostPolicy: autocert.HostWhitelist(cfg.TLSDomains...), - } - - // Create TLS server on port 443 - tlsServer = &http.Server{ - Addr: ":443", - Handler: l, - TLSConfig: TLSConfig(m, cfg.Certs...), - } - - // Create HTTP server for ACME challenges and redirects on port 80 - httpServer = &http.Server{ - Addr: ":80", - Handler: m.HTTPHandler(nil), - } - - // Start TLS server - go func() { - log.I.F("starting TLS listener on https://:443") - if err := tlsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { - log.E.F("TLS server error: %v", err) - } - }() - - // Start HTTP server for ACME challenges - go func() { - log.I.F("starting HTTP listener on http://:80 for ACME challenges") - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.E.F("HTTP server error: %v", err) - } - }() - } - } - } - - // Start regular HTTP server if TLS is not enabled or as fallback - if !tlsEnabled { - addr := fmt.Sprintf("%s:%d", cfg.Listen, cfg.Port) - log.I.F("starting listener on http://%s", addr) - - httpServer = &http.Server{ - Addr: addr, + l.transportMgr.Add(tlstransport.New(&tlstransport.Config{ + Domains: cfg.TLSDomains, + Certs: cfg.Certs, + DataDir: cfg.DataDir, Handler: l, - } + })) + } else { + l.transportMgr.Add(tcp.New(&tcp.Config{ + Addr: fmt.Sprintf("%s:%d", cfg.Listen, cfg.Port), + Handler: l, + })) + } - go func() { - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.E.F("HTTP server error: %v", err) - } - }() + // Start all transports + if err := l.transportMgr.StartAll(ctx); err != nil { + log.E.F("transport startup failed: %v", err) } // Graceful shutdown handler @@ -759,12 +693,6 @@ func Run( log.I.F("archive manager stopped") } - // Stop Tor service if running - if l.torService != nil { - l.torService.Stop() - log.I.F("Tor service stopped") - } - // Stop garbage collector if running if l.garbageCollector != nil { l.garbageCollector.Stop() @@ -795,25 +723,12 @@ func Run( log.I.F("WireGuard server stopped") } - // Create shutdown context with timeout - shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 10*time.Second) - defer cancelShutdown() - - // Shutdown TLS server if running - if tlsServer != nil { - if err := tlsServer.Shutdown(shutdownCtx); err != nil { - log.E.F("TLS server shutdown error: %v", err) - } else { - log.I.F("TLS server shutdown completed") - } - } - - // Shutdown HTTP server - if httpServer != nil { - if err := httpServer.Shutdown(shutdownCtx); err != nil { - log.E.F("HTTP server shutdown error: %v", err) - } else { - log.I.F("HTTP server shutdown completed") + // Stop all transports (TCP/TLS/Tor) + if l.transportMgr != nil { + shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelShutdown() + if err := l.transportMgr.StopAll(shutdownCtx); err != nil { + log.E.F("transport shutdown error: %v", err) } } diff --git a/app/server.go b/app/server.go index cb9777e..ca8d083 100644 --- a/app/server.go +++ b/app/server.go @@ -47,7 +47,7 @@ import ( dsync "next.orly.dev/pkg/sync" "next.orly.dev/pkg/wireguard" "next.orly.dev/pkg/archive" - "next.orly.dev/pkg/tor" + "next.orly.dev/pkg/transport" ) type Server struct { @@ -122,8 +122,8 @@ type Server struct { accessTracker *storage.AccessTracker garbageCollector *storage.GarbageCollector - // Tor hidden service - torService *tor.Service + // Transport manager for network transports (TCP, TLS, Tor, etc.) + transportMgr *transport.Manager // Branding/white-label customization brandingMgr *branding.Manager diff --git a/app/tls.go b/app/tls.go deleted file mode 100644 index bf7314d..0000000 --- a/app/tls.go +++ /dev/null @@ -1,132 +0,0 @@ -package app - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "strings" - "sync" - - "golang.org/x/crypto/acme/autocert" - "lol.mleku.dev/chk" - "lol.mleku.dev/log" -) - -// TLSConfig returns a TLS configuration that works with LetsEncrypt automatic SSL cert issuer -// as well as any provided certificate files from providers. -// -// The certs are provided in the form of paths where .pem and .key files exist -func TLSConfig(m *autocert.Manager, certs ...string) (tc *tls.Config) { - certMap := make(map[string]*tls.Certificate) - var mx sync.Mutex - - for _, certPath := range certs { - if certPath == "" { - continue - } - - var err error - var c tls.Certificate - - // Load certificate and key files - if c, err = tls.LoadX509KeyPair( - certPath+".pem", certPath+".key", - ); chk.E(err) { - log.E.F("failed to load certificate from %s: %v", certPath, err) - continue - } - - // Extract domain names from certificate - if len(c.Certificate) > 0 { - if x509Cert, err := x509.ParseCertificate(c.Certificate[0]); err == nil { - // Use the common name as the primary domain - if x509Cert.Subject.CommonName != "" { - certMap[x509Cert.Subject.CommonName] = &c - log.I.F("loaded certificate for domain: %s", x509Cert.Subject.CommonName) - } - // Also add any subject alternative names - for _, san := range x509Cert.DNSNames { - if san != "" { - certMap[san] = &c - log.I.F("loaded certificate for SAN domain: %s", san) - } - } - } - } - } - - if m == nil { - // Create a basic TLS config without autocert - tc = &tls.Config{ - GetCertificate: func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { - mx.Lock() - defer mx.Unlock() - - // Check for exact match first - if cert, exists := certMap[helo.ServerName]; exists { - return cert, nil - } - - // Check for wildcard matches - for domain, cert := range certMap { - if strings.HasPrefix(domain, "*.") { - baseDomain := domain[2:] // Remove "*." - if strings.HasSuffix(helo.ServerName, baseDomain) { - return cert, nil - } - } - } - - return nil, fmt.Errorf("no certificate found for %s", helo.ServerName) - }, - } - } else { - tc = m.TLSConfig() - tc.GetCertificate = func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { - mx.Lock() - - // Check for exact match first - if cert, exists := certMap[helo.ServerName]; exists { - mx.Unlock() - return cert, nil - } - - // Check for wildcard matches - for domain, cert := range certMap { - if strings.HasPrefix(domain, "*.") { - baseDomain := domain[2:] // Remove "*." - if strings.HasSuffix(helo.ServerName, baseDomain) { - mx.Unlock() - return cert, nil - } - } - } - - mx.Unlock() - - // Fall back to autocert for domains not in our certificate map - return m.GetCertificate(helo) - } - } - - return tc -} - -// ValidateTLSConfig checks if the TLS configuration is valid -func ValidateTLSConfig(domains []string, certs []string) (err error) { - if len(domains) == 0 { - return fmt.Errorf("no TLS domains specified") - } - - // Validate domain names - for _, domain := range domains { - if domain == "" { - continue - } - if strings.Contains(domain, " ") || strings.Contains(domain, "\t") { - return fmt.Errorf("invalid domain name: %s", domain) - } - } - - return nil -} diff --git a/pkg/interfaces/transport/transport.go b/pkg/interfaces/transport/transport.go new file mode 100644 index 0000000..68dab3f --- /dev/null +++ b/pkg/interfaces/transport/transport.go @@ -0,0 +1,16 @@ +// Package transport defines the interface for pluggable network transports. +package transport + +import "context" + +// Transport represents a network transport that serves the relay. +type Transport interface { + // Name returns the transport identifier (e.g., "tcp", "tls", "tor"). + Name() string + // Start begins accepting connections through this transport. + Start(ctx context.Context) error + // Stop gracefully shuts down the transport. + Stop(ctx context.Context) error + // Addresses returns the addresses this transport is reachable on. + Addresses() []string +} diff --git a/pkg/transport/manager.go b/pkg/transport/manager.go new file mode 100644 index 0000000..583edcf --- /dev/null +++ b/pkg/transport/manager.go @@ -0,0 +1,86 @@ +// Package transport provides a manager for pluggable network transports. +package transport + +import ( + "context" + "fmt" + "sync" + + "lol.mleku.dev/log" + + iface "next.orly.dev/pkg/interfaces/transport" +) + +// Manager manages multiple transports and coordinates their lifecycle. +type Manager struct { + mu sync.RWMutex + transports []iface.Transport +} + +// NewManager creates a new transport manager. +func NewManager() *Manager { + return &Manager{} +} + +// Add registers a transport with the manager. +func (m *Manager) Add(t iface.Transport) { + m.mu.Lock() + defer m.mu.Unlock() + m.transports = append(m.transports, t) +} + +// StartAll starts all registered transports in order. +// If any transport fails to start, previously started transports are stopped. +func (m *Manager) StartAll(ctx context.Context) error { + m.mu.RLock() + defer m.mu.RUnlock() + + for i, t := range m.transports { + log.I.F("starting transport: %s", t.Name()) + if err := t.Start(ctx); err != nil { + // Stop previously started transports in reverse order + for j := i - 1; j >= 0; j-- { + if stopErr := m.transports[j].Stop(ctx); stopErr != nil { + log.E.F("failed to stop transport %s during rollback: %v", + m.transports[j].Name(), stopErr) + } + } + return fmt.Errorf("transport %s failed to start: %w", t.Name(), err) + } + log.I.F("transport started: %s", t.Name()) + } + return nil +} + +// StopAll stops all transports in reverse order. +func (m *Manager) StopAll(ctx context.Context) error { + m.mu.RLock() + defer m.mu.RUnlock() + + var firstErr error + for i := len(m.transports) - 1; i >= 0; i-- { + t := m.transports[i] + log.I.F("stopping transport: %s", t.Name()) + if err := t.Stop(ctx); err != nil { + log.E.F("failed to stop transport %s: %v", t.Name(), err) + if firstErr == nil { + firstErr = err + } + } else { + log.I.F("transport stopped: %s", t.Name()) + } + } + return firstErr +} + +// Addresses returns all addresses from all transports. +func (m *Manager) Addresses() []string { + m.mu.RLock() + defer m.mu.RUnlock() + + var addrs []string + for _, t := range m.transports { + addrs = append(addrs, t.Addresses()...) + } + return addrs +} diff --git a/pkg/transport/tcp/tcp.go b/pkg/transport/tcp/tcp.go new file mode 100644 index 0000000..00c9ba9 --- /dev/null +++ b/pkg/transport/tcp/tcp.go @@ -0,0 +1,78 @@ +// Package tcp provides a plain HTTP transport for the relay. +package tcp + +import ( + "context" + "fmt" + "net/http" + "sync" + + "lol.mleku.dev/log" +) + +// Config holds TCP transport configuration. +type Config struct { + // Addr is the listen address (e.g., "0.0.0.0:3334"). + Addr string + // Handler is the HTTP handler to serve. + Handler http.Handler +} + +// Transport serves HTTP over plain TCP. +type Transport struct { + cfg *Config + server *http.Server + mu sync.Mutex +} + +// New creates a new TCP transport. +func New(cfg *Config) *Transport { + return &Transport{cfg: cfg} +} + +func (t *Transport) Name() string { return "tcp" } + +func (t *Transport) Start(ctx context.Context) error { + t.mu.Lock() + defer t.mu.Unlock() + + t.server = &http.Server{ + Addr: t.cfg.Addr, + Handler: t.cfg.Handler, + } + + log.I.F("starting listener on http://%s", t.cfg.Addr) + + errCh := make(chan error, 1) + go func() { + if err := t.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- err + } + close(errCh) + }() + + // Give the server a moment to fail on bind errors + select { + case err := <-errCh: + if err != nil { + return fmt.Errorf("tcp listen on %s: %w", t.cfg.Addr, err) + } + default: + } + + return nil +} + +func (t *Transport) Stop(ctx context.Context) error { + t.mu.Lock() + defer t.mu.Unlock() + + if t.server == nil { + return nil + } + return t.server.Shutdown(ctx) +} + +func (t *Transport) Addresses() []string { + return []string{"ws://" + t.cfg.Addr + "/"} +} diff --git a/pkg/transport/tls/tls.go b/pkg/transport/tls/tls.go new file mode 100644 index 0000000..ff8518c --- /dev/null +++ b/pkg/transport/tls/tls.go @@ -0,0 +1,247 @@ +// Package tls provides a TLS/ACME transport for the relay. +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "golang.org/x/crypto/acme/autocert" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" +) + +// Config holds TLS transport configuration. +type Config struct { + // Domains is the list of domains for ACME auto-cert. + Domains []string + // Certs is a list of manual certificate paths (without extension). + // For each path, .pem and .key files are loaded. + Certs []string + // DataDir is the base data directory for the autocert cache. + DataDir string + // Handler is the HTTP handler to serve. + Handler http.Handler +} + +// Transport serves HTTPS with automatic or manual TLS certificates. +// It runs two servers: HTTPS on :443 and HTTP on :80 for ACME challenges. +type Transport struct { + cfg *Config + tlsServer *http.Server + httpServer *http.Server + mu sync.Mutex +} + +// New creates a new TLS transport. +func New(cfg *Config) *Transport { + return &Transport{cfg: cfg} +} + +func (t *Transport) Name() string { return "tls" } + +func (t *Transport) Start(ctx context.Context) error { + t.mu.Lock() + defer t.mu.Unlock() + + if err := ValidateConfig(t.cfg.Domains, t.cfg.Certs); err != nil { + return fmt.Errorf("invalid TLS configuration: %w", err) + } + + // Create cache directory for autocert + cacheDir := filepath.Join(t.cfg.DataDir, "autocert") + if err := os.MkdirAll(cacheDir, 0700); err != nil { + return fmt.Errorf("failed to create autocert cache directory: %w", err) + } + + // Set up autocert manager + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(cacheDir), + HostPolicy: autocert.HostWhitelist(t.cfg.Domains...), + } + + // Create TLS server on port 443 + t.tlsServer = &http.Server{ + Addr: ":443", + Handler: t.cfg.Handler, + TLSConfig: tlsConfig(m, t.cfg.Certs...), + } + + // Create HTTP server for ACME challenges and redirects on port 80 + t.httpServer = &http.Server{ + Addr: ":80", + Handler: m.HTTPHandler(nil), + } + + log.I.F("TLS enabled for domains: %v", t.cfg.Domains) + + // Start TLS server + go func() { + log.I.F("starting TLS listener on https://:443") + if err := t.tlsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.E.F("TLS server error: %v", err) + } + }() + + // Start HTTP server for ACME challenges + go func() { + log.I.F("starting HTTP listener on http://:80 for ACME challenges") + if err := t.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.E.F("HTTP server error: %v", err) + } + }() + + return nil +} + +func (t *Transport) Stop(ctx context.Context) error { + t.mu.Lock() + defer t.mu.Unlock() + + var firstErr error + + if t.tlsServer != nil { + if err := t.tlsServer.Shutdown(ctx); err != nil { + log.E.F("TLS server shutdown error: %v", err) + firstErr = err + } else { + log.I.F("TLS server shutdown completed") + } + } + + if t.httpServer != nil { + if err := t.httpServer.Shutdown(ctx); err != nil { + log.E.F("HTTP server shutdown error: %v", err) + if firstErr == nil { + firstErr = err + } + } else { + log.I.F("HTTP server shutdown completed") + } + } + + return firstErr +} + +func (t *Transport) Addresses() []string { + var addrs []string + for _, domain := range t.cfg.Domains { + addrs = append(addrs, "wss://"+domain+"/") + } + return addrs +} + +// ValidateConfig checks if the TLS configuration is valid. +func ValidateConfig(domains []string, certs []string) error { + if len(domains) == 0 { + return fmt.Errorf("no TLS domains specified") + } + + for _, domain := range domains { + if domain == "" { + continue + } + if strings.Contains(domain, " ") || strings.Contains(domain, "\t") { + return fmt.Errorf("invalid domain name: %s", domain) + } + } + + return nil +} + +// tlsConfig returns a TLS configuration that works with LetsEncrypt automatic +// SSL cert issuer as well as any provided certificate files. +// +// Certs are provided as paths where .pem and .key files exist. +func tlsConfig(m *autocert.Manager, certs ...string) *tls.Config { + certMap := make(map[string]*tls.Certificate) + var mx sync.Mutex + + for _, certPath := range certs { + if certPath == "" { + continue + } + + var err error + var c tls.Certificate + + if c, err = tls.LoadX509KeyPair( + certPath+".pem", certPath+".key", + ); chk.E(err) { + log.E.F("failed to load certificate from %s: %v", certPath, err) + continue + } + + if len(c.Certificate) > 0 { + if x509Cert, err := x509.ParseCertificate(c.Certificate[0]); err == nil { + if x509Cert.Subject.CommonName != "" { + certMap[x509Cert.Subject.CommonName] = &c + log.I.F("loaded certificate for domain: %s", x509Cert.Subject.CommonName) + } + for _, san := range x509Cert.DNSNames { + if san != "" { + certMap[san] = &c + log.I.F("loaded certificate for SAN domain: %s", san) + } + } + } + } + } + + if m == nil { + return &tls.Config{ + GetCertificate: func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { + mx.Lock() + defer mx.Unlock() + + if cert, exists := certMap[helo.ServerName]; exists { + return cert, nil + } + + for domain, cert := range certMap { + if strings.HasPrefix(domain, "*.") { + baseDomain := domain[2:] + if strings.HasSuffix(helo.ServerName, baseDomain) { + return cert, nil + } + } + } + + return nil, fmt.Errorf("no certificate found for %s", helo.ServerName) + }, + } + } + + tc := m.TLSConfig() + tc.GetCertificate = func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { + mx.Lock() + + if cert, exists := certMap[helo.ServerName]; exists { + mx.Unlock() + return cert, nil + } + + for domain, cert := range certMap { + if strings.HasPrefix(domain, "*.") { + baseDomain := domain[2:] + if strings.HasSuffix(helo.ServerName, baseDomain) { + mx.Unlock() + return cert, nil + } + } + } + + mx.Unlock() + + return m.GetCertificate(helo) + } + + return tc +} diff --git a/pkg/transport/tor/tor.go b/pkg/transport/tor/tor.go new file mode 100644 index 0000000..9a8a292 --- /dev/null +++ b/pkg/transport/tor/tor.go @@ -0,0 +1,91 @@ +// Package tor provides a Tor hidden service transport for the relay. +// It wraps the existing pkg/tor service as a pluggable transport. +package tor + +import ( + "context" + "net/http" + + "lol.mleku.dev/log" + + torservice "next.orly.dev/pkg/tor" +) + +// Config holds Tor transport configuration. +type Config struct { + // Port is the internal port for the hidden service. + Port int + // DataDir is the directory for Tor data (torrc, keys, hostname, etc.). + DataDir string + // Binary is the path to the tor executable. + Binary string + // SOCKSPort is the port for outbound SOCKS connections (0 = disabled). + SOCKSPort int + // Handler is the HTTP handler to serve. + Handler http.Handler +} + +// Transport serves the relay as a Tor hidden service. +type Transport struct { + cfg *Config + service *torservice.Service +} + +// New creates a new Tor transport. +func New(cfg *Config) *Transport { + return &Transport{cfg: cfg} +} + +func (t *Transport) Name() string { return "tor" } + +func (t *Transport) Start(ctx context.Context) error { + svcCfg := &torservice.Config{ + Port: t.cfg.Port, + DataDir: t.cfg.DataDir, + Binary: t.cfg.Binary, + SOCKSPort: t.cfg.SOCKSPort, + Handler: t.cfg.Handler, + } + + var err error + t.service, err = torservice.New(svcCfg) + if err != nil { + return err + } + + if err = t.service.Start(); err != nil { + t.service = nil + return err + } + + if addr := t.service.OnionWSAddress(); addr != "" { + log.I.F("Tor hidden service listening on port %d, address: %s", t.cfg.Port, addr) + } else { + log.I.F("Tor hidden service listening on port %d (waiting for .onion address)", t.cfg.Port) + } + + return nil +} + +func (t *Transport) Stop(ctx context.Context) error { + if t.service == nil { + return nil + } + return t.service.Stop() +} + +func (t *Transport) Addresses() []string { + if t.service == nil { + return nil + } + if addr := t.service.OnionWSAddress(); addr != "" { + return []string{addr} + } + return nil +} + +// Service returns the underlying Tor service for access to Tor-specific +// functionality (e.g., OnionAddress, DataDir). +func (t *Transport) Service() *torservice.Service { + return t.service +}