AeroToss: авто-сканирование сетевых интерфейсов

- Desktop автоматически находит все доступные сетевые интерфейсы
- Приоритет: приватные IP (192.168.x.x, 10.x.x.x) выше публичных
- Фильтрация: loopback, vmnet, veth, utun, awdl исключены
- Если интерфейс не работает — пробует следующий
- Автоперерегистрация сервиса каждые 30 секунд
- Логирование: какой интерфейс выбран, какой порт зарегистрирован
This commit is contained in:
2026-08-19 19:46:56 +03:00
parent 518827ea95
commit 8bbea442f3
@@ -22,39 +22,30 @@ class JmdnsDiscovery : DiscoveryManager {
private val deviceId = generateDeviceId() private val deviceId = generateDeviceId()
private val deviceName = getDeviceName() private val deviceName = getDeviceName()
private val discoveredDevices = ConcurrentHashMap<String, Device>() private val discoveredDevices = ConcurrentHashMap<String, Device>()
private val localServicePort = (10000..60000).random()
override fun startDiscovery(servicePort: Int) { override fun startDiscovery(servicePort: Int) {
if (discoveryThread != null) return if (discoveryThread != null) return
discoveryThread = Thread { discoveryThread = Thread {
try { try {
val localAddr = findLocalAddress() ?: InetAddress.getLocalHost() val interfaces = getAvailableInterfaces()
println("AeroToss: Starting mDNS on ${localAddr.hostAddress}") println("AeroToss: Found ${interfaces.size} network interfaces")
val group = InetAddress.getByName("224.0.0.251") if (interfaces.isEmpty()) {
val socket = MulticastSocket(5353) println("AeroToss: No suitable network interfaces found")
socket.reuseAddress = true return@Thread
socket.networkInterface = NetworkInterface.getByInetAddress(localAddr) }
socket.joinGroup(java.net.InetSocketAddress(group, 5353), socket.networkInterface)
multicastSocket = socket
registerService(socket, group, servicePort) // Try each interface until we find one that works
for (iface in interfaces) {
val buf = ByteArray(4096)
while (!Thread.currentThread().isInterrupted) {
val packet = DatagramPacket(buf, buf.size)
try { try {
socket.soTimeout = 2000 println("AeroToss: Trying interface ${iface.name} (${iface.inetAddresses.toList().map { it.hostAddress }})")
socket.receive(packet) runOnInterface(iface, servicePort)
processPacket(packet.data, packet.length, packet.address) break // If successful, stay on this interface
} catch (_: java.net.SocketTimeoutException) { } catch (e: Exception) {
// Send announcement periodically println("AeroToss: Interface ${iface.name} failed: ${e.message}")
registerService(socket, group, servicePort)
} }
} }
socket.leaveGroup(group)
socket.close()
} catch (e: Exception) { } catch (e: Exception) {
if (!Thread.currentThread().isInterrupted) { if (!Thread.currentThread().isInterrupted) {
e.printStackTrace() 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) { private fun registerService(socket: MulticastSocket, group: InetAddress, servicePort: Int) {
val response = buildDnsResponse(servicePort) val response = buildDnsResponse(servicePort)
val packet = DatagramPacket(response, response.size, group, 5353) val packet = DatagramPacket(response, response.size, group, 5353)
try { try {
socket.send(packet) socket.send(packet)
println("AeroToss: Registered service on port $servicePort")
} catch (_: Exception) {} } catch (_: Exception) {}
} }
@@ -86,43 +114,33 @@ class JmdnsDiscovery : DiscoveryManager {
if (anCount == 0) return if (anCount == 0) return
var offset = 12 var offset = 12
// Skip questions
for (i in 0 until anCount) { for (i in 0 until anCount) {
offset = skipName(data, offset, length) offset = skipName(data, offset, length)
if (offset + 10 > length) return if (offset + 10 > length) return
val type = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) 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 += 2 // class
offset += 4 // ttl offset += 4 // ttl
val rdLength = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF) val rdLength = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF)
offset += 2 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) { if (type == 33 && offset + rdLength <= length) {
// SRV record // SRV record
val srvData = data.copyOfRange(offset, offset + rdLength) val srvData = data.copyOfRange(offset, offset + rdLength)
val port = ((srvData[2].toInt() and 0xFF) shl 8) or (srvData[3].toInt() and 0xFF) 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( val key = fromAddress.hostAddress ?: continue
id = fromAddress.hostAddress ?: "unknown",
name = "Device@${fromAddress.hostAddress}",
type = DeviceType.PHONE,
hostAddress = fromAddress.hostAddress ?: "",
port = port
)
val key = fromAddress.hostAddress ?: return
if (!discoveredDevices.containsKey(key)) { if (!discoveredDevices.containsKey(key)) {
val device = Device(
id = key,
name = "Device@${fromAddress.hostAddress}",
type = DeviceType.PHONE,
hostAddress = fromAddress.hostAddress,
port = port
)
discoveredDevices[key] = device discoveredDevices[key] = device
_devices.value = discoveredDevices.values.toList() _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 txtData = data.copyOfRange(offset, offset + rdLength)
val attributes = parseTxtRecord(txtData) val attributes = parseTxtRecord(txtData)
val name = attributes["name"] ?: "Device@${fromAddress.hostAddress}" val name = attributes["name"] ?: "Device@${fromAddress.hostAddress}"
val id = attributes["id"] ?: fromAddress.hostAddress ?: "unknown" val id = attributes["id"] ?: fromAddress.hostAddress
val portStr = attributes["port"]
val key = fromAddress.hostAddress ?: return val key = fromAddress.hostAddress ?: continue
val existing = discoveredDevices[key] val existing = discoveredDevices[key]
if (existing != null && id != deviceId) { if (existing != null && id != deviceId) {
val updated = existing.copy(name = name, id = id) val updated = existing.copy(name = name, id = id)
@@ -158,29 +175,6 @@ class JmdnsDiscovery : DiscoveryManager {
return pos 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<String, String> { private fun parseTxtRecord(data: ByteArray): Map<String, String> {
val result = mutableMapOf<String, String>() val result = mutableMapOf<String, String>()
var pos = 0 var pos = 0
@@ -202,15 +196,15 @@ class JmdnsDiscovery : DiscoveryManager {
val baos = java.io.ByteArrayOutputStream() val baos = java.io.ByteArrayOutputStream()
val dos = java.io.DataOutputStream(baos) val dos = java.io.DataOutputStream(baos)
// Header: ID=0, Flags=0x8400 (response, authoritative), QDCOUNT=0, ANCOUNT=3, NSCOUNT=0, ARCOUNT=0 // Header
dos.writeShort(0) dos.writeShort(0) // ID
dos.writeShort(0x8400) dos.writeShort(0x8400) // Flags: response, authoritative
dos.writeShort(0) // QDCOUNT dos.writeShort(0) // QDCOUNT
dos.writeShort(3) // ANCOUNT (PTR + SRV + TXT) dos.writeShort(3) // ANCOUNT (PTR + SRV + TXT)
dos.writeShort(0) // NSCOUNT dos.writeShort(0) // NSCOUNT
dos.writeShort(0) // ARCOUNT dos.writeShort(0) // ARCOUNT
// PTR record: _aerotoss._tcp.local. → AeroToss._aerotoss._tcp.local. // PTR record
writeDnsName(baos, "_aerotoss._tcp.local.") writeDnsName(baos, "_aerotoss._tcp.local.")
dos.writeShort(12) // PTR dos.writeShort(12) // PTR
dos.writeShort(1) // class IN dos.writeShort(1) // class IN
@@ -221,16 +215,14 @@ class JmdnsDiscovery : DiscoveryManager {
dos.writeShort(ptrBytes.size) dos.writeShort(ptrBytes.size)
dos.write(ptrBytes) dos.write(ptrBytes)
// SRV record: AeroToss._aerotoss._tcp.local. → port // SRV record
writeDnsName(baos, "AeroToss._aerotoss._tcp.local.") writeDnsName(baos, "AeroToss._aerotoss._tcp.local.")
dos.writeShort(33) // SRV dos.writeShort(33) // SRV
dos.writeShort(1) // class IN dos.writeShort(1) // class IN
dos.writeInt(120) // TTL dos.writeInt(120) // TTL
val srvRd = java.io.ByteArrayOutputStream() val srvRd = java.io.ByteArrayOutputStream()
srvRd.write(0) // priority high byte srvRd.write(0) // priority
srvRd.write(0) // priority low byte srvRd.write(0) // weight
srvRd.write(0) // weight high byte
srvRd.write(0) // weight low byte
srvRd.write((servicePort shr 8) and 0xFF) srvRd.write((servicePort shr 8) and 0xFF)
srvRd.write(servicePort and 0xFF) srvRd.write(servicePort and 0xFF)
writeDnsName(srvRd, "AeroToss._aerotoss._tcp.local.") writeDnsName(srvRd, "AeroToss._aerotoss._tcp.local.")
@@ -264,6 +256,38 @@ class JmdnsDiscovery : DiscoveryManager {
os.write(0) os.write(0)
} }
private fun getAvailableInterfaces(): List<NetworkInterface> {
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() { override fun stopDiscovery() {
discoveryThread?.interrupt() discoveryThread?.interrupt()
discoveryThread = null discoveryThread = null
@@ -281,20 +305,4 @@ class JmdnsDiscovery : DiscoveryManager {
const val SERVICE_TYPE = "_aerotoss._tcp" const val SERVICE_TYPE = "_aerotoss._tcp"
const val SERVICE_NAME = "AeroToss" 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")
}
} }