diff options
Diffstat (limited to 'transport/mesh/discovery.go')
| -rw-r--r-- | transport/mesh/discovery.go | 332 |
1 files changed, 332 insertions, 0 deletions
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)) +} |
