Compare commits

...

2 Commits

Author SHA1 Message Date
woikos
ea7bc75fac Fix NIP-11 caching and export streaming issues (v0.46.2)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Fix Content-Type header being set on request instead of response
- Add Vary: Accept header to prevent browser/CDN caching NIP-11 for HTML
- Add periodic flushing during export for HTTP streaming (every 100 events)
- Add initial flush after headers to start streaming immediately
- Add X-Content-Type-Options: nosniff to prevent browser buffering

Files modified:
- app/handle-relayinfo.go: Fix header and add Vary: Accept
- app/server.go: Add initial flush and nosniff header for export
- pkg/database/export.go: Add periodic and final flushing for streaming

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 07:17:48 +01:00
woikos
2e9cde01f8 Refactor Tor to subprocess mode, enabled by default (v0.46.1)
Some checks failed
Go / build-and-release (push) Has been cancelled
- Spawn tor binary as subprocess instead of requiring external daemon
- Auto-generate torrc in $ORLY_DATA_DIR/tor/ (userspace, no root)
- Enable Tor by default; gracefully disable if tor binary not found
- Add ORLY_TOR_BINARY and ORLY_TOR_SOCKS config options
- Remove external Tor setup scripts and documentation

Files modified:
- app/config/config.go: New subprocess-based Tor config options
- app/main.go: Updated Tor initialization for new config
- pkg/tor/service.go: Rewritten for subprocess management
- Removed: deploy/orly-tor.service, docs/TOR_SETUP.md, scripts/tor-*.sh

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 06:01:09 +01:00
11 changed files with 279 additions and 837 deletions

View File

@@ -149,11 +149,12 @@ type C struct {
BunkerEnabled bool `env:"ORLY_BUNKER_ENABLED" default:"false" usage:"enable NIP-46 bunker signing service (requires WireGuard)"`
BunkerPort int `env:"ORLY_BUNKER_PORT" default:"3335" usage:"internal port for bunker WebSocket (only accessible via WireGuard)"`
// Tor hidden service configuration
TorEnabled bool `env:"ORLY_TOR_ENABLED" default:"false" usage:"enable Tor hidden service integration (requires external Tor daemon)"`
TorPort int `env:"ORLY_TOR_PORT" default:"3336" usage:"internal port that Tor forwards .onion traffic to"`
TorHSDir string `env:"ORLY_TOR_HS_DIR" usage:"Tor HiddenServiceDir path to read .onion hostname (e.g., /var/lib/tor/orly-relay)"`
TorOnionAddress string `env:"ORLY_TOR_ONION_ADDRESS" usage:"manual .onion address override (optional, auto-detected from TorHSDir if empty)"`
// Tor hidden service configuration (subprocess mode - runs tor binary automatically)
TorEnabled bool `env:"ORLY_TOR_ENABLED" default:"true" usage:"enable Tor hidden service (spawns tor subprocess; disable with false if tor not installed)"`
TorPort int `env:"ORLY_TOR_PORT" default:"3336" usage:"internal port for Tor hidden service traffic"`
TorDataDir string `env:"ORLY_TOR_DATA_DIR" usage:"Tor data directory (default: $ORLY_DATA_DIR/tor)"`
TorBinary string `env:"ORLY_TOR_BINARY" default:"tor" usage:"path to tor binary (default: search in PATH)"`
TorSOCKS int `env:"ORLY_TOR_SOCKS" default:"0" usage:"SOCKS port for outbound Tor connections (0=disabled)"`
// Cashu access token configuration (NIP-XX)
CashuEnabled bool `env:"ORLY_CASHU_ENABLED" default:"false" usage:"enable Cashu blind signature tokens for access control"`
@@ -645,11 +646,17 @@ func (cfg *C) GetStorageConfigValues() (
func (cfg *C) GetTorConfigValues() (
enabled bool,
port int,
hsDir string,
onionAddress string,
dataDir string,
binary string,
socksPort int,
) {
dataDir = cfg.TorDataDir
if dataDir == "" {
dataDir = filepath.Join(cfg.DataDir, "tor")
}
return cfg.TorEnabled,
cfg.TorPort,
cfg.TorHSDir,
cfg.TorOnionAddress
dataDir,
cfg.TorBinary,
cfg.TorSOCKS
}

View File

@@ -37,7 +37,8 @@ type ExtendedRelayInfo struct {
// Informer interface implementation or predefined server configuration. It
// returns this document as a JSON response to the client.
func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "application/json")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Vary", "Accept")
log.D.Ln("handling relay information document")
var info *relayinfo.T
nips := []relayinfo.NIP{

View File

@@ -549,22 +549,24 @@ func Run(
log.I.F("archive relay manager initialized with %d relays", len(archiveRelays))
}
// Initialize Tor hidden service if enabled
torEnabled, torPort, torHSDir, torOnionAddr := cfg.GetTorConfigValues()
// Initialize Tor hidden service if enabled (spawns tor subprocess)
torEnabled, torPort, torDataDir, torBinary, torSOCKSPort := cfg.GetTorConfigValues()
if torEnabled {
torCfg := &tor.Config{
Port: torPort,
HSDir: torHSDir,
OnionAddress: torOnionAddr,
Handler: l,
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("failed to create Tor service: %v", err)
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)

View File

@@ -738,6 +738,12 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Disposition", "attachment; filename=\""+filename+"\"",
)
w.Header().Set("X-Content-Type-Options", "nosniff")
// Flush headers to start streaming immediately
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
// Stream export
s.DB.Export(s.Ctx, w, pks...)

View File

@@ -1,73 +0,0 @@
# ORLY Relay with Tor Hidden Service - Systemd Unit
#
# This is an example systemd unit for running ORLY with Tor support.
# Copy and customize for your deployment.
#
# Installation:
# 1. Copy to /etc/systemd/system/orly-tor.service
# 2. Edit paths and environment variables as needed
# 3. sudo systemctl daemon-reload
# 4. sudo systemctl enable orly-tor
# 5. sudo systemctl start orly-tor
#
# Prerequisites:
# - Tor daemon running (systemctl enable tor && systemctl start tor)
# - Hidden service configured (run scripts/tor-setup.sh)
[Unit]
Description=ORLY Nostr Relay with Tor Hidden Service
Documentation=https://git.mleku.dev/mleku/orly
After=network.target tor.service
Requires=tor.service
Wants=tor.service
[Service]
Type=simple
User=orly
Group=orly
# Working directory
WorkingDirectory=/opt/orly
# Main relay binary
ExecStart=/opt/orly/orly
# Environment configuration
# Core settings
Environment=ORLY_PORT=3334
Environment=ORLY_DATA_DIR=/var/lib/orly
Environment=ORLY_LOG_LEVEL=info
# Tor hidden service settings
Environment=ORLY_TOR_ENABLED=true
Environment=ORLY_TOR_PORT=3336
Environment=ORLY_TOR_HS_DIR=/var/lib/tor/orly-relay
# ACL mode (choose one: none, follows, managed)
Environment=ORLY_ACL_MODE=none
# TLS (optional - uncomment and configure for production)
# Environment=ORLY_TLS_DOMAINS=relay.example.com
# Resource limits
LimitNOFILE=65535
LimitNPROC=4096
# Restart policy
Restart=always
RestartSec=5
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/orly
PrivateTmp=yes
# Allow reading Tor hidden service directory
# Note: The Tor user must grant read access to the orly user
# Option 1: Add orly user to debian-tor group
# Option 2: Use ACLs: setfacl -R -m u:orly:rx /var/lib/tor/orly-relay
[Install]
WantedBy=multi-user.target

View File

@@ -1,294 +0,0 @@
# Tor Hidden Service Setup for ORLY Relay
This guide explains how to configure ORLY to automatically mirror your relay as a Tor hidden service, making it accessible via a `.onion` address.
## Overview
When Tor support is enabled:
1. ORLY listens on a dedicated internal port for Tor traffic
2. The Tor daemon forwards `.onion` traffic to this port
3. ORLY automatically detects the `.onion` address
4. The `.onion` address is included in NIP-11 relay information
## Quick Start
### Development (Local Testing)
```bash
# One-time setup (requires Tor installed)
./scripts/tor-dev-setup.sh
# Start relay with Tor
ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=~/.tor/orly-dev/hidden_service ./orly
```
### Production
```bash
# One-time setup (requires root)
sudo ./scripts/tor-setup.sh
# Start relay with Tor
ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=/var/lib/tor/orly-relay ./orly
```
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `ORLY_TOR_ENABLED` | `false` | Enable Tor hidden service integration |
| `ORLY_TOR_PORT` | `3336` | Internal port Tor forwards traffic to |
| `ORLY_TOR_HS_DIR` | - | Path to Tor's HiddenServiceDir |
| `ORLY_TOR_ONION_ADDRESS` | - | Manual `.onion` override (optional) |
### Example Configurations
**Basic Tor setup:**
```bash
export ORLY_TOR_ENABLED=true
export ORLY_TOR_HS_DIR=/var/lib/tor/orly-relay
./orly
```
**Custom port:**
```bash
export ORLY_TOR_ENABLED=true
export ORLY_TOR_PORT=3337
export ORLY_TOR_HS_DIR=/var/lib/tor/orly-relay
./orly
```
**Manual address (if auto-detection doesn't work):**
```bash
export ORLY_TOR_ENABLED=true
export ORLY_TOR_ONION_ADDRESS=abc123xyz.onion
./orly
```
## How It Works
### Architecture
```
Internet Users Tor Users
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Regular │ │ Tor │
│ Traffic │ │ Network │
│ (HTTPS) │ │ │
└────┬─────┘ └──────┬───────┘
│ │
│ Port 443 │ .onion:80
▼ ▼
┌─────────────────────────────────────┐
│ ORLY Relay │
│ │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ Main Server │ │ Tor Service │ │
│ │ Port 3334 │ │ Port 3336 │ │
│ └──────┬──────┘ └───────┬───────┘ │
│ │ │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌────────────┐ │
│ │ Database │ │
│ └────────────┘ │
└─────────────────────────────────────┘
```
### Address Detection
1. The Tor daemon creates a hidden service directory containing:
- `hostname` - The `.onion` address
- `hs_ed25519_secret_key` - Private key (persistent)
- `hs_ed25519_public_key` - Public key
2. ORLY watches the `hostname` file and automatically detects the address
3. The address is included in NIP-11 relay information under the `addresses` field
### NIP-11 Integration
When Tor is enabled and the `.onion` address is detected, the NIP-11 relay info includes:
```json
{
"name": "ORLY",
"description": "...",
"pubkey": "...",
"addresses": [
"wss://relay.example.com",
"ws://abc123xyz.onion/"
]
}
```
## Manual Tor Configuration
If you prefer to configure Tor manually instead of using the setup scripts:
### 1. Install Tor
**Debian/Ubuntu:**
```bash
sudo apt update && sudo apt install tor
```
**Arch Linux:**
```bash
sudo pacman -S tor
```
**macOS:**
```bash
brew install tor
```
### 2. Configure Hidden Service
Add to `/etc/tor/torrc`:
```
HiddenServiceDir /var/lib/tor/orly-relay/
HiddenServicePort 80 127.0.0.1:3336
```
### 3. Set Permissions
```bash
# Create directory
sudo mkdir -p /var/lib/tor/orly-relay
# Set ownership (Debian/Ubuntu)
sudo chown debian-tor:debian-tor /var/lib/tor/orly-relay
sudo chmod 700 /var/lib/tor/orly-relay
# Or on other systems
sudo chown tor:tor /var/lib/tor/orly-relay
```
### 4. Restart Tor
```bash
sudo systemctl restart tor
```
### 5. Verify
```bash
# Check the .onion address
sudo cat /var/lib/tor/orly-relay/hostname
```
## Systemd Service
For production deployments, use the provided systemd unit:
```bash
# Copy unit file
sudo cp deploy/orly-tor.service /etc/systemd/system/
# Edit configuration
sudo nano /etc/systemd/system/orly-tor.service
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable orly-tor
sudo systemctl start orly-tor
```
### Permissions for Hidden Service Directory
The ORLY process needs read access to the Tor hidden service directory:
**Option 1: Add user to Tor group**
```bash
sudo usermod -aG debian-tor orly
```
**Option 2: Use ACLs**
```bash
sudo setfacl -R -m u:orly:rx /var/lib/tor/orly-relay
```
## Troubleshooting
### .onion address not appearing in NIP-11
1. Check if Tor is running:
```bash
systemctl status tor
```
2. Check if hostname file exists:
```bash
cat /var/lib/tor/orly-relay/hostname
```
3. Check ORLY logs for Tor-related messages
4. Verify environment variables are set:
```bash
echo $ORLY_TOR_ENABLED
echo $ORLY_TOR_HS_DIR
```
### Permission denied errors
Ensure ORLY can read the hidden service directory:
```bash
# Check permissions
ls -la /var/lib/tor/orly-relay/
# Fix with ACL
sudo setfacl -m u:$(whoami):rx /var/lib/tor/orly-relay
```
### Tor connection timeouts
1. Check Tor logs:
```bash
journalctl -u tor -f
```
2. For development, check:
```bash
tail -f ~/.tor/orly-dev/tor.log
```
3. Ensure Tor can reach the network (check firewall rules)
### Different .onion address after restart
This means the hidden service key was lost. The key is stored in:
- Production: `/var/lib/tor/orly-relay/hs_ed25519_secret_key`
- Development: `~/.tor/orly-dev/hidden_service/hs_ed25519_secret_key`
To preserve your `.onion` address, back up the entire hidden service directory.
## Security Considerations
1. **Keep the hidden service key safe** - The `hs_ed25519_secret_key` file is your identity. If compromised, attackers can impersonate your relay.
2. **Restrict file permissions** - Hidden service directories should be `chmod 700`.
3. **Separate Tor traffic** - The dedicated Tor port (3336) keeps Tor traffic isolated from regular traffic.
4. **Regular updates** - Keep Tor updated for security patches.
## Testing with Tor Browser
1. Download Tor Browser from https://www.torproject.org/
2. Navigate to your `.onion` address:
```
ws://your-address.onion/
```
3. Or test with curl over Tor:
```bash
curl --socks5-hostname localhost:9050 -H "Accept: application/nostr+json" http://your-address.onion/
```

View File

@@ -17,6 +17,11 @@ import (
"git.mleku.dev/mleku/nostr/utils/units"
)
// Flusher interface for HTTP streaming
type flusher interface {
Flush()
}
// Export the complete database of stored events to an io.Writer in line structured minified
// JSON. Supports both legacy and compact event formats.
func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
@@ -24,11 +29,18 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
evB := make([]byte, 0, units.Mb)
evBuf := bytes.NewBuffer(evB)
// Get flusher for HTTP streaming if available
var f flusher
if fl, ok := w.(flusher); ok {
f = fl
}
// Performance tracking
startTime := time.Now()
var eventCount, bytesWritten int64
lastLogTime := startTime
const logInterval = 5 * time.Second
const flushInterval = 100 // Flush every N events
log.I.F("export: starting export operation")
@@ -109,6 +121,11 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
eventCount++
ev.Free()
// Flush periodically for HTTP streaming
if f != nil && eventCount%flushInterval == 0 {
f.Flush()
}
// Progress logging every logInterval
if time.Since(lastLogTime) >= logInterval {
elapsed := time.Since(startTime)
@@ -169,6 +186,11 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
eventCount++
ev.Free()
// Flush periodically for HTTP streaming
if f != nil && eventCount%flushInterval == 0 {
f.Flush()
}
// Progress logging every logInterval
if time.Since(lastLogTime) >= logInterval {
elapsed := time.Since(startTime)
@@ -186,6 +208,11 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
return
}
// Final flush
if f != nil {
f.Flush()
}
// Final export summary
elapsed := time.Since(startTime)
eventsPerSec := float64(eventCount) / elapsed.Seconds()
@@ -244,6 +271,11 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
eventCount++
ev.Free()
// Flush periodically for HTTP streaming
if f != nil && eventCount%flushInterval == 0 {
f.Flush()
}
// Progress logging every logInterval
if time.Since(lastLogTime) >= logInterval {
elapsed := time.Since(startTime)
@@ -261,6 +293,11 @@ func (d *D) Export(c context.Context, w io.Writer, pubkeys ...[]byte) {
}
}
// Final flush
if f != nil {
f.Flush()
}
// Final export summary for pubkey export
elapsed := time.Since(startTime)
eventsPerSec := float64(eventCount) / elapsed.Seconds()

View File

@@ -1,13 +1,19 @@
// 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.
// It spawns a tor subprocess with automatic configuration and manages
// the hidden service lifecycle.
package tor
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
@@ -16,25 +22,32 @@ import (
"lol.mleku.dev/log"
)
// Config holds Tor hidden service configuration.
// Config holds Tor subprocess configuration.
type Config struct {
// Port is the internal port that Tor forwards .onion traffic to
// Port is the internal port for the hidden service
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
// 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 (typically the main relay handler)
Handler http.Handler
}
// Service manages the Tor hidden service listener.
// Service manages the Tor subprocess and hidden service listener.
type Service struct {
cfg *Config
listener net.Listener
server *http.Server
// onionAddress is the detected or configured .onion address
// Tor subprocess
cmd *exec.Cmd
stdout io.ReadCloser
stderr io.ReadCloser
// onionAddress is the detected .onion address
onionAddress string
// hostname watcher
@@ -47,11 +60,29 @@ type Service struct {
}
// New creates a new Tor service with the given configuration.
// Returns an error if the tor binary is not found.
func New(cfg *Config) (*Service, error) {
if cfg.Port == 0 {
cfg.Port = 3336
}
// Find tor binary
binary := cfg.Binary
if binary == "" {
binary = "tor"
}
torPath, err := exec.LookPath(binary)
if err != nil {
return nil, fmt.Errorf("tor binary not found: %w (install tor or set ORLY_TOR_ENABLED=false)", err)
}
cfg.Binary = torPath
// Ensure data directory exists
if err := os.MkdirAll(cfg.DataDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create Tor data directory: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
s := &Service{
@@ -60,43 +91,117 @@ func New(cfg *Config) (*Service, error) {
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.
// generateTorrc creates the torrc configuration file.
func (s *Service) generateTorrc() (string, error) {
torrcPath := filepath.Join(s.cfg.DataDir, "torrc")
hsDir := filepath.Join(s.cfg.DataDir, "hidden_service")
// Ensure hidden service directory exists with correct permissions
if err := os.MkdirAll(hsDir, 0700); err != nil {
return "", fmt.Errorf("failed to create hidden service directory: %w", err)
}
var sb strings.Builder
sb.WriteString("# ORLY Tor hidden service configuration\n")
sb.WriteString("# Auto-generated - do not edit\n\n")
// Data directory
sb.WriteString(fmt.Sprintf("DataDirectory %s/data\n", s.cfg.DataDir))
// Hidden service configuration
sb.WriteString(fmt.Sprintf("HiddenServiceDir %s\n", hsDir))
sb.WriteString(fmt.Sprintf("HiddenServicePort 80 127.0.0.1:%d\n", s.cfg.Port))
// Optional SOCKS port for outbound connections
if s.cfg.SOCKSPort > 0 {
sb.WriteString(fmt.Sprintf("SocksPort %d\n", s.cfg.SOCKSPort))
} else {
sb.WriteString("SocksPort 0\n")
}
// Disable unused features to reduce resource usage
sb.WriteString("ControlPort 0\n")
sb.WriteString("Log notice stdout\n")
// Write torrc
if err := os.WriteFile(torrcPath, []byte(sb.String()), 0600); err != nil {
return "", fmt.Errorf("failed to write torrc: %w", err)
}
// Create data subdirectory
if err := os.MkdirAll(filepath.Join(s.cfg.DataDir, "data"), 0700); err != nil {
return "", fmt.Errorf("failed to create Tor data subdirectory: %w", err)
}
return torrcPath, nil
}
// Start spawns the Tor subprocess and initializes the listener.
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) {
// Generate torrc
torrcPath, err := s.generateTorrc()
if err != nil {
return err
}
log.I.F("starting Tor subprocess with config: %s", torrcPath)
// Start tor subprocess
s.cmd = exec.CommandContext(s.ctx, s.cfg.Binary, "-f", torrcPath)
// Capture stdout/stderr for logging
s.stdout, err = s.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to get Tor stdout: %w", err)
}
s.stderr, err = s.cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to get Tor stderr: %w", err)
}
if err := s.cmd.Start(); err != nil {
return fmt.Errorf("failed to start Tor: %w", err)
}
log.I.F("Tor subprocess started (PID %d)", s.cmd.Process.Pid)
// Log Tor output
s.wg.Add(2)
go s.logOutput("tor", s.stdout)
go s.logOutput("tor", s.stderr)
// Monitor subprocess
s.wg.Add(1)
go s.monitorProcess()
// Start hostname watcher
hsDir := filepath.Join(s.cfg.DataDir, "hidden_service")
s.hostnameWatcher = NewHostnameWatcher(hsDir)
s.hostnameWatcher.OnChange(func(addr string) {
s.mu.Lock()
s.onionAddress = addr
s.mu.Unlock()
log.I.F("Tor hidden service 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()
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
// Create listener for the hidden service port
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) {
s.Stop()
return fmt.Errorf("failed to listen on %s: %w", addr, err)
}
@@ -112,7 +217,7 @@ func (s *Service) Start() error {
s.wg.Add(1)
go func() {
defer s.wg.Done()
log.I.F("Tor listener started on %s", addr)
log.I.F("Tor hidden service 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)
}
@@ -121,6 +226,40 @@ func (s *Service) Start() error {
return nil
}
// logOutput reads from a pipe and logs each line.
func (s *Service) logOutput(prefix string, r io.ReadCloser) {
defer s.wg.Done()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
// Filter out common noise
if strings.Contains(line, "Bootstrapped") {
log.I.F("[%s] %s", prefix, line)
} else if strings.Contains(line, "[warn]") || strings.Contains(line, "[err]") {
log.W.F("[%s] %s", prefix, line)
} else {
log.D.F("[%s] %s", prefix, line)
}
}
}
// monitorProcess watches the Tor subprocess and logs when it exits.
func (s *Service) monitorProcess() {
defer s.wg.Done()
err := s.cmd.Wait()
if err != nil {
select {
case <-s.ctx.Done():
// Expected shutdown
log.D.F("Tor subprocess exited (shutdown)")
default:
log.E.F("Tor subprocess exited unexpectedly: %v", err)
}
} else {
log.I.F("Tor subprocess exited cleanly")
}
}
// Stop gracefully shuts down the Tor service.
func (s *Service) Stop() error {
s.cancel()
@@ -135,7 +274,7 @@ func (s *Service) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.server.Shutdown(ctx); chk.E(err) {
return err
// Continue shutdown anyway
}
}
@@ -144,12 +283,33 @@ func (s *Service) Stop() error {
s.listener.Close()
}
// Terminate Tor subprocess
if s.cmd != nil && s.cmd.Process != nil {
log.D.F("sending SIGTERM to Tor subprocess (PID %d)", s.cmd.Process.Pid)
s.cmd.Process.Signal(os.Interrupt)
// Give it a few seconds to exit gracefully
done := make(chan struct{})
go func() {
s.cmd.Wait()
close(done)
}()
select {
case <-done:
log.D.F("Tor subprocess exited gracefully")
case <-time.After(5 * time.Second):
log.W.F("Tor subprocess did not exit, killing")
s.cmd.Process.Kill()
}
}
s.wg.Wait()
log.I.F("Tor service stopped")
return nil
}
// OnionAddress returns the current .onion address (without .onion suffix).
// OnionAddress returns the current .onion address.
func (s *Service) OnionAddress() string {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -172,7 +332,7 @@ func (s *Service) OnionWSAddress() string {
// IsRunning returns whether the Tor service is currently running.
func (s *Service) IsRunning() bool {
return s.listener != nil
return s.listener != nil && s.cmd != nil && s.cmd.Process != nil
}
// Upgrader returns a WebSocket upgrader configured for Tor connections.
@@ -186,3 +346,13 @@ func (s *Service) Upgrader() *websocket.Upgrader {
},
}
}
// DataDir returns the Tor data directory path.
func (s *Service) DataDir() string {
return s.cfg.DataDir
}
// HiddenServiceDir returns the hidden service directory path.
func (s *Service) HiddenServiceDir() string {
return filepath.Join(s.cfg.DataDir, "hidden_service")
}

View File

@@ -1 +1 @@
v0.46.0
v0.46.2

View File

@@ -1,217 +0,0 @@
#!/bin/bash
# tor-dev-setup.sh - Development Tor hidden service setup for ORLY relay
#
# This script sets up a user-space Tor hidden service for local development.
# No root privileges required (except for initial Tor installation).
#
# Usage: ./scripts/tor-dev-setup.sh [port]
# port: internal port ORLY listens on for Tor traffic (default: 3336)
#
# After running this script:
# 1. Start ORLY with: ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=~/.tor/orly-dev ./orly
# 2. Connect via Tor Browser to the .onion address
set -e
# Configuration
TOR_PORT="${1:-3336}"
TOR_DATA_DIR="${HOME}/.tor/orly-dev"
TOR_CONFIG="${TOR_DATA_DIR}/torrc"
TOR_PID_FILE="${TOR_DATA_DIR}/tor.pid"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
debug() { echo -e "${BLUE}[DEBUG]${NC} $1"; }
# Check if Tor is installed
check_tor() {
if ! command -v tor &> /dev/null; then
error "Tor is not installed. Please install it first:
Debian/Ubuntu: sudo apt install tor
Arch: sudo pacman -S tor
macOS: brew install tor
Fedora: sudo dnf install tor"
fi
info "Tor is installed: $(tor --version | head -1)"
}
# Create directory structure
setup_dirs() {
info "Creating directory structure..."
mkdir -p "${TOR_DATA_DIR}"
mkdir -p "${TOR_DATA_DIR}/hidden_service"
chmod 700 "${TOR_DATA_DIR}"
chmod 700 "${TOR_DATA_DIR}/hidden_service"
info "Directory created: ${TOR_DATA_DIR}"
}
# Create Tor configuration
create_config() {
info "Creating Tor configuration..."
cat > "$TOR_CONFIG" << EOF
# ORLY Development Tor Configuration
# Generated by tor-dev-setup.sh on $(date)
# Data directory
DataDirectory ${TOR_DATA_DIR}/data
# Run in background
RunAsDaemon 1
PidFile ${TOR_PID_FILE}
# SOCKS proxy for outgoing connections (optional, for testing)
SocksPort 9150
# Hidden service for ORLY relay
HiddenServiceDir ${TOR_DATA_DIR}/hidden_service/
HiddenServicePort 80 127.0.0.1:${TOR_PORT}
# Logging
Log notice file ${TOR_DATA_DIR}/tor.log
EOF
chmod 600 "$TOR_CONFIG"
info "Configuration created: ${TOR_CONFIG}"
}
# Stop existing Tor instance
stop_tor() {
if [ -f "$TOR_PID_FILE" ]; then
PID=$(cat "$TOR_PID_FILE" 2>/dev/null)
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
info "Stopping existing Tor instance (PID: $PID)..."
kill "$PID" 2>/dev/null || true
sleep 2
fi
rm -f "$TOR_PID_FILE"
fi
}
# Start Tor
start_tor() {
info "Starting Tor..."
# Ensure data directory exists
mkdir -p "${TOR_DATA_DIR}/data"
# Start Tor with our config
tor -f "$TOR_CONFIG" 2>&1 | head -20 &
# Wait for Tor to bootstrap
info "Waiting for Tor to connect to the network..."
for i in {1..60}; do
if [ -f "${TOR_DATA_DIR}/hidden_service/hostname" ]; then
ONION_ADDR=$(cat "${TOR_DATA_DIR}/hidden_service/hostname")
if [ -n "$ONION_ADDR" ]; then
break
fi
fi
# Check if Tor is still running
if [ -f "$TOR_PID_FILE" ]; then
PID=$(cat "$TOR_PID_FILE")
if ! kill -0 "$PID" 2>/dev/null; then
error "Tor process died. Check ${TOR_DATA_DIR}/tor.log"
fi
fi
sleep 1
echo -n "."
done
echo ""
if [ -f "${TOR_DATA_DIR}/hidden_service/hostname" ]; then
ONION_ADDR=$(cat "${TOR_DATA_DIR}/hidden_service/hostname")
info "Tor started successfully"
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}Hidden Service Address:${NC}"
echo -e "${YELLOW}${ONION_ADDR}${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
else
warn "Tor started but hidden service not ready yet"
warn "Check: tail -f ${TOR_DATA_DIR}/tor.log"
fi
}
# Print usage instructions
print_instructions() {
echo ""
info "Development Tor setup complete!"
echo ""
echo " To start ORLY with Tor:"
echo -e " ${BLUE}ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=${TOR_DATA_DIR}/hidden_service ./orly${NC}"
echo ""
echo " To view the .onion address:"
echo -e " ${BLUE}cat ${TOR_DATA_DIR}/hidden_service/hostname${NC}"
echo ""
echo " To view Tor logs:"
echo -e " ${BLUE}tail -f ${TOR_DATA_DIR}/tor.log${NC}"
echo ""
echo " To stop Tor:"
echo -e " ${BLUE}kill \$(cat ${TOR_PID_FILE})${NC}"
echo ""
echo " To restart Tor:"
echo -e " ${BLUE}./scripts/tor-dev-setup.sh${NC}"
echo ""
}
# Status command
status() {
if [ -f "$TOR_PID_FILE" ]; then
PID=$(cat "$TOR_PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
info "Tor is running (PID: $PID)"
if [ -f "${TOR_DATA_DIR}/hidden_service/hostname" ]; then
ONION_ADDR=$(cat "${TOR_DATA_DIR}/hidden_service/hostname")
echo -e " Address: ${YELLOW}${ONION_ADDR}${NC}"
fi
return 0
fi
fi
warn "Tor is not running"
return 1
}
# Main
main() {
case "${1:-}" in
status)
status
exit $?
;;
stop)
stop_tor
info "Tor stopped"
exit 0
;;
*)
;;
esac
info "ORLY Development Tor Setup"
info "Internal port: ${TOR_PORT}"
echo ""
check_tor
setup_dirs
stop_tor
create_config
start_tor
print_instructions
}
main "$@"

View File

@@ -1,197 +0,0 @@
#!/bin/bash
# tor-setup.sh - Production Tor hidden service setup for ORLY relay
#
# This script installs Tor and configures a hidden service for the relay.
# The .onion address will be automatically detected by ORLY.
#
# Usage: sudo ./scripts/tor-setup.sh [port]
# port: internal port ORLY listens on for Tor traffic (default: 3336)
#
# Requirements:
# - Root privileges (for installing packages and configuring Tor)
# - Systemd-based Linux distribution
#
# After running this script:
# 1. Start ORLY with: ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=/var/lib/tor/orly-relay ./orly
# 2. The .onion address will appear in logs and NIP-11
set -e
# Configuration
TOR_PORT="${1:-3336}"
HS_NAME="orly-relay"
HS_DIR="/var/lib/tor/${HS_NAME}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# Check if running as root
if [ "$EUID" -ne 0 ]; then
error "Please run as root: sudo $0"
fi
# Detect package manager and install Tor
install_tor() {
info "Installing Tor..."
if command -v apt-get &> /dev/null; then
# Debian/Ubuntu
apt-get update
apt-get install -y tor
elif command -v dnf &> /dev/null; then
# Fedora/RHEL
dnf install -y tor
elif command -v pacman &> /dev/null; then
# Arch Linux
pacman -Sy --noconfirm tor
elif command -v apk &> /dev/null; then
# Alpine
apk add tor
elif command -v brew &> /dev/null; then
# macOS (run as user, not root)
brew install tor
else
error "Unsupported package manager. Please install Tor manually."
fi
info "Tor installed successfully"
}
# Configure hidden service
configure_tor() {
info "Configuring Tor hidden service..."
TORRC="/etc/tor/torrc"
# Check if hidden service already configured
if grep -q "HiddenServiceDir ${HS_DIR}" "$TORRC" 2>/dev/null; then
warn "Hidden service already configured in ${TORRC}"
return 0
fi
# Backup original torrc
if [ -f "$TORRC" ]; then
cp "$TORRC" "${TORRC}.backup.$(date +%Y%m%d%H%M%S)"
info "Backed up original torrc"
fi
# Add hidden service configuration
cat >> "$TORRC" << EOF
# ORLY Relay Hidden Service
# Added by tor-setup.sh on $(date)
HiddenServiceDir ${HS_DIR}/
HiddenServicePort 80 127.0.0.1:${TOR_PORT}
EOF
info "Hidden service configured: ${HS_DIR}"
}
# Set permissions
set_permissions() {
info "Setting directory permissions..."
# Create hidden service directory if it doesn't exist
mkdir -p "$HS_DIR"
# Set correct ownership (debian-tor on Debian/Ubuntu, tor on others)
if id "debian-tor" &>/dev/null; then
chown -R debian-tor:debian-tor "$HS_DIR"
elif id "tor" &>/dev/null; then
chown -R tor:tor "$HS_DIR"
fi
chmod 700 "$HS_DIR"
info "Permissions set"
}
# Restart Tor service
restart_tor() {
info "Restarting Tor service..."
if command -v systemctl &> /dev/null; then
systemctl enable tor
systemctl restart tor
elif command -v service &> /dev/null; then
service tor restart
else
warn "Could not restart Tor. Please restart manually."
return 1
fi
# Wait for Tor to create the hostname file
info "Waiting for hidden service to initialize..."
for i in {1..30}; do
if [ -f "${HS_DIR}/hostname" ]; then
break
fi
sleep 1
done
if [ -f "${HS_DIR}/hostname" ]; then
ONION_ADDR=$(cat "${HS_DIR}/hostname")
info "Tor service started successfully"
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}Hidden Service Address:${NC}"
echo -e "${YELLOW}${ONION_ADDR}${NC}"
echo -e "${GREEN}======================================${NC}"
echo ""
else
warn "Tor started but hostname file not yet created"
warn "Check: ls -la ${HS_DIR}/"
fi
}
# Print usage instructions
print_instructions() {
echo ""
info "Setup complete! To enable Tor in ORLY:"
echo ""
echo " Option 1 - Environment variables:"
echo " export ORLY_TOR_ENABLED=true"
echo " export ORLY_TOR_HS_DIR=${HS_DIR}"
echo " export ORLY_TOR_PORT=${TOR_PORT}"
echo " ./orly"
echo ""
echo " Option 2 - Command line:"
echo " ORLY_TOR_ENABLED=true ORLY_TOR_HS_DIR=${HS_DIR} ./orly"
echo ""
echo " The .onion address will automatically appear in NIP-11 relay info."
echo ""
echo " To view the .onion address:"
echo " cat ${HS_DIR}/hostname"
echo ""
echo " To check Tor status:"
echo " systemctl status tor"
echo ""
}
# Main
main() {
info "ORLY Tor Hidden Service Setup"
info "Internal port: ${TOR_PORT}"
echo ""
# Check if Tor is already installed
if ! command -v tor &> /dev/null; then
install_tor
else
info "Tor is already installed"
fi
configure_tor
set_permissions
restart_tor
print_instructions
}
main