diff options
| author | gabrix73 <gabriel1@frozenstar.info> | 2026-04-10 16:10:48 +0200 |
|---|---|---|
| committer | gabrix73 <gabriel1@frozenstar.info> | 2026-04-10 16:10:48 +0200 |
| commit | 88375da30ea667ec25c2000874557fcc4785a558 (patch) | |
| tree | 71364a43230977481c6d3280246aa8bb48562bdc /transport | |
| download | khimera-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.go | 284 | ||||
| -rw-r--r-- | transport/mesh/discovery.go | 332 | ||||
| -rw-r--r-- | transport/mesh/mesh.go | 248 | ||||
| -rw-r--r-- | transport/mesh/storeforward.go | 319 | ||||
| -rw-r--r-- | transport/tor/embedded.go | 357 | ||||
| -rw-r--r-- | transport/tor/tor.go | 129 | ||||
| -rw-r--r-- | transport/transport.go | 163 |
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) +} |
