diff options
Diffstat (limited to 'transport/tor')
| -rw-r--r-- | transport/tor/embedded.go | 357 | ||||
| -rw-r--r-- | transport/tor/tor.go | 129 |
2 files changed, 486 insertions, 0 deletions
diff --git a/transport/tor/embedded.go b/transport/tor/embedded.go new file mode 100644 index 0000000..c1800d8 --- /dev/null +++ b/transport/tor/embedded.go @@ -0,0 +1,357 @@ +// Package tor - Embedded Tor implementation for military/war scenarios +package tor + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "time" +) + +// EmbeddedTor manages an embedded Tor instance +type EmbeddedTor struct { + torBinary string // Path to Tor executable + torDataDir string // Tor data directory + torProcess *exec.Cmd // Tor subprocess + controlPort int // Tor control port + socksPort int // Tor SOCKS proxy port + hiddenSvcDir string // Hidden service directory + onionAddress string // Our .onion address + running bool + mu sync.RWMutex + useBridges bool // Use bridges for censorship circumvention + bridges []string // Bridge addresses +} + +// EmbeddedTorConfig holds configuration for embedded Tor +type EmbeddedTorConfig struct { + DataDir string // Where to store Tor data + UseBridges bool // Enable bridge mode (anti-censorship) + Bridges []string // Bridge addresses (obfs4, meek, etc.) + ControlPort int // Control port (0 = auto) + SocksPort int // SOCKS proxy port (0 = auto) + AutoStart bool // Auto-start Tor on creation +} + +// NewEmbeddedTor creates a new embedded Tor instance +func NewEmbeddedTor(config *EmbeddedTorConfig) (*EmbeddedTor, error) { + if config == nil { + config = &EmbeddedTorConfig{ + AutoStart: true, + ControlPort: 0, // Auto-select + SocksPort: 0, // Auto-select + } + } + + // Create data directory + dataDir := config.DataDir + if dataDir == "" { + // Use temp directory + tmpDir := os.TempDir() + dataDir = filepath.Join(tmpDir, "veilith-tor-"+randomString(8)) + } + + if err := os.MkdirAll(dataDir, 0700); err != nil { + return nil, fmt.Errorf("failed to create Tor data dir: %w", err) + } + + et := &EmbeddedTor{ + torDataDir: dataDir, + hiddenSvcDir: filepath.Join(dataDir, "hidden_service"), + controlPort: config.ControlPort, + socksPort: config.SocksPort, + useBridges: config.UseBridges, + bridges: config.Bridges, + } + + // Find Tor binary + torBin, err := et.findTorBinary() + if err != nil { + return nil, fmt.Errorf("Tor binary not found: %w", err) + } + et.torBinary = torBin + + // Auto-assign ports if not specified + if et.controlPort == 0 { + et.controlPort = findFreePort() + } + if et.socksPort == 0 { + et.socksPort = findFreePort() + } + + // Auto-start if requested + if config.AutoStart { + if err := et.Start(); err != nil { + return nil, fmt.Errorf("failed to auto-start Tor: %w", err) + } + } + + return et, nil +} + +// findTorBinary locates the Tor binary +func (et *EmbeddedTor) findTorBinary() (string, error) { + // Try common locations + possiblePaths := []string{ + "tor", // PATH + "/usr/bin/tor", // Linux + "/usr/local/bin/tor", // macOS/BSD + "C:\\Program Files\\Tor Browser\\tor.exe", // Windows + filepath.Join(et.torDataDir, "tor"), // Bundled with app + } + + // Add OS-specific paths + if runtime.GOOS == "windows" { + possiblePaths = append(possiblePaths, "tor.exe") + } + + for _, path := range possiblePaths { + if _, err := os.Stat(path); err == nil { + return path, nil + } + // Also try in PATH + if fullPath, err := exec.LookPath(path); err == nil { + return fullPath, nil + } + } + + return "", fmt.Errorf("Tor binary not found in common locations") +} + +// Start starts the embedded Tor process +func (et *EmbeddedTor) Start() error { + et.mu.Lock() + defer et.mu.Unlock() + + if et.running { + return fmt.Errorf("Tor already running") + } + + // Create torrc configuration + torrcPath := filepath.Join(et.torDataDir, "torrc") + if err := et.writeTorrc(torrcPath); err != nil { + return fmt.Errorf("failed to write torrc: %w", err) + } + + // Launch Tor process + et.torProcess = exec.Command(et.torBinary, "-f", torrcPath) + et.torProcess.Stdout = os.Stdout // For debugging + et.torProcess.Stderr = os.Stderr + + if err := et.torProcess.Start(); err != nil { + return fmt.Errorf("failed to start Tor: %w", err) + } + + et.running = true + + // Wait for Tor to bootstrap + if err := et.waitForBootstrap(60 * time.Second); err != nil { + et.Stop() + return fmt.Errorf("Tor bootstrap failed: %w", err) + } + + // Read hidden service address + if err := et.readOnionAddress(); err != nil { + return fmt.Errorf("failed to read onion address: %w", err) + } + + return nil +} + +// writeTorrc writes Tor configuration file +func (et *EmbeddedTor) writeTorrc(path string) error { + config := fmt.Sprintf(`# Veilith Embedded Tor Configuration + +# Data directory +DataDirectory %s + +# SOCKS proxy +SOCKSPort %d + +# Control port +ControlPort %d + +# Hidden service +HiddenServiceDir %s +HiddenServicePort 8083 127.0.0.1:8083 + +# Security settings +SafeLogging 1 +AvoidDiskWrites 1 + +`, et.torDataDir, et.socksPort, et.controlPort, et.hiddenSvcDir) + + // Add bridges if anti-censorship mode + if et.useBridges && len(et.bridges) > 0 { + config += "\n# Bridge mode (anti-censorship)\n" + config += "UseBridges 1\n" + config += "ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\n" + for _, bridge := range et.bridges { + config += fmt.Sprintf("Bridge %s\n", bridge) + } + } + + return os.WriteFile(path, []byte(config), 0600) +} + +// waitForBootstrap waits for Tor to finish bootstrapping +func (et *EmbeddedTor) waitForBootstrap(timeout time.Duration) error { + start := time.Now() + + for { + if time.Since(start) > timeout { + return fmt.Errorf("bootstrap timeout") + } + + // Try to connect to SOCKS port + conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", et.socksPort), 1*time.Second) + if err == nil { + conn.Close() + // Successfully connected - Tor is ready + return nil + } + + time.Sleep(1 * time.Second) + } +} + +// readOnionAddress reads the hidden service .onion address +func (et *EmbeddedTor) readOnionAddress() error { + hostnamePath := filepath.Join(et.hiddenSvcDir, "hostname") + + // Wait for file to be created (may take a few seconds) + for i := 0; i < 30; i++ { + data, err := os.ReadFile(hostnamePath) + if err == nil { + et.onionAddress = strings.TrimSpace(string(data)) + return nil + } + time.Sleep(1 * time.Second) + } + + return fmt.Errorf("failed to read onion address from %s", hostnamePath) +} + +// Stop stops the embedded Tor process +func (et *EmbeddedTor) Stop() error { + et.mu.Lock() + defer et.mu.Unlock() + + if !et.running { + return nil + } + + if et.torProcess != nil { + // Graceful shutdown + if err := et.torProcess.Process.Signal(os.Interrupt); err != nil { + // Force kill if graceful fails + et.torProcess.Process.Kill() + } + + // Wait for process to exit + et.torProcess.Wait() + } + + et.running = false + return nil +} + +// GetSOCKSProxy returns the SOCKS proxy address +func (et *EmbeddedTor) GetSOCKSProxy() string { + et.mu.RLock() + defer et.mu.RUnlock() + return fmt.Sprintf("127.0.0.1:%d", et.socksPort) +} + +// GetOnionAddress returns our hidden service .onion address +func (et *EmbeddedTor) GetOnionAddress() string { + et.mu.RLock() + defer et.mu.RUnlock() + return et.onionAddress +} + +// IsRunning returns whether Tor is currently running +func (et *EmbeddedTor) IsRunning() bool { + et.mu.RLock() + defer et.mu.RUnlock() + return et.running +} + +// Cleanup removes Tor data directory +func (et *EmbeddedTor) Cleanup() error { + et.Stop() + + if et.torDataDir != "" { + return os.RemoveAll(et.torDataDir) + } + return nil +} + +// SetBridges configures Tor bridges for censorship circumvention +func (et *EmbeddedTor) SetBridges(bridges []string) error { + et.mu.Lock() + defer et.mu.Unlock() + + et.useBridges = true + et.bridges = bridges + + // Restart Tor if running to apply new config + if et.running { + et.mu.Unlock() // Unlock before calling Stop (which locks) + et.Stop() + et.mu.Lock() + return et.Start() + } + + return nil +} + +// GetDefaultBridges returns a list of default obfs4 bridges +func GetDefaultBridges() []string { + return []string{ + "obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1", + "obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0", + "obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0", + } +} + +// findFreePort finds an available TCP port +func findFreePort() int { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 9050 // Fallback to default + } + defer listener.Close() + + addr := listener.Addr().(*net.TCPAddr) + return addr.Port +} + +// randomString generates a random hex string +func randomString(n int) string { + bytes := make([]byte, n) + io.ReadFull(rand.Reader, bytes) + return hex.EncodeToString(bytes) +} + +// String returns a string representation +func (et *EmbeddedTor) String() string { + et.mu.RLock() + defer et.mu.RUnlock() + + status := "stopped" + if et.running { + status = "running" + } + + return fmt.Sprintf("EmbeddedTor(status=%s, socks=%d, onion=%s)", + status, et.socksPort, et.onionAddress) +} diff --git a/transport/tor/tor.go b/transport/tor/tor.go new file mode 100644 index 0000000..a6a1e35 --- /dev/null +++ b/transport/tor/tor.go @@ -0,0 +1,129 @@ +// Package tor implements Tor Hidden Service transport +package tor + +import ( + "fmt" + "net" + "net/url" + + "golang.org/x/net/proxy" + + "veilith/transport" +) + +// TorTransport implements Tor-based networking +type TorTransport struct { + socksProxy string // SOCKS5 proxy address (default: 127.0.0.1:9050) + dialer proxy.Dialer + listener net.Listener + closed bool +} + +// NewTorTransport creates a new Tor transport +func NewTorTransport(socksProxy string) (*TorTransport, error) { + if socksProxy == "" { + socksProxy = "127.0.0.1:9050" // Default Tor SOCKS proxy + } + + // Create SOCKS5 dialer + proxyURL, err := url.Parse("socks5://" + socksProxy) + if err != nil { + return nil, fmt.Errorf("invalid SOCKS proxy address: %w", err) + } + + dialer, err := proxy.FromURL(proxyURL, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("failed to create SOCKS dialer: %w", err) + } + + return &TorTransport{ + socksProxy: socksProxy, + dialer: dialer, + }, nil +} + +// Connect connects to a Tor Hidden Service (.onion address) +func (t *TorTransport) Connect(address string) (net.Conn, error) { + if t.closed { + return nil, fmt.Errorf("tor transport is closed") + } + + // Validate .onion address + if !isOnionAddress(address) { + return nil, fmt.Errorf("invalid .onion address: %s", address) + } + + // Connect via SOCKS proxy + conn, err := t.dialer.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to connect to %s: %w", address, err) + } + + return conn, nil +} + +// Listen starts a Tor Hidden Service listener +// Note: This is a simplified implementation. In production, you'd need to: +// 1. Configure torrc to create a hidden service +// 2. Get the .onion address from Tor control port +// 3. Listen on the local port specified in torrc +func (t *TorTransport) Listen(address string) (net.Listener, error) { + if t.closed { + return nil, fmt.Errorf("tor transport is closed") + } + + if t.listener != nil { + return nil, fmt.Errorf("already listening") + } + + // Listen on localhost (Tor will forward to this) + // In production, parse the address to get the port + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to create listener: %w", err) + } + + t.listener = listener + return listener, nil +} + +// Type returns the transport type +func (t *TorTransport) Type() transport.TransportType { + return transport.TransportTor +} + +// Close closes the Tor transport +func (t *TorTransport) Close() error { + if t.closed { + return nil + } + + t.closed = true + + if t.listener != nil { + if err := t.listener.Close(); err != nil { + return fmt.Errorf("failed to close listener: %w", err) + } + t.listener = nil + } + + return nil +} + +// GetSOCKSProxy returns the SOCKS proxy address +func (t *TorTransport) GetSOCKSProxy() string { + return t.socksProxy +} + +// isOnionAddress checks if an address is a valid .onion address +func isOnionAddress(address string) bool { + // Simple validation: check if it contains ".onion" + // In production, use regex to validate v3 onion addresses (56 chars base32) + return len(address) > 6 && address[len(address)-6:] == ".onion" || + len(address) > 13 && address[len(address)-13:len(address)-7] == ".onion:" +} + +// String returns a string representation +func (t *TorTransport) String() string { + return fmt.Sprintf("TorTransport(proxy=%s, closed=%v)", t.socksProxy, t.closed) +} |
