// 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 == "" { 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)) }