AeroToss: авто-сканирование сетевых интерфейсов
- Desktop автоматически находит все доступные сетевые интерфейсы - Приоритет: приватные IP (192.168.x.x, 10.x.x.x) выше публичных - Фильтрация: loopback, vmnet, veth, utun, awdl исключены - Если интерфейс не работает — пробует следующий - Автоперерегистрация сервиса каждые 30 секунд - Логирование: какой интерфейс выбран, какой порт зарегистрирован
This commit is contained in:
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user