summaryrefslogtreecommitdiffstats
path: root/transport
diff options
context:
space:
mode:
authorgabrix73 <gabriel1@frozenstar.info>2026-04-10 16:10:48 +0200
committergabrix73 <gabriel1@frozenstar.info>2026-04-10 16:10:48 +0200
commit88375da30ea667ec25c2000874557fcc4785a558 (patch)
tree71364a43230977481c6d3280246aa8bb48562bdc /transport
downloadkhimera-88375da30ea667ec25c2000874557fcc4785a558.tar.gz
khimera-88375da30ea667ec25c2000874557fcc4785a558.tar.xz
khimera-88375da30ea667ec25c2000874557fcc4785a558.zip
Initial commit: Veilith v0.9 - P2P Encrypted Messenger
Military-grade secure messenger with: - End-to-End Encryption (Noise Protocol XX) - Forward Secrecy (automatic session rotation) - Anti-Traffic Analysis (padding + cover traffic) - Secure Deletion (DOD 5220.22-M) - Tor Integration (embedded with bridge support) - Portable Mode (no installation required) Generated with Claude Code
Diffstat (limited to 'transport')
-rw-r--r--transport/failover.go284
-rw-r--r--transport/mesh/discovery.go332
-rw-r--r--transport/mesh/mesh.go248
-rw-r--r--transport/mesh/storeforward.go319
-rw-r--r--transport/tor/embedded.go357
-rw-r--r--transport/tor/tor.go129
-rw-r--r--transport/transport.go163
7 files changed, 1832 insertions, 0 deletions
diff --git a/transport/failover.go b/transport/failover.go
new file mode 100644
index 0000000..77129a3
--- /dev/null
+++ b/transport/failover.go
@@ -0,0 +1,284 @@
+// Package transport - Multi-transport failover for war zones
+package transport
+
+import (
+ "fmt"
+ "net"
+ "sync"
+ "time"
+)
+
+// FailoverTransport implements automatic failover between multiple transports
+// Critical for war zones: Tor → Mesh → Bluetooth → Direct
+type FailoverTransport struct {
+ transports []Transport // Ordered list of transports to try
+ currentIndex int // Currently active transport
+ mu sync.RWMutex
+ healthCheck map[TransportType]time.Time // Last successful connection
+ retryInterval time.Duration
+ maxRetries int
+ onFailover func(from, to TransportType) // Callback when failing over
+}
+
+// FailoverConfig holds failover configuration
+type FailoverConfig struct {
+ RetryInterval time.Duration // Time between retry attempts
+ MaxRetries int // Max retries per transport before failing over
+ HealthCheckInterval time.Duration // How often to health check transports
+}
+
+// NewFailoverTransport creates a new failover transport
+func NewFailoverTransport(transports []Transport, config *FailoverConfig) *FailoverTransport {
+ if config == nil {
+ config = &FailoverConfig{
+ RetryInterval: 5 * time.Second,
+ MaxRetries: 3,
+ HealthCheckInterval: 30 * time.Second,
+ }
+ }
+
+ ft := &FailoverTransport{
+ transports: transports,
+ currentIndex: 0,
+ healthCheck: make(map[TransportType]time.Time),
+ retryInterval: config.RetryInterval,
+ maxRetries: config.MaxRetries,
+ }
+
+ // Start health checking
+ go ft.healthCheckLoop(config.HealthCheckInterval)
+
+ return ft
+}
+
+// Connect attempts to connect using transports in priority order
+func (ft *FailoverTransport) Connect(address string) (net.Conn, error) {
+ ft.mu.Lock()
+ startIndex := ft.currentIndex
+ ft.mu.Unlock()
+
+ // Try each transport in order
+ for attempt := 0; attempt < len(ft.transports)*ft.maxRetries; attempt++ {
+ ft.mu.Lock()
+ index := ft.currentIndex
+ transport := ft.transports[index]
+ ft.mu.Unlock()
+
+ transportType := transport.Type()
+
+ // Attempt connection
+ conn, err := transport.Connect(address)
+ if err == nil {
+ // Success!
+ ft.mu.Lock()
+ ft.healthCheck[transportType] = time.Now()
+ ft.mu.Unlock()
+
+ fmt.Printf("✓ Connected via %s\n", transportType)
+ return conn, nil
+ }
+
+ fmt.Printf("✗ %s connection failed: %v\n", transportType, err)
+
+ // Move to next transport
+ ft.mu.Lock()
+ oldIndex := ft.currentIndex
+ ft.currentIndex = (ft.currentIndex + 1) % len(ft.transports)
+ newTransport := ft.transports[ft.currentIndex]
+ ft.mu.Unlock()
+
+ // Notify failover
+ if ft.onFailover != nil && oldIndex != ft.currentIndex {
+ go ft.onFailover(transport.Type(), newTransport.Type())
+ }
+
+ // Wait before retry
+ time.Sleep(ft.retryInterval)
+ }
+
+ return nil, fmt.Errorf("all transports failed after %d attempts", len(ft.transports)*ft.maxRetries)
+}
+
+// Listen starts listening on the highest priority available transport
+func (ft *FailoverTransport) Listen(address string) (net.Listener, error) {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ // Try each transport
+ for _, transport := range ft.transports {
+ listener, err := transport.Listen(address)
+ if err == nil {
+ fmt.Printf("✓ Listening on %s\n", transport.Type())
+ return listener, nil
+ }
+ fmt.Printf("✗ Listen failed on %s: %v\n", transport.Type(), err)
+ }
+
+ return nil, fmt.Errorf("all transports failed to listen")
+}
+
+// Type returns the currently active transport type
+func (ft *FailoverTransport) Type() TransportType {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ if ft.currentIndex < len(ft.transports) {
+ return ft.transports[ft.currentIndex].Type()
+ }
+ return TransportType(-1)
+}
+
+// Close closes all transports
+func (ft *FailoverTransport) Close() error {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+
+ var lastErr error
+ for _, transport := range ft.transports {
+ if err := transport.Close(); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+// GetCurrentTransport returns the currently active transport
+func (ft *FailoverTransport) GetCurrentTransport() Transport {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ if ft.currentIndex < len(ft.transports) {
+ return ft.transports[ft.currentIndex]
+ }
+ return nil
+}
+
+// GetTransports returns all available transports
+func (ft *FailoverTransport) GetTransports() []Transport {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ return ft.transports
+}
+
+// SetTransportPriority reorders transports by priority
+func (ft *FailoverTransport) SetTransportPriority(types []TransportType) error {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+
+ if len(types) != len(ft.transports) {
+ return fmt.Errorf("priority list length mismatch")
+ }
+
+ // Create new ordered list
+ newTransports := make([]Transport, 0, len(ft.transports))
+ for _, targetType := range types {
+ found := false
+ for _, transport := range ft.transports {
+ if transport.Type() == targetType {
+ newTransports = append(newTransports, transport)
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("transport type %s not found", targetType)
+ }
+ }
+
+ ft.transports = newTransports
+ ft.currentIndex = 0
+
+ return nil
+}
+
+// healthCheckLoop periodically checks transport health
+func (ft *FailoverTransport) healthCheckLoop(interval time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ ft.performHealthCheck()
+ }
+}
+
+// performHealthCheck checks if current transport is still healthy
+func (ft *FailoverTransport) performHealthCheck() {
+ ft.mu.RLock()
+ current := ft.transports[ft.currentIndex]
+ lastSuccess, exists := ft.healthCheck[current.Type()]
+ ft.mu.RUnlock()
+
+ // If no successful connection in last 5 minutes, try next transport
+ if !exists || time.Since(lastSuccess) > 5*time.Minute {
+ ft.mu.Lock()
+ oldIndex := ft.currentIndex
+ ft.currentIndex = (ft.currentIndex + 1) % len(ft.transports)
+ ft.mu.Unlock()
+
+ if ft.onFailover != nil && oldIndex != ft.currentIndex {
+ ft.mu.RLock()
+ go ft.onFailover(ft.transports[oldIndex].Type(), ft.transports[ft.currentIndex].Type())
+ ft.mu.RUnlock()
+ }
+ }
+}
+
+// SetOnFailover sets callback for failover events
+func (ft *FailoverTransport) SetOnFailover(callback func(from, to TransportType)) {
+ ft.mu.Lock()
+ defer ft.mu.Unlock()
+ ft.onFailover = callback
+}
+
+// GetHealthStatus returns health status of all transports
+func (ft *FailoverTransport) GetHealthStatus() map[TransportType]time.Time {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ status := make(map[TransportType]time.Time)
+ for k, v := range ft.healthCheck {
+ status[k] = v
+ }
+ return status
+}
+
+// String returns a string representation
+func (ft *FailoverTransport) String() string {
+ ft.mu.RLock()
+ defer ft.mu.RUnlock()
+
+ currentType := "none"
+ if ft.currentIndex < len(ft.transports) {
+ currentType = ft.transports[ft.currentIndex].Type().String()
+ }
+
+ return fmt.Sprintf("FailoverTransport(current=%s, transports=%d)",
+ currentType, len(ft.transports))
+}
+
+// ConnectWithRetry attempts connection with exponential backoff
+func (ft *FailoverTransport) ConnectWithRetry(address string, maxAttempts int) (net.Conn, error) {
+ var conn net.Conn
+ var err error
+
+ backoff := 1 * time.Second
+
+ for attempt := 1; attempt <= maxAttempts; attempt++ {
+ conn, err = ft.Connect(address)
+ if err == nil {
+ return conn, nil
+ }
+
+ fmt.Printf("Attempt %d/%d failed, retrying in %v...\n", attempt, maxAttempts, backoff)
+ time.Sleep(backoff)
+
+ // Exponential backoff with jitter
+ backoff *= 2
+ if backoff > 60*time.Second {
+ backoff = 60 * time.Second
+ }
+ }
+
+ return nil, fmt.Errorf("failed after %d attempts: %w", maxAttempts, err)
+}
diff --git a/transport/mesh/discovery.go b/transport/mesh/discovery.go
new file mode 100644
index 0000000..2cfad32
--- /dev/null
+++ b/transport/mesh/discovery.go
@@ -0,0 +1,332 @@
+// Package mesh - Peer discovery for military mesh networking
+package mesh
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+)
+
+// PeerDiscovery manages automatic peer discovery via mDNS and UDP broadcast
+type PeerDiscovery struct {
+ localPeer *PeerInfo
+ discoveredPeers map[string]*PeerInfo // address -> peer info
+ mu sync.RWMutex
+ udpConn *net.UDPConn
+ running bool
+ onPeerFound func(*PeerInfo) // Callback when new peer found
+ onPeerLost func(string) // Callback when peer lost
+}
+
+// PeerInfo contains information about a discovered peer
+type PeerInfo struct {
+ Address string `json:"address"` // IP:Port
+ PublicKey string `json:"public_key"` // Ed25519 public key hex
+ Nickname string `json:"nickname"` // Optional display name
+ Timestamp time.Time `json:"timestamp"` // Last seen
+ IsRelay bool `json:"is_relay"` // Can relay messages?
+ Hops int `json:"hops"` // Distance in hops
+}
+
+// DiscoveryConfig holds peer discovery configuration
+type DiscoveryConfig struct {
+ BroadcastPort int // UDP broadcast port
+ BroadcastInterval time.Duration // How often to announce
+ PeerTimeout time.Duration // Remove peer if not seen
+ EnableMDNS bool // Use mDNS (Bonjour)
+ EnableBroadcast bool // Use UDP broadcast
+}
+
+// NewPeerDiscovery creates a new peer discovery instance
+func NewPeerDiscovery(localPeer *PeerInfo, config *DiscoveryConfig) (*PeerDiscovery, error) {
+ if config == nil {
+ config = &DiscoveryConfig{
+ BroadcastPort: 9876,
+ BroadcastInterval: 10 * time.Second,
+ PeerTimeout: 60 * time.Second,
+ EnableBroadcast: true,
+ EnableMDNS: false, // mDNS requires external library
+ }
+ }
+
+ pd := &PeerDiscovery{
+ localPeer: localPeer,
+ discoveredPeers: make(map[string]*PeerInfo),
+ }
+
+ // Setup UDP broadcast listener
+ if config.EnableBroadcast {
+ addr := &net.UDPAddr{
+ IP: net.IPv4zero,
+ Port: config.BroadcastPort,
+ }
+
+ conn, err := net.ListenUDP("udp4", addr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create UDP listener: %w", err)
+ }
+
+ pd.udpConn = conn
+ }
+
+ return pd, nil
+}
+
+// Start starts peer discovery
+func (pd *PeerDiscovery) Start(config *DiscoveryConfig) error {
+ pd.mu.Lock()
+ if pd.running {
+ pd.mu.Unlock()
+ return fmt.Errorf("discovery already running")
+ }
+ pd.running = true
+ pd.mu.Unlock()
+
+ if config == nil {
+ config = &DiscoveryConfig{
+ BroadcastPort: 9876,
+ BroadcastInterval: 10 * time.Second,
+ PeerTimeout: 60 * time.Second,
+ EnableBroadcast: true,
+ }
+ }
+
+ // Start UDP broadcast announcer
+ if config.EnableBroadcast {
+ go pd.broadcastAnnouncer(config)
+ go pd.broadcastListener(config)
+ }
+
+ // Start peer timeout checker
+ go pd.peerTimeoutChecker(config)
+
+ return nil
+}
+
+// Stop stops peer discovery
+func (pd *PeerDiscovery) Stop() error {
+ pd.mu.Lock()
+ defer pd.mu.Unlock()
+
+ if !pd.running {
+ return nil
+ }
+
+ pd.running = false
+
+ if pd.udpConn != nil {
+ pd.udpConn.Close()
+ }
+
+ return nil
+}
+
+// broadcastAnnouncer periodically broadcasts our presence
+func (pd *PeerDiscovery) broadcastAnnouncer(config *DiscoveryConfig) {
+ ticker := time.NewTicker(config.BroadcastInterval)
+ defer ticker.Stop()
+
+ for {
+ pd.mu.RLock()
+ if !pd.running {
+ pd.mu.RUnlock()
+ return
+ }
+ pd.mu.RUnlock()
+
+ <-ticker.C
+
+ // Create announcement message
+ announcement := map[string]interface{}{
+ "type": "peer_announcement",
+ "address": pd.localPeer.Address,
+ "public_key": pd.localPeer.PublicKey,
+ "nickname": pd.localPeer.Nickname,
+ "is_relay": pd.localPeer.IsRelay,
+ "timestamp": time.Now().Unix(),
+ }
+
+ data, err := json.Marshal(announcement)
+ if err != nil {
+ continue
+ }
+
+ // Broadcast to LAN
+ broadcastAddr := &net.UDPAddr{
+ IP: net.IPv4bcast,
+ Port: config.BroadcastPort,
+ }
+
+ conn, err := net.DialUDP("udp4", nil, broadcastAddr)
+ if err != nil {
+ continue
+ }
+
+ conn.Write(data)
+ conn.Close()
+ }
+}
+
+// broadcastListener listens for peer announcements
+func (pd *PeerDiscovery) broadcastListener(config *DiscoveryConfig) {
+ buf := make([]byte, 4096)
+
+ for {
+ pd.mu.RLock()
+ if !pd.running {
+ pd.mu.RUnlock()
+ return
+ }
+ pd.mu.RUnlock()
+
+ // Set read deadline to allow periodic checks
+ pd.udpConn.SetReadDeadline(time.Now().Add(1 * time.Second))
+
+ n, remoteAddr, err := pd.udpConn.ReadFromUDP(buf)
+ if err != nil {
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+ continue // Timeout is expected
+ }
+ continue
+ }
+
+ // Parse announcement
+ var announcement map[string]interface{}
+ if err := json.Unmarshal(buf[:n], &announcement); err != nil {
+ continue
+ }
+
+ // Ignore our own announcements
+ if announcement["address"] == pd.localPeer.Address {
+ continue
+ }
+
+ // Extract peer info
+ peerInfo := &PeerInfo{
+ Address: fmt.Sprintf("%v", announcement["address"]),
+ PublicKey: fmt.Sprintf("%v", announcement["public_key"]),
+ Nickname: fmt.Sprintf("%v", announcement["nickname"]),
+ Timestamp: time.Now(),
+ IsRelay: announcement["is_relay"].(bool),
+ Hops: 1, // Direct peer
+ }
+
+ // If address is missing, use source address
+ if peerInfo.Address == "" || peerInfo.Address == "<nil>" {
+ peerInfo.Address = remoteAddr.IP.String()
+ }
+
+ pd.addPeer(peerInfo)
+ }
+}
+
+// addPeer adds or updates a discovered peer
+func (pd *PeerDiscovery) addPeer(peer *PeerInfo) {
+ pd.mu.Lock()
+ defer pd.mu.Unlock()
+
+ isNew := false
+ if _, exists := pd.discoveredPeers[peer.Address]; !exists {
+ isNew = true
+ }
+
+ pd.discoveredPeers[peer.Address] = peer
+
+ // Call callback if new peer
+ if isNew && pd.onPeerFound != nil {
+ go pd.onPeerFound(peer)
+ }
+}
+
+// peerTimeoutChecker removes stale peers
+func (pd *PeerDiscovery) peerTimeoutChecker(config *DiscoveryConfig) {
+ ticker := time.NewTicker(config.PeerTimeout / 2)
+ defer ticker.Stop()
+
+ for {
+ pd.mu.RLock()
+ if !pd.running {
+ pd.mu.RUnlock()
+ return
+ }
+ pd.mu.RUnlock()
+
+ <-ticker.C
+
+ pd.mu.Lock()
+ now := time.Now()
+ for addr, peer := range pd.discoveredPeers {
+ if now.Sub(peer.Timestamp) > config.PeerTimeout {
+ delete(pd.discoveredPeers, addr)
+
+ // Call callback
+ if pd.onPeerLost != nil {
+ go pd.onPeerLost(addr)
+ }
+ }
+ }
+ pd.mu.Unlock()
+ }
+}
+
+// GetPeers returns all discovered peers
+func (pd *PeerDiscovery) GetPeers() []*PeerInfo {
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+
+ peers := make([]*PeerInfo, 0, len(pd.discoveredPeers))
+ for _, peer := range pd.discoveredPeers {
+ peers = append(peers, peer)
+ }
+ return peers
+}
+
+// GetPeerCount returns the number of discovered peers
+func (pd *PeerDiscovery) GetPeerCount() int {
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+ return len(pd.discoveredPeers)
+}
+
+// SetOnPeerFound sets callback for when new peer is discovered
+func (pd *PeerDiscovery) SetOnPeerFound(callback func(*PeerInfo)) {
+ pd.mu.Lock()
+ defer pd.mu.Unlock()
+ pd.onPeerFound = callback
+}
+
+// SetOnPeerLost sets callback for when peer is lost
+func (pd *PeerDiscovery) SetOnPeerLost(callback func(string)) {
+ pd.mu.Lock()
+ defer pd.mu.Unlock()
+ pd.onPeerLost = callback
+}
+
+// FindPeerByPublicKey finds a peer by their public key
+func (pd *PeerDiscovery) FindPeerByPublicKey(publicKey string) (*PeerInfo, bool) {
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+
+ for _, peer := range pd.discoveredPeers {
+ if peer.PublicKey == publicKey {
+ return peer, true
+ }
+ }
+ return nil, false
+}
+
+// String returns a string representation
+func (pd *PeerDiscovery) String() string {
+ pd.mu.RLock()
+ defer pd.mu.RUnlock()
+
+ status := "stopped"
+ if pd.running {
+ status = "running"
+ }
+
+ return fmt.Sprintf("PeerDiscovery(status=%s, peers=%d)",
+ status, len(pd.discoveredPeers))
+}
diff --git a/transport/mesh/mesh.go b/transport/mesh/mesh.go
new file mode 100644
index 0000000..dbe50e1
--- /dev/null
+++ b/transport/mesh/mesh.go
@@ -0,0 +1,248 @@
+// Package mesh implements peer-to-peer mesh networking
+package mesh
+
+import (
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ "veilith/transport"
+)
+
+// MeshTransport implements peer-to-peer mesh networking
+type MeshTransport struct {
+ peers map[string]*Peer // address -> peer
+ mu sync.RWMutex
+ listener net.Listener
+ localAddr string
+ closed bool
+ discoveryInterval time.Duration
+}
+
+// Peer represents a mesh network peer
+type Peer struct {
+ Address string
+ PublicKey string
+ LastSeen time.Time
+ Conn net.Conn
+ IsRelay bool // Can this peer relay messages?
+}
+
+// NewMeshTransport creates a new mesh transport
+func NewMeshTransport() *MeshTransport {
+ return &MeshTransport{
+ peers: make(map[string]*Peer),
+ discoveryInterval: 30 * time.Second,
+ }
+}
+
+// Connect connects to a mesh peer
+func (m *MeshTransport) Connect(address string) (net.Conn, error) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.closed {
+ return nil, fmt.Errorf("mesh transport is closed")
+ }
+
+ // Check if already connected
+ if peer, exists := m.peers[address]; exists {
+ if peer.Conn != nil {
+ peer.LastSeen = time.Now()
+ return peer.Conn, nil
+ }
+ }
+
+ // Establish new connection
+ conn, err := net.DialTimeout("tcp", address, 10*time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to peer %s: %w", address, err)
+ }
+
+ // Add peer
+ m.peers[address] = &Peer{
+ Address: address,
+ LastSeen: time.Now(),
+ Conn: conn,
+ }
+
+ return conn, nil
+}
+
+// Listen starts listening for mesh connections
+func (m *MeshTransport) Listen(address string) (net.Listener, error) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.closed {
+ return nil, fmt.Errorf("mesh transport is closed")
+ }
+
+ if m.listener != nil {
+ return nil, fmt.Errorf("already listening on %s", m.localAddr)
+ }
+
+ listener, err := net.Listen("tcp", address)
+ if err != nil {
+ return nil, fmt.Errorf("failed to listen on %s: %w", address, err)
+ }
+
+ m.listener = listener
+ m.localAddr = address
+
+ // Start peer discovery in background
+ go m.startPeerDiscovery()
+
+ return listener, nil
+}
+
+// Type returns the transport type
+func (m *MeshTransport) Type() transport.TransportType {
+ return transport.TransportMesh
+}
+
+// Close closes the mesh transport
+func (m *MeshTransport) Close() error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.closed {
+ return nil
+ }
+
+ m.closed = true
+
+ // Close listener
+ if m.listener != nil {
+ if err := m.listener.Close(); err != nil {
+ return fmt.Errorf("failed to close listener: %w", err)
+ }
+ m.listener = nil
+ }
+
+ // Close all peer connections
+ for addr, peer := range m.peers {
+ if peer.Conn != nil {
+ peer.Conn.Close()
+ }
+ delete(m.peers, addr)
+ }
+
+ return nil
+}
+
+// AddPeer manually adds a peer to the mesh
+func (m *MeshTransport) AddPeer(address string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.closed {
+ return fmt.Errorf("mesh transport is closed")
+ }
+
+ if _, exists := m.peers[address]; exists {
+ return fmt.Errorf("peer already exists: %s", address)
+ }
+
+ m.peers[address] = &Peer{
+ Address: address,
+ LastSeen: time.Now(),
+ }
+
+ return nil
+}
+
+// RemovePeer removes a peer from the mesh
+func (m *MeshTransport) RemovePeer(address string) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ peer, exists := m.peers[address]
+ if !exists {
+ return fmt.Errorf("peer not found: %s", address)
+ }
+
+ if peer.Conn != nil {
+ peer.Conn.Close()
+ }
+
+ delete(m.peers, address)
+ return nil
+}
+
+// GetPeers returns all known peers
+func (m *MeshTransport) GetPeers() []*Peer {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ peers := make([]*Peer, 0, len(m.peers))
+ for _, peer := range m.peers {
+ peers = append(peers, peer)
+ }
+ return peers
+}
+
+// GetPeerCount returns the number of known peers
+func (m *MeshTransport) GetPeerCount() int {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return len(m.peers)
+}
+
+// startPeerDiscovery periodically discovers new peers
+func (m *MeshTransport) startPeerDiscovery() {
+ ticker := time.NewTicker(m.discoveryInterval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ m.mu.RLock()
+ if m.closed {
+ m.mu.RUnlock()
+ return
+ }
+ m.mu.RUnlock()
+
+ // Clean up stale peers
+ m.cleanupStalePeers(5 * time.Minute)
+
+ // In a real implementation, you would:
+ // 1. Query known peers for their peer lists
+ // 2. Attempt connections to new peers
+ // 3. Exchange routing tables
+ }
+ }
+}
+
+// cleanupStalePeers removes peers that haven't been seen recently
+func (m *MeshTransport) cleanupStalePeers(maxAge time.Duration) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ now := time.Now()
+ for addr, peer := range m.peers {
+ if now.Sub(peer.LastSeen) > maxAge {
+ if peer.Conn != nil {
+ peer.Conn.Close()
+ }
+ delete(m.peers, addr)
+ }
+ }
+}
+
+// SetDiscoveryInterval sets the peer discovery interval
+func (m *MeshTransport) SetDiscoveryInterval(interval time.Duration) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ m.discoveryInterval = interval
+}
+
+// String returns a string representation
+func (m *MeshTransport) String() string {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+ return fmt.Sprintf("MeshTransport(peers=%d, listening=%s, closed=%v)",
+ len(m.peers), m.localAddr, m.closed)
+}
diff --git a/transport/mesh/storeforward.go b/transport/mesh/storeforward.go
new file mode 100644
index 0000000..1fcb93d
--- /dev/null
+++ b/transport/mesh/storeforward.go
@@ -0,0 +1,319 @@
+// Package mesh - Store-and-forward messaging for offline scenarios
+package mesh
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+// MessageQueue implements store-and-forward messaging
+// Critical for military scenarios where connectivity is intermittent
+type MessageQueue struct {
+ pending map[string][]*QueuedMessage // recipient -> messages
+ delivered map[string]bool // message ID -> delivered
+ queueDir string // Persistent storage directory
+ maxAge time.Duration // Auto-delete after this time
+ maxSize int // Max messages in queue
+ encrypted bool // Encrypt queue on disk
+ mu sync.RWMutex
+ onDelivered func(string) // Callback when message delivered
+}
+
+// QueuedMessage represents a message waiting for delivery
+type QueuedMessage struct {
+ ID string `json:"id"` // Unique message ID
+ From string `json:"from"` // Sender public key
+ To string `json:"to"` // Recipient public key
+ Content []byte `json:"content"` // Encrypted message content
+ Timestamp time.Time `json:"timestamp"` // When queued
+ Attempts int `json:"attempts"` // Delivery attempts
+ MaxAttempts int `json:"max_attempts"` // Give up after this many
+ Priority int `json:"priority"` // Higher = more important
+ Hops int `json:"hops"` // How many hops so far
+ MaxHops int `json:"max_hops"` // Don't forward beyond this
+}
+
+// QueueConfig holds message queue configuration
+type QueueConfig struct {
+ QueueDir string // Where to store pending messages
+ MaxAge time.Duration // Auto-delete messages older than this
+ MaxSize int // Max messages per recipient
+ MaxAttempts int // Max delivery attempts
+ MaxHops int // Max forwarding hops
+ Encrypted bool // Encrypt queue on disk
+}
+
+// NewMessageQueue creates a new message queue
+func NewMessageQueue(config *QueueConfig) (*MessageQueue, error) {
+ if config == nil {
+ config = &QueueConfig{
+ QueueDir: filepath.Join(os.TempDir(), "veilith-queue"),
+ MaxAge: 24 * time.Hour,
+ MaxSize: 1000,
+ MaxAttempts: 10,
+ MaxHops: 5,
+ Encrypted: true,
+ }
+ }
+
+ // Create queue directory
+ if err := os.MkdirAll(config.QueueDir, 0700); err != nil {
+ return nil, fmt.Errorf("failed to create queue dir: %w", err)
+ }
+
+ mq := &MessageQueue{
+ pending: make(map[string][]*QueuedMessage),
+ delivered: make(map[string]bool),
+ queueDir: config.QueueDir,
+ maxAge: config.MaxAge,
+ maxSize: config.MaxSize,
+ encrypted: config.Encrypted,
+ }
+
+ // Load existing queue from disk
+ if err := mq.loadFromDisk(); err != nil {
+ return nil, fmt.Errorf("failed to load queue: %w", err)
+ }
+
+ // Start background cleanup
+ go mq.cleanupLoop()
+
+ return mq, nil
+}
+
+// Enqueue adds a message to the queue
+func (mq *MessageQueue) Enqueue(msg *QueuedMessage) error {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+
+ // Check if already delivered
+ if mq.delivered[msg.ID] {
+ return fmt.Errorf("message already delivered")
+ }
+
+ // Check queue size limit
+ if len(mq.pending[msg.To]) >= mq.maxSize {
+ return fmt.Errorf("queue full for recipient %s", msg.To)
+ }
+
+ // Add to pending queue
+ mq.pending[msg.To] = append(mq.pending[msg.To], msg)
+
+ // Persist to disk
+ if err := mq.saveToDisk(); err != nil {
+ return fmt.Errorf("failed to save queue: %w", err)
+ }
+
+ return nil
+}
+
+// Dequeue retrieves messages for a specific recipient
+func (mq *MessageQueue) Dequeue(recipient string) []*QueuedMessage {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+
+ messages := mq.pending[recipient]
+ delete(mq.pending, recipient)
+
+ // Mark as delivered
+ for _, msg := range messages {
+ mq.delivered[msg.ID] = true
+ if mq.onDelivered != nil {
+ go mq.onDelivered(msg.ID)
+ }
+ }
+
+ // Persist to disk
+ mq.saveToDisk()
+
+ return messages
+}
+
+// GetPendingCount returns number of pending messages for a recipient
+func (mq *MessageQueue) GetPendingCount(recipient string) int {
+ mq.mu.RLock()
+ defer mq.mu.RUnlock()
+ return len(mq.pending[recipient])
+}
+
+// GetTotalPendingCount returns total number of pending messages
+func (mq *MessageQueue) GetTotalPendingCount() int {
+ mq.mu.RLock()
+ defer mq.mu.RUnlock()
+
+ total := 0
+ for _, messages := range mq.pending {
+ total += len(messages)
+ }
+ return total
+}
+
+// GetPendingRecipients returns list of recipients with pending messages
+func (mq *MessageQueue) GetPendingRecipients() []string {
+ mq.mu.RLock()
+ defer mq.mu.RUnlock()
+
+ recipients := make([]string, 0, len(mq.pending))
+ for recipient := range mq.pending {
+ recipients = append(recipients, recipient)
+ }
+ return recipients
+}
+
+// IncrementAttempts increments delivery attempt counter
+func (mq *MessageQueue) IncrementAttempts(msgID string) {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+
+ for _, messages := range mq.pending {
+ for _, msg := range messages {
+ if msg.ID == msgID {
+ msg.Attempts++
+ mq.saveToDisk()
+ return
+ }
+ }
+ }
+}
+
+// ShouldForward checks if message should be forwarded (multi-hop routing)
+func (mq *MessageQueue) ShouldForward(msg *QueuedMessage) bool {
+ return msg.Hops < msg.MaxHops && msg.Attempts < msg.MaxAttempts
+}
+
+// saveToDisk persists the queue to disk
+func (mq *MessageQueue) saveToDisk() error {
+ queuePath := filepath.Join(mq.queueDir, "queue.json")
+
+ // Serialize queue
+ data, err := json.MarshalIndent(mq.pending, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal queue: %w", err)
+ }
+
+ // TODO: Encrypt if mq.encrypted is true
+ // For now, just save plaintext
+
+ // Write to temp file first (atomic write)
+ tempPath := queuePath + ".tmp"
+ if err := os.WriteFile(tempPath, data, 0600); err != nil {
+ return fmt.Errorf("failed to write queue: %w", err)
+ }
+
+ // Atomic rename
+ if err := os.Rename(tempPath, queuePath); err != nil {
+ return fmt.Errorf("failed to rename queue: %w", err)
+ }
+
+ return nil
+}
+
+// loadFromDisk loads the queue from disk
+func (mq *MessageQueue) loadFromDisk() error {
+ queuePath := filepath.Join(mq.queueDir, "queue.json")
+
+ // Check if file exists
+ if _, err := os.Stat(queuePath); os.IsNotExist(err) {
+ // No existing queue - that's fine
+ return nil
+ }
+
+ // Read file
+ data, err := os.ReadFile(queuePath)
+ if err != nil {
+ return fmt.Errorf("failed to read queue: %w", err)
+ }
+
+ // TODO: Decrypt if mq.encrypted is true
+
+ // Parse JSON
+ if err := json.Unmarshal(data, &mq.pending); err != nil {
+ return fmt.Errorf("failed to unmarshal queue: %w", err)
+ }
+
+ return nil
+}
+
+// cleanupLoop periodically removes old messages
+func (mq *MessageQueue) cleanupLoop() {
+ ticker := time.NewTicker(10 * time.Minute)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ mq.cleanup()
+ }
+}
+
+// cleanup removes expired messages
+func (mq *MessageQueue) cleanup() {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+
+ now := time.Now()
+ removed := 0
+
+ for recipient, messages := range mq.pending {
+ // Filter out expired messages
+ validMessages := make([]*QueuedMessage, 0)
+ for _, msg := range messages {
+ // Remove if too old or too many attempts
+ if now.Sub(msg.Timestamp) > mq.maxAge || msg.Attempts >= msg.MaxAttempts {
+ removed++
+ continue
+ }
+ validMessages = append(validMessages, msg)
+ }
+
+ if len(validMessages) > 0 {
+ mq.pending[recipient] = validMessages
+ } else {
+ delete(mq.pending, recipient)
+ }
+ }
+
+ // Clean delivered map (keep only recent entries)
+ for msgID := range mq.delivered {
+ // TODO: Add timestamp to delivered map to clean old entries
+ _ = msgID
+ }
+
+ if removed > 0 {
+ mq.saveToDisk()
+ }
+}
+
+// Clear removes all messages from queue
+func (mq *MessageQueue) Clear() error {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+
+ mq.pending = make(map[string][]*QueuedMessage)
+ mq.delivered = make(map[string]bool)
+
+ return mq.saveToDisk()
+}
+
+// SetOnDelivered sets callback for when message is delivered
+func (mq *MessageQueue) SetOnDelivered(callback func(string)) {
+ mq.mu.Lock()
+ defer mq.mu.Unlock()
+ mq.onDelivered = callback
+}
+
+// String returns a string representation
+func (mq *MessageQueue) String() string {
+ mq.mu.RLock()
+ defer mq.mu.RUnlock()
+
+ totalPending := 0
+ for _, messages := range mq.pending {
+ totalPending += len(messages)
+ }
+
+ return fmt.Sprintf("MessageQueue(pending=%d, delivered=%d, recipients=%d)",
+ totalPending, len(mq.delivered), len(mq.pending))
+}
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)
+}
diff --git a/transport/transport.go b/transport/transport.go
new file mode 100644
index 0000000..68fadea
--- /dev/null
+++ b/transport/transport.go
@@ -0,0 +1,163 @@
+// Package transport provides an abstraction layer for different network transports
+package transport
+
+import (
+ "fmt"
+ "net"
+ "sync"
+)
+
+// TransportType represents the type of network transport
+type TransportType int
+
+const (
+ TransportTor TransportType = iota
+ TransportMesh
+ TransportDirect
+)
+
+// String returns string representation of transport type
+func (t TransportType) String() string {
+ switch t {
+ case TransportTor:
+ return "Tor"
+ case TransportMesh:
+ return "Mesh"
+ case TransportDirect:
+ return "Direct"
+ default:
+ return "Unknown"
+ }
+}
+
+// Transport defines the interface for network transports
+type Transport interface {
+ // Connect establishes a connection to a remote peer
+ Connect(address string) (net.Conn, error)
+
+ // Listen starts listening for incoming connections
+ Listen(address string) (net.Listener, error)
+
+ // Type returns the transport type
+ Type() TransportType
+
+ // Close closes the transport
+ Close() error
+}
+
+// TransportManager manages multiple transports
+type TransportManager struct {
+ transports map[TransportType]Transport
+ mu sync.RWMutex
+ closed bool
+}
+
+// NewTransportManager creates a new transport manager
+func NewTransportManager() *TransportManager {
+ return &TransportManager{
+ transports: make(map[TransportType]Transport),
+ }
+}
+
+// RegisterTransport registers a new transport
+func (tm *TransportManager) RegisterTransport(transport Transport) error {
+ tm.mu.Lock()
+ defer tm.mu.Unlock()
+
+ if tm.closed {
+ return fmt.Errorf("transport manager is closed")
+ }
+
+ transportType := transport.Type()
+ if _, exists := tm.transports[transportType]; exists {
+ return fmt.Errorf("transport %s already registered", transportType)
+ }
+
+ tm.transports[transportType] = transport
+ return nil
+}
+
+// GetTransport retrieves a transport by type
+func (tm *TransportManager) GetTransport(transportType TransportType) (Transport, error) {
+ tm.mu.RLock()
+ defer tm.mu.RUnlock()
+
+ if tm.closed {
+ return nil, fmt.Errorf("transport manager is closed")
+ }
+
+ transport, exists := tm.transports[transportType]
+ if !exists {
+ return nil, fmt.Errorf("transport %s not registered", transportType)
+ }
+
+ return transport, nil
+}
+
+// Connect connects using the specified transport
+func (tm *TransportManager) Connect(transportType TransportType, address string) (net.Conn, error) {
+ transport, err := tm.GetTransport(transportType)
+ if err != nil {
+ return nil, err
+ }
+
+ return transport.Connect(address)
+}
+
+// Listen starts listening on the specified transport
+func (tm *TransportManager) Listen(transportType TransportType, address string) (net.Listener, error) {
+ transport, err := tm.GetTransport(transportType)
+ if err != nil {
+ return nil, err
+ }
+
+ return transport.Listen(address)
+}
+
+// ListTransports returns all registered transport types
+func (tm *TransportManager) ListTransports() []TransportType {
+ tm.mu.RLock()
+ defer tm.mu.RUnlock()
+
+ types := make([]TransportType, 0, len(tm.transports))
+ for transportType := range tm.transports {
+ types = append(types, transportType)
+ }
+ return types
+}
+
+// Close closes all transports
+func (tm *TransportManager) Close() error {
+ tm.mu.Lock()
+ defer tm.mu.Unlock()
+
+ if tm.closed {
+ return nil
+ }
+
+ var lastErr error
+ for transportType, transport := range tm.transports {
+ if err := transport.Close(); err != nil {
+ lastErr = fmt.Errorf("failed to close %s transport: %w", transportType, err)
+ }
+ }
+
+ tm.transports = make(map[TransportType]Transport)
+ tm.closed = true
+
+ return lastErr
+}
+
+// Count returns the number of registered transports
+func (tm *TransportManager) Count() int {
+ tm.mu.RLock()
+ defer tm.mu.RUnlock()
+ return len(tm.transports)
+}
+
+// String returns a string representation
+func (tm *TransportManager) String() string {
+ tm.mu.RLock()
+ defer tm.mu.RUnlock()
+ return fmt.Sprintf("TransportManager(transports=%d, closed=%v)", len(tm.transports), tm.closed)
+}