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.
 
 
 
 
 
 

188 lines
4.4 KiB

// Package tor provides Tor hidden service integration for the ORLY relay.
// It manages a listener on a dedicated port that receives traffic forwarded
// from the Tor daemon, and exposes the .onion address for NIP-11 integration.
package tor
import (
"context"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// Config holds Tor hidden service configuration.
type Config struct {
// Port is the internal port that Tor forwards .onion traffic to
Port int
// HSDir is the Tor HiddenServiceDir path to read .onion hostname from
HSDir string
// OnionAddress is an optional manual override for the .onion address
OnionAddress string
// Handler is the HTTP handler to serve (typically the main relay handler)
Handler http.Handler
}
// Service manages the Tor hidden service listener.
type Service struct {
cfg *Config
listener net.Listener
server *http.Server
// onionAddress is the detected or configured .onion address
onionAddress string
// hostname watcher
hostnameWatcher *HostnameWatcher
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
}
// New creates a new Tor service with the given configuration.
func New(cfg *Config) (*Service, error) {
if cfg.Port == 0 {
cfg.Port = 3336
}
ctx, cancel := context.WithCancel(context.Background())
s := &Service{
cfg: cfg,
ctx: ctx,
cancel: cancel,
}
// If manual address provided, use it
if cfg.OnionAddress != "" {
s.onionAddress = cfg.OnionAddress
log.I.F("using configured .onion address: %s", s.onionAddress)
}
return s, nil
}
// Start initializes the Tor listener and hostname watcher.
func (s *Service) Start() error {
// Start hostname watcher if HSDir is configured
if s.cfg.HSDir != "" {
s.hostnameWatcher = NewHostnameWatcher(s.cfg.HSDir)
s.hostnameWatcher.OnChange(func(addr string) {
s.mu.Lock()
s.onionAddress = addr
s.mu.Unlock()
log.I.F("detected .onion address: %s", addr)
})
if err := s.hostnameWatcher.Start(); err != nil {
log.W.F("failed to start hostname watcher: %v", err)
} else {
// Get initial address
if addr := s.hostnameWatcher.Address(); addr != "" {
s.mu.Lock()
s.onionAddress = addr
s.mu.Unlock()
}
}
}
// Create listener
addr := fmt.Sprintf("127.0.0.1:%d", s.cfg.Port)
var err error
s.listener, err = net.Listen("tcp", addr)
if chk.E(err) {
return fmt.Errorf("failed to listen on %s: %w", addr, err)
}
// Create HTTP server with the provided handler
s.server = &http.Server{
Handler: s.cfg.Handler,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start serving
s.wg.Add(1)
go func() {
defer s.wg.Done()
log.I.F("Tor listener started on %s", addr)
if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed {
log.E.F("Tor server error: %v", err)
}
}()
return nil
}
// Stop gracefully shuts down the Tor service.
func (s *Service) Stop() error {
s.cancel()
// Stop hostname watcher
if s.hostnameWatcher != nil {
s.hostnameWatcher.Stop()
}
// Shutdown HTTP server
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
}
}
// Close listener
if s.listener != nil {
s.listener.Close()
}
s.wg.Wait()
log.I.F("Tor service stopped")
return nil
}
// OnionAddress returns the current .onion address (without .onion suffix).
func (s *Service) OnionAddress() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.onionAddress
}
// OnionWSAddress returns the full WebSocket URL for the hidden service.
// Format: ws://<address>.onion/
func (s *Service) OnionWSAddress() string {
addr := s.OnionAddress()
if addr == "" {
return ""
}
// Ensure address ends with .onion
if len(addr) >= 6 && addr[len(addr)-6:] != ".onion" {
addr = addr + ".onion"
}
return "ws://" + addr + "/"
}
// IsRunning returns whether the Tor service is currently running.
func (s *Service) IsRunning() bool {
return s.listener != nil
}
// Upgrader returns a WebSocket upgrader configured for Tor connections.
// Tor connections don't send Origin headers, so we skip origin check.
func (s *Service) Upgrader() *websocket.Upgrader {
return &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for Tor
},
}
}