1// LanDiscovery.swift2// SophaxChatCore3//4// mDNS/Bonjour discovery — enables automatic iOS ↔ Android peer discovery5// on the same WiFi network, without any central server.6//7// Both platforms advertise "_sophaxchat._tcp." on port 25519.8// On discovery, the resolved address is passed to the delegate which9// initiates a TCP connection. From there the normal Hello / X3DH handshake10// takes over — this file is transport-only, no crypto.1112import Foundation1314public protocol LanDiscoveryDelegate: AnyObject {15/// Called when a peer's TCP address has been resolved. Implementor should16/// call `ChatManager.connectViaTCP(address:)` with the provided address.17 func lanDiscovery(didFind address: String)18}1920public final class LanDiscovery: NSObject {2122// _sophaxchat._tcp. on port 25519 (Curve25519 homage — same as TCPTransport)23 private static let serviceType ="_sophaxchat._tcp."24 private static let port: Int32 = 255192526 private var netService: NetService?27 private var browser: NetServiceBrowser?28 private var myServiceName: String?2930/// Active resolved services — keyed by service name to avoid duplicate TCP connects.31 private var resolved = Set<String>()3233 public weak var delegate: LanDiscoveryDelegate?3435 public override init(){ super.init()}3637// MARK: - Public3839/// Start advertising and browsing. Call from the main thread (or ChatManager's queue).40 public func start(peerID: String){41 myServiceName = peerID4243// Advertise ourselves so others can find us44 let svc = NetService(45 domain:"local.",46 type: Self.serviceType,47 name: peerID,48 port: Self.port49)50 svc.delegate = self51 svc.publish()52 netService = svc5354// Browse for others55 let b = NetServiceBrowser()56 b.delegate = self57 b.searchForServices(ofType: Self.serviceType, inDomain:"local.")58 browser = b59}6061 public func stop(){62 netService?.stop()63 browser?.stop()64 netService = nil65 browser = nil66 resolved.removeAll()67}68}6970// MARK: - NetServiceBrowserDelegate7172extension LanDiscovery: NetServiceBrowserDelegate {7374 public func netServiceBrowser(75 _ browser: NetServiceBrowser,76 didFind service: NetService,77 moreComing: Bool78){79// Skip ourselves80 guard service.name != myServiceName else { return }81// Skip peers we've already connected to this session82 guard !resolved.contains(service.name) else { return }8384 service.delegate = self85 service.resolve(withTimeout: 5.0)86}8788 public func netServiceBrowser(89 _ browser: NetServiceBrowser,90 didRemove service: NetService,91 moreComing: Bool92){93 resolved.remove(service.name)94}9596 public func netServiceBrowser(97 _ browser: NetServiceBrowser,98 didNotSearch errorDict:[String: NSNumber]99){100// Browse error — non-fatal, discovery will be absent until next restart101}102}103104// MARK: - NetServiceDelegate105106extension LanDiscovery: NetServiceDelegate {107108 public func netServiceDidPublish(_ sender: NetService){109// Advertising confirmed — no action needed110}111112 public func netService(_ sender: NetService, didNotPublish errorDict:[String: NSNumber]){113// Publication failure (e.g., port already in use) — non-fatal114}115116 public func netServiceDidResolveAddress(_ sender: NetService){117// `hostName` is the resolved mDNS hostname (e.g. "iPhone.local")118 guard let host = sender.hostName else { return }119 resolved.insert(sender.name)120 delegate?.lanDiscovery(didFind:"\(host):\(sender.port)")121}122123 public func netService(_ sender: NetService, didNotResolve errorDict:[String: NSNumber]){124// Resolution failure — non-fatal, peer will be re-discovered on next browse cycle125}126}