From 8bbea442f3101ac8cdf66140688eef176367fbd6 Mon Sep 17 00:00:00 2001 From: grendervill Date: Wed, 19 Aug 2026 19:46:56 +0300 Subject: [PATCH] =?UTF-8?q?AeroToss:=20=D0=B0=D0=B2=D1=82=D0=BE-=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2=D1=8B=D1=85=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Desktop автоматически находит все доступные сетевые интерфейсы - Приоритет: приватные IP (192.168.x.x, 10.x.x.x) выше публичных - Фильтрация: loopback, vmnet, veth, utun, awdl исключены - Если интерфейс не работает — пробует следующий - Автоперерегистрация сервиса каждые 30 секунд - Логирование: какой интерфейс выбран, какой порт зарегистрирован --- .../com/aerotoss/discovery/JmdnsDiscovery.kt | 194 +++++++++--------- 1 file changed, 101 insertions(+), 93 deletions(-) diff --git a/app/src/desktopMain/kotlin/com/aerotoss/discovery/JmdnsDiscovery.kt b/app/src/desktopMain/kotlin/com/aerotoss/discovery/JmdnsDiscovery.kt index 5f32aa3..4a9c3c9 100644 --- a/app/src/desktopMain/kotlin/com/aerotoss/discovery/JmdnsDiscovery.kt +++ b/app/src/desktopMain/kotlin/com/aerotoss/discovery/JmdnsDiscovery.kt @@ -22,39 +22,30 @@ class JmdnsDiscovery : DiscoveryManager { private val deviceId = generateDeviceId() private val deviceName = getDeviceName() private val discoveredDevices = ConcurrentHashMap() - private val localServicePort = (10000..60000).random() override fun startDiscovery(servicePort: Int) { if (discoveryThread != null) return discoveryThread = Thread { try { - val localAddr = findLocalAddress() ?: InetAddress.getLocalHost() - println("AeroToss: Starting mDNS on ${localAddr.hostAddress}") + val interfaces = getAvailableInterfaces() + println("AeroToss: Found ${interfaces.size} network interfaces") - val group = InetAddress.getByName("224.0.0.251") - val socket = MulticastSocket(5353) - socket.reuseAddress = true - socket.networkInterface = NetworkInterface.getByInetAddress(localAddr) - socket.joinGroup(java.net.InetSocketAddress(group, 5353), socket.networkInterface) - multicastSocket = socket + if (interfaces.isEmpty()) { + println("AeroToss: No suitable network interfaces found") + return@Thread + } - registerService(socket, group, servicePort) - - val buf = ByteArray(4096) - while (!Thread.currentThread().isInterrupted) { - val packet = DatagramPacket(buf, buf.size) + // Try each interface until we find one that works + for (iface in interfaces) { try { - socket.soTimeout = 2000 - socket.receive(packet) - processPacket(packet.data, packet.length, packet.address) - } catch (_: java.net.SocketTimeoutException) { - // Send announcement periodically - registerService(socket, group, servicePort) + println("AeroToss: Trying interface ${iface.name} (${iface.inetAddresses.toList().map { it.hostAddress }})") + runOnInterface(iface, servicePort) + break // If successful, stay on this interface + } catch (e: Exception) { + println("AeroToss: Interface ${iface.name} failed: ${e.message}") } } - socket.leaveGroup(group) - socket.close() } catch (e: Exception) { if (!Thread.currentThread().isInterrupted) { e.printStackTrace() @@ -68,11 +59,48 @@ class JmdnsDiscovery : DiscoveryManager { } } + private fun runOnInterface(iface: NetworkInterface, servicePort: Int) { + val group = InetAddress.getByName("224.0.0.251") + val socket = MulticastSocket(5353) + socket.reuseAddress = true + socket.networkInterface = iface + socket.joinGroup(java.net.InetSocketAddress(group, 5353), iface) + multicastSocket = socket + + println("AeroToss: Listening on ${iface.name}") + + // Initial registration + registerService(socket, group, servicePort) + + // Listen for queries and respond + val buf = ByteArray(4096) + var lastRegister = System.currentTimeMillis() + + while (!Thread.currentThread().isInterrupted) { + val packet = DatagramPacket(buf, buf.size) + try { + socket.soTimeout = 2000 + socket.receive(packet) + processPacket(packet.data, packet.length, packet.address) + } catch (_: java.net.SocketTimeoutException) { + // Re-register periodically (every 30 seconds) + val now = System.currentTimeMillis() + if (now - lastRegister > 30_000) { + registerService(socket, group, servicePort) + lastRegister = now + } + } + } + socket.leaveGroup(group) + socket.close() + } + private fun registerService(socket: MulticastSocket, group: InetAddress, servicePort: Int) { val response = buildDnsResponse(servicePort) val packet = DatagramPacket(response, response.size, group, 5353) try { socket.send(packet) + println("AeroToss: Registered service on port $servicePort") } catch (_: Exception) {} } @@ -86,43 +114,33 @@ class JmdnsDiscovery : DiscoveryManager { if (anCount == 0) return var offset = 12 - // Skip questions for (i in 0 until anCount) { offset = skipName(data, offset, length) if (offset + 10 > length) return val type = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) - offset += 2 // type + offset += 2 offset += 2 // class offset += 4 // ttl val rdLength = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) offset += 2 - if (type == 12 && offset + rdLength <= length) { - // PTR record - service instance name - val serviceName = parseDnsName(data, offset, length) - if (serviceName.contains("_aerotoss._tcp")) { - // Found our service, request more info - } - } - if (type == 33 && offset + rdLength <= length) { // SRV record val srvData = data.copyOfRange(offset, offset + rdLength) val port = ((srvData[2].toInt() and 0xFF) shl 8) or (srvData[3].toInt() and 0xFF) - val target = parseDnsName(srvData, 6, srvData.size) - val device = Device( - id = fromAddress.hostAddress ?: "unknown", - name = "Device@${fromAddress.hostAddress}", - type = DeviceType.PHONE, - hostAddress = fromAddress.hostAddress ?: "", - port = port - ) - - val key = fromAddress.hostAddress ?: return + val key = fromAddress.hostAddress ?: continue if (!discoveredDevices.containsKey(key)) { + val device = Device( + id = key, + name = "Device@${fromAddress.hostAddress}", + type = DeviceType.PHONE, + hostAddress = fromAddress.hostAddress, + port = port + ) discoveredDevices[key] = device _devices.value = discoveredDevices.values.toList() + println("AeroToss: Discovered device at ${fromAddress.hostAddress}:$port") } } @@ -131,10 +149,9 @@ class JmdnsDiscovery : DiscoveryManager { val txtData = data.copyOfRange(offset, offset + rdLength) val attributes = parseTxtRecord(txtData) val name = attributes["name"] ?: "Device@${fromAddress.hostAddress}" - val id = attributes["id"] ?: fromAddress.hostAddress ?: "unknown" - val portStr = attributes["port"] + val id = attributes["id"] ?: fromAddress.hostAddress - val key = fromAddress.hostAddress ?: return + val key = fromAddress.hostAddress ?: continue val existing = discoveredDevices[key] if (existing != null && id != deviceId) { val updated = existing.copy(name = name, id = id) @@ -158,29 +175,6 @@ class JmdnsDiscovery : DiscoveryManager { return pos } - private fun parseDnsName(data: ByteArray, offset: Int, length: Int): String { - val sb = StringBuilder() - var pos = offset - while (pos < length) { - val len = data[pos].toInt() and 0xFF - if (len == 0) break - if ((len and 0xC0) == 0xC0) { - val ptr = ((len and 0x3F) shl 8) or (data[pos + 1].toInt() and 0xFF) - pos += 2 - val sub = parseDnsName(data, ptr, length) - if (sb.isNotEmpty()) sb.append(".") - sb.append(sub) - break - } - pos++ - if (pos + len > length) break - if (sb.isNotEmpty()) sb.append(".") - sb.append(String(data, pos, len, Charsets.UTF_8)) - pos += len - } - return sb.toString() - } - private fun parseTxtRecord(data: ByteArray): Map { val result = mutableMapOf() var pos = 0 @@ -202,15 +196,15 @@ class JmdnsDiscovery : DiscoveryManager { val baos = java.io.ByteArrayOutputStream() val dos = java.io.DataOutputStream(baos) - // Header: ID=0, Flags=0x8400 (response, authoritative), QDCOUNT=0, ANCOUNT=3, NSCOUNT=0, ARCOUNT=0 - dos.writeShort(0) - dos.writeShort(0x8400) + // Header + dos.writeShort(0) // ID + dos.writeShort(0x8400) // Flags: response, authoritative dos.writeShort(0) // QDCOUNT dos.writeShort(3) // ANCOUNT (PTR + SRV + TXT) dos.writeShort(0) // NSCOUNT dos.writeShort(0) // ARCOUNT - // PTR record: _aerotoss._tcp.local. → AeroToss._aerotoss._tcp.local. + // PTR record writeDnsName(baos, "_aerotoss._tcp.local.") dos.writeShort(12) // PTR dos.writeShort(1) // class IN @@ -221,16 +215,14 @@ class JmdnsDiscovery : DiscoveryManager { dos.writeShort(ptrBytes.size) dos.write(ptrBytes) - // SRV record: AeroToss._aerotoss._tcp.local. → port + // SRV record writeDnsName(baos, "AeroToss._aerotoss._tcp.local.") dos.writeShort(33) // SRV dos.writeShort(1) // class IN dos.writeInt(120) // TTL val srvRd = java.io.ByteArrayOutputStream() - srvRd.write(0) // priority high byte - srvRd.write(0) // priority low byte - srvRd.write(0) // weight high byte - srvRd.write(0) // weight low byte + srvRd.write(0) // priority + srvRd.write(0) // weight srvRd.write((servicePort shr 8) and 0xFF) srvRd.write(servicePort and 0xFF) writeDnsName(srvRd, "AeroToss._aerotoss._tcp.local.") @@ -264,6 +256,38 @@ class JmdnsDiscovery : DiscoveryManager { os.write(0) } + private fun getAvailableInterfaces(): List { + return NetworkInterface.getNetworkInterfaces()?.toList() + ?.filter { iface -> + iface.isUp && + !iface.isLoopback && + !isVirtual(iface) && + iface.inetAddresses.toList().any { it is java.net.Inet4Address } + } + ?.sortedByDescending { iface: NetworkInterface -> + // Prefer interfaces with private IP addresses (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + iface.inetAddresses.toList().any { addr: java.net.InetAddress -> + addr is java.net.Inet4Address && isPrivateAddress(addr) + } + } + ?: emptyList() + } + + private fun isPrivateAddress(addr: java.net.Inet4Address): Boolean { + val bytes = addr.address + return (bytes[0].toInt() and 0xFF == 192 && bytes[1].toInt() and 0xFF == 168) || + (bytes[0].toInt() and 0xFF == 10) || + (bytes[0].toInt() and 0xFF == 172 && (bytes[1].toInt() and 0xFF in 16..31)) + } + + private fun isVirtual(iface: NetworkInterface): Boolean { + return iface.isVirtual || + iface.name.startsWith("vmnet") || + iface.name.startsWith("veth") || + iface.name.startsWith("utun") || + iface.name.startsWith("awdl") + } + override fun stopDiscovery() { discoveryThread?.interrupt() discoveryThread = null @@ -281,20 +305,4 @@ class JmdnsDiscovery : DiscoveryManager { const val SERVICE_TYPE = "_aerotoss._tcp" const val SERVICE_NAME = "AeroToss" } - - private fun findLocalAddress(): InetAddress? { - return try { - NetworkInterface.getNetworkInterfaces()?.toList() - ?.filter { it.isUp && !it.isLoopback && !isVirtual(it) } - ?.flatMap { it.inetAddresses.toList() } - ?.firstOrNull { it is java.net.Inet4Address } - ?: InetAddress.getLocalHost() - } catch (_: Exception) { - try { InetAddress.getLocalHost() } catch (_: Exception) { null } - } - } - - private fun isVirtual(iface: NetworkInterface): Boolean { - return iface.isVirtual || iface.name.startsWith("vmnet") || iface.name.startsWith("veth") - } }