Browse Source

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 <noreply@anthropic.com>
imwald-v0.58.10
woikos 4 months ago
parent
commit
b3e8aad8f0
No known key found for this signature in database
  1. 8
      app/handle-relayinfo.go
  2. 143
      app/main.go
  3. 6
      app/server.go
  4. 132
      app/tls.go
  5. 16
      pkg/interfaces/transport/transport.go
  6. 86
      pkg/transport/manager.go
  7. 78
      pkg/transport/tcp/tcp.go
  8. 247
      pkg/transport/tls/tls.go
  9. 91
      pkg/transport/tor/tor.go

8
app/handle-relayinfo.go

@ -200,11 +200,9 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
addresses = append(addresses, s.Config.RelayAddresses...) addresses = append(addresses, s.Config.RelayAddresses...)
} }
// Add Tor hidden service address if available // Add addresses from all transports (Tor .onion, etc.)
if s.torService != nil { if s.transportMgr != nil {
if onionAddr := s.torService.OnionWSAddress(); onionAddr != "" { addresses = append(addresses, s.transportMgr.Addresses()...)
addresses = append(addresses, onionAddr)
}
} }
// Build graph query config if enabled // Build graph query config if enabled

143
app/main.go

@ -3,7 +3,6 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -11,7 +10,6 @@ import (
"time" "time"
"github.com/adrg/xdg" "github.com/adrg/xdg"
"golang.org/x/crypto/acme/autocert"
"lol.mleku.dev/chk" "lol.mleku.dev/chk"
"lol.mleku.dev/log" "lol.mleku.dev/log"
"next.orly.dev/app/branding" "next.orly.dev/app/branding"
@ -32,9 +30,12 @@ import (
"next.orly.dev/pkg/spider" "next.orly.dev/pkg/spider"
"next.orly.dev/pkg/storage" "next.orly.dev/pkg/storage"
dsync "next.orly.dev/pkg/sync" 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/wireguard"
"next.orly.dev/pkg/archive" "next.orly.dev/pkg/archive"
"next.orly.dev/pkg/tor"
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" "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)) 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() torEnabled, torPort, torDataDir, torBinary, torSOCKSPort := cfg.GetTorConfigValues()
if torEnabled { if torEnabled {
torCfg := &tor.Config{ tt := tortransport.New(&tortransport.Config{
Port: torPort, Port: torPort,
DataDir: torDataDir, DataDir: torDataDir,
Binary: torBinary, Binary: torBinary,
SOCKSPort: torSOCKSPort, SOCKSPort: torSOCKSPort,
Handler: l, Handler: l,
} })
var err error l.transportMgr.Add(tt)
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)
}
}
}
} }
// Start rate limiter if enabled // Start rate limiter if enabled
@ -653,81 +642,26 @@ func Run(
// Wait for database to be ready before accepting requests // Wait for database to be ready before accepting requests
log.I.F("waiting for database warmup to complete...") log.I.F("waiting for database warmup to complete...")
<-db.Ready() <-db.Ready()
log.I.F("database ready, starting HTTP servers") log.I.F("database ready, starting transports")
// Check if TLS is enabled
var tlsEnabled bool
var tlsServer *http.Server
var httpServer *http.Server
// Add TLS or plain TCP transport (mutually exclusive)
if len(cfg.TLSDomains) > 0 { if len(cfg.TLSDomains) > 0 {
// Validate TLS configuration l.transportMgr.Add(tlstransport.New(&tlstransport.Config{
if err = ValidateTLSConfig(cfg.TLSDomains, cfg.Certs); chk.E(err) { Domains: cfg.TLSDomains,
log.E.F("invalid TLS configuration: %v", err) Certs: cfg.Certs,
} else { DataDir: cfg.DataDir,
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, Handler: l,
TLSConfig: TLSConfig(m, cfg.Certs...), }))
} } else {
l.transportMgr.Add(tcp.New(&tcp.Config{
// Create HTTP server for ACME challenges and redirects on port 80 Addr: fmt.Sprintf("%s:%d", cfg.Listen, cfg.Port),
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,
Handler: l, Handler: l,
}))
} }
go func() { // Start all transports
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := l.transportMgr.StartAll(ctx); err != nil {
log.E.F("HTTP server error: %v", err) log.E.F("transport startup failed: %v", err)
}
}()
} }
// Graceful shutdown handler // Graceful shutdown handler
@ -759,12 +693,6 @@ func Run(
log.I.F("archive manager stopped") 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 // Stop garbage collector if running
if l.garbageCollector != nil { if l.garbageCollector != nil {
l.garbageCollector.Stop() l.garbageCollector.Stop()
@ -795,25 +723,12 @@ func Run(
log.I.F("WireGuard server stopped") log.I.F("WireGuard server stopped")
} }
// Create shutdown context with timeout // Stop all transports (TCP/TLS/Tor)
if l.transportMgr != nil {
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 10*time.Second) shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelShutdown() defer cancelShutdown()
if err := l.transportMgr.StopAll(shutdownCtx); err != nil {
// Shutdown TLS server if running log.E.F("transport shutdown error: %v", err)
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")
} }
} }

6
app/server.go

@ -47,7 +47,7 @@ import (
dsync "next.orly.dev/pkg/sync" dsync "next.orly.dev/pkg/sync"
"next.orly.dev/pkg/wireguard" "next.orly.dev/pkg/wireguard"
"next.orly.dev/pkg/archive" "next.orly.dev/pkg/archive"
"next.orly.dev/pkg/tor" "next.orly.dev/pkg/transport"
) )
type Server struct { type Server struct {
@ -122,8 +122,8 @@ type Server struct {
accessTracker *storage.AccessTracker accessTracker *storage.AccessTracker
garbageCollector *storage.GarbageCollector garbageCollector *storage.GarbageCollector
// Tor hidden service // Transport manager for network transports (TCP, TLS, Tor, etc.)
torService *tor.Service transportMgr *transport.Manager
// Branding/white-label customization // Branding/white-label customization
brandingMgr *branding.Manager brandingMgr *branding.Manager

132
app/tls.go

@ -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
}

16
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
}

86
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
}

78
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 + "/"}
}

247
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
}

91
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
}
Loading…
Cancel
Save