diff --git a/.gitignore b/.gitignore index 10ad19a..a0f2b44 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ bin/ .idea/ /src/nativeInterop/cinterop/def/ +/src/cinterop/win32_join_multicast_group.def diff --git a/README.md b/README.md index 56468dc..ce54cdc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # native-mdns -A Kotlin multiplatform library for mdns announcer(mdns server), only broadcast is supported, +A Kotlin multiplatform library for mdns announcer(mdns server), only broadcast is supported, Also, nmdns can be compiled to shared/static lib for other languages calling, see [c++ example](#cpp) -mDNS is usually used for Apple's AirPlay protocol family, there's an example in README.md [How to use](#How-to-use) +mDNS is usually used for Apple's AirPlay protocol family, there's an example in README.md [How to use](#How-to-use) Supported platforms: + 1. windows64(mingwx64) 2. linuxX64 3. linuxArm64 @@ -14,13 +15,25 @@ Supported platforms: 5. macosX64 6. jvm(11 or later) + +Platform implementations are based on: + +- Apple: posix socket +- Windows: WinSock +- Linux: posix socket +- JVM: `java.net.MulticastSocket` + +No external dependencies required. + Supported record type + 1. A 2. PTR 3. SRV 4. TXT Supported features + 1. Event hooking(Kotlin only) > AAAA is not supported diff --git a/build.gradle.kts b/build.gradle.kts index ea9e7e4..8983a4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,9 +2,10 @@ import com.vanniktech.maven.publish.SonatypeHost import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - kotlin("multiplatform") version "2.2.0" + kotlin("multiplatform") version "2.3.10" id("com.vanniktech.maven.publish") version "0.31.0-rc2" id("signing") + id("cn.rtast.kdef") version "0.1.1" } val libVersion: String by project @@ -20,7 +21,14 @@ kotlin { val nativeTargets = listOf( linuxArm64(), linuxX64(), - mingwX64(), + mingwX64 { + compilations["main"].cinterops { + @Suppress("UNUSED") + val joinMulticastGroup by creating { + definitionFile = file("src/cinterop/win32_join_multicast_group.def") + } + } + }, macosArm64(), macosX64() ) @@ -125,4 +133,9 @@ tasks.register("linkSharedLibrary") { dependsOn(tasks.named("linkReleaseSharedLinuxX64")) dependsOn(tasks.named("linkReleaseSharedMacosArm64")) dependsOn(tasks.named("linkReleaseSharedMingwX64")) +} + +kdef { + outputDir = file("src/cinterop") + defFiles.add(file("src/cinterop/template/win32_join_multicast_group.def")) } \ No newline at end of file diff --git a/clibs/win32_join_multicast.c b/clibs/win32_join_multicast.c new file mode 100644 index 0000000..dddf201 --- /dev/null +++ b/clibs/win32_join_multicast.c @@ -0,0 +1,40 @@ +#include "win32_join_multicast.h" + +#include +#include +#include + +int join_multicast_group(SOCKET sock, const char *group) { + struct ip_mreq mreq; + mreq.imr_multiaddr.s_addr = inet_addr(group); + mreq.imr_interface.s_addr = INADDR_ANY; + + if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *) &mreq, sizeof(mreq)) == SOCKET_ERROR) { + return 1; + } + return 0; +} + +int socket_sendto(SOCKET fd, const uint8_t *data, int len, const char *host, unsigned short port) { + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = inet_addr(host); + + const int sent = sendto(fd, (const char *) data, len, 0, (struct sockaddr *) &addr, sizeof(addr)); + if (sent == SOCKET_ERROR) { + return 1; + } + return 0; +} + +int leave_multicast_group(SOCKET sock, const char *group) { + struct ip_mreq mreq; + mreq.imr_multiaddr.s_addr = inet_addr(group); + mreq.imr_interface.s_addr = INADDR_ANY; + + if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char *) &mreq, sizeof(mreq)) == SOCKET_ERROR) { + return 1; + } + return 0; +} \ No newline at end of file diff --git a/clibs/win32_join_multicast.h b/clibs/win32_join_multicast.h new file mode 100644 index 0000000..4b7e7fb --- /dev/null +++ b/clibs/win32_join_multicast.h @@ -0,0 +1,22 @@ +#ifndef WIN32_JOIN_MULTICAST_H +#define WIN32_JOIN_MULTICAST_H + +#include +#include + +#ifdef __cplusplus +extern "C" { + +#endif + +int join_multicast_group(SOCKET sock, const char *group); + +int socket_sendto(SOCKET fd, const uint8_t *data, int len, const char *host, unsigned short port); + +int leave_multicast_group(SOCKET sock, const char *group); + +#ifdef __cplusplus +} +#endif + +#endif // WIN32_JOIN_MULTICAST_H diff --git a/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt b/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt deleted file mode 100644 index 108de64..0000000 --- a/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - - -@file:OptIn(ExperimentalForeignApi::class) -@file:Suppress("unused") - -package cn.rtast.nmdns - -import kotlinx.cinterop.* -import platform.darwin.inet_addr -import platform.posix.* - -public actual class Socket1 internal actual constructor() { - /** - * socket fd - */ - private var socket: Int = -1 - - public actual fun bind(ip: String, port: Int): Socket1 { - socket = socket(AF_INET, SOCK_DGRAM, 0) - if (socket < 0) perror("Failed to create udp socket") - memScoped { - val reuse = alloc() - reuse.value = 1 - if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf().convert()) < 0) { - perror("Failed to setsocketopt SO_REUSEADDR") - } - } - memScoped { - val addr = alloc() - addr.sin_family = AF_INET.convert() - addr.sin_port = htons(port.toUShort()) - addr.sin_addr.s_addr = inet_addr(ip) - if (bind(socket, addr.ptr.reinterpret(), sizeOf().convert()) < 0) { - perror("Failed to bind") - close(socket) - } - } - memScoped { - val mreq = alloc() - mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251") - mreq.imr_interface.s_addr = inet_addr(ip) - if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf().convert()) < 0) { - perror("setsockopt IP_ADD_MEMBERSHIP") - } - } - - return this - } - - public actual fun send(packet: ByteArray) { - memScoped { - val addr = alloc() - addr.sin_family = AF_INET.convert() - addr.sin_port = htons(5353u) - addr.sin_addr.s_addr = inet_addr("224.0.0.251") - val cbuf = packet.refTo(0).getPointer(this) - sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf().convert()) - } - } - - public actual fun destroy() { - close(socket) - } -} \ No newline at end of file diff --git a/src/appleMain/kotlin/cn/rtast/nmdns/util/multicast_socket.apple.kt b/src/appleMain/kotlin/cn/rtast/nmdns/util/multicast_socket.apple.kt new file mode 100644 index 0000000..a1ecba5 --- /dev/null +++ b/src/appleMain/kotlin/cn/rtast/nmdns/util/multicast_socket.apple.kt @@ -0,0 +1,60 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.nmdns.util + +import kotlinx.cinterop.* +import platform.darwin.inet_addr +import platform.darwin.sockaddr_in +import platform.posix.* + +public actual class MulticastSocket actual constructor() { + + private var fd: Int = -1 + + public actual fun bind(port: Int): Unit = memScoped { + fd = socket(AF_INET, SOCK_DGRAM, 0) + val opt = alloc() + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf().convert()) + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = INADDR_ANY + bind(fd, addr.ptr.reinterpret(), sizeOf().convert()) + } + + public actual fun joinMulticastGroup(group: String): Unit = memScoped { + val merq = alloc() + merq.imr_multiaddr.s_addr = inet_addr(group) + merq.imr_interface.s_addr = INADDR_ANY + setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, merq.ptr, sizeOf().convert()) + } + + public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = inet_addr(host) + data.usePinned { + sendto(fd, it.addressOf(0), data.size.convert(), 0, addr.ptr.reinterpret(), sizeOf().convert()) + } + } + + public actual fun receive(buf: ByteArray): Long = memScoped { + buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) } + } + + public actual fun close(): Unit = run { if (fd > 0) close(fd) } + + public actual fun leaveMulticastGroup(group: String): Unit = memScoped { + val merq = alloc() + merq.imr_multiaddr.s_addr = inet_addr(group) + merq.imr_interface.s_addr = INADDR_ANY + setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, merq.ptr, sizeOf().convert()) + } +} \ No newline at end of file diff --git a/src/appleTest/kotlin/test/TestApple.kt b/src/appleTest/kotlin/test/TestApple.kt index 15bd828..2b0c280 100644 --- a/src/appleTest/kotlin/test/TestApple.kt +++ b/src/appleTest/kotlin/test/TestApple.kt @@ -7,7 +7,7 @@ package test -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import cn.rtast.nmdns.registerService import cn.rtast.nmdns.sleep1 import kotlin.test.Test diff --git a/src/cinterop/libwin32_join_multicast_group.a b/src/cinterop/libwin32_join_multicast_group.a new file mode 100644 index 0000000..7d5381e Binary files /dev/null and b/src/cinterop/libwin32_join_multicast_group.a differ diff --git a/src/cinterop/template/win32_join_multicast_group.def b/src/cinterop/template/win32_join_multicast_group.def new file mode 100644 index 0000000..a5a4db8 --- /dev/null +++ b/src/cinterop/template/win32_join_multicast_group.def @@ -0,0 +1,3 @@ +headers = {{PATH}}/clibs/win32_join_multicast.h +compilerOpts = -I{{PATH}}/clibs +linkerOpts = {{PATH}}/src/cinterop/libwin32_join_multicast_group.a -lws2_32 \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt b/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt deleted file mode 100644 index 020c398..0000000 --- a/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - - -package cn.rtast.nmdns - -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.BINARY) -@RequiresOptIn -public annotation class CExport \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/MDNSRecord.kt b/src/commonMain/kotlin/cn/rtast/nmdns/MDNSRecord.kt new file mode 100644 index 0000000..9ef492e --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/MDNSRecord.kt @@ -0,0 +1,16 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/14 + */ + + +package cn.rtast.nmdns + +public data class MDNSRecord( + val name: String, + val type: String, + val host: String, + val port: Int, + val txt: Map = emptyMap(), +) \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/mdns_announcer.kt b/src/commonMain/kotlin/cn/rtast/nmdns/mdns_announcer.kt new file mode 100644 index 0000000..8ad040c --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/mdns_announcer.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/14 + */ + + +package cn.rtast.nmdns + +import cn.rtast.nmdns.util.MulticastSocket + +public class MDNSAnnouncer(private val socket: MulticastSocket) { + + public fun register(record: MDNSRecord) { + + } + + public fun unregister(record: MDNSRecord) { + + } + + public fun onQuery(callback: (queryType: String, queryName: String) -> Unit) {} + + public fun start() { + + } + + + public fun stop() { + + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt b/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt deleted file mode 100644 index aa4957a..0000000 --- a/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/28/25 - */ - - -@file:OptIn(ExperimentalNativeApi::class) - -package cn.rtast.nmdns - -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.CName - -public fun NMDNSAnnouncer.onRegister(callback: () -> Unit) { - onRegisterCallback = callback -} - -public fun NMDNSAnnouncer.onBroadcast(callback: () -> Unit) { - onBroadcastCallback = callback -} - -public fun NMDNSAnnouncer.onUnregistered(callback: () -> Unit) { - onUnregisterCallback = callback -} - -public data class NMDNSAnnouncer( - /** - * service type - * airplay service type: _airplay._tcp.local. - */ - internal val serviceType: String, - /** - * service name - */ - internal val serviceName: String, - /** - * local hostname - */ - internal val hostname: String, - /** - * service ip, lan ip mostly - */ - internal val ipAddress: String, - /** - * service port - */ - internal val port: Int = 7004, - /** - * mdns server port - */ - internal val mdnsPort: Int = 3025, - /** - * txt record content - */ - internal val txtRecords: List, - /** - * mdns socket server - */ - internal val server: Socket1, - /** - * srv packet - */ - internal val packet: ByteArray, -) { - - internal var onRegisterCallback: (() -> Unit)? = null - internal var onBroadcastCallback: (() -> Unit)? = null - internal var onUnregisterCallback: (() -> Unit)? = null - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as NMDNSAnnouncer - - if (port != other.port) return false - if (mdnsPort != other.mdnsPort) return false - if (serviceType != other.serviceType) return false - if (serviceName != other.serviceName) return false - if (hostname != other.hostname) return false - if (ipAddress != other.ipAddress) return false - if (txtRecords != other.txtRecords) return false - if (server != other.server) return false - if (!packet.contentEquals(other.packet)) return false - - return true - } - - override fun hashCode(): Int { - var result = port - result = 31 * result + mdnsPort - result = 31 * result + serviceType.hashCode() - result = 31 * result + serviceName.hashCode() - result = 31 * result + hostname.hashCode() - result = 31 * result + ipAddress.hashCode() - result = 31 * result + txtRecords.hashCode() - result = 31 * result + server.hashCode() - result = 31 * result + packet.contentHashCode() - return result - } - - /** - * broadcast packet - */ - public fun broadcast() { - this.onBroadcastCallback?.invoke() - this.server.send(packet) - } - - /** - * unregister service - */ - public fun unregister() { - this.onUnregisterCallback?.invoke() - this.server.destroy() - } -} - -/** - * broadcast packet - * export for c - */ -@CExport -@CName("broadcast") -public fun broadcast1(server: NMDNSAnnouncer) { - server.server.send(server.packet) -} - -/** - * unregister service - * export for c - */ -@CExport -@CName("unregister") -public fun unregister1(server: NMDNSAnnouncer) { - server.unregister() -} - -/** - * register service - * airplay2 txt records - * listOf( - * "deviceid=${randomMacAddress()}", - * "model=AppleTV3,2C", - * "features=0x5A7FFFF7,0x1E", - * "srcvers=220.68", - * "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" - * ) - */ -@CName("register_service") -public fun registerService( - serviceType: String, - serviceName: String, - hostname: String, - ipAddress: String, - port: Int, - mdnsPort: Int = 9872, - bindAddress: String, - txtRecords: List, - callback: NMDNSAnnouncer.() -> Unit = {}, -): NMDNSAnnouncer { - val socket = createSocket() - .bind(bindAddress, mdnsPort) - - val server = NMDNSAnnouncer( - serviceType, - serviceName, - hostname, - ipAddress, - port, - mdnsPort, - txtRecords, - socket, - buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords), - ) - server.callback() - server.onRegisterCallback?.invoke() - return server -} - -internal fun buildPacket( - serviceType: String, - serviceName: String, - hostname: String, - ip: String, - port: Int, - txtRecords: List, -): ByteArray { - val records = mutableListOf() - records += encodeRR(serviceType, 12, encodeName(serviceName)) - - val srvData = ByteArray(6) { 0 } - srvData[4] = (port shr 8).toByte() - srvData[5] = (port and 0xFF).toByte() - records += encodeRR(serviceName, 33, srvData + encodeName(hostname)) - - val ipParts = ip.split(".").map { it.toInt().toByte() } - records += encodeRR(hostname, 1, ipParts.toByteArray()) - - if (txtRecords.isNotEmpty()) { - val txtData = encodeTxt(txtRecords) - records += encodeRR(serviceName, 16, txtData) - } - - val header = ByteArray(12) - header[2] = 0x84.toByte() - header[3] = 0x00 - - header[6] = ((records.size shr 8) and 0xFF).toByte() - header[7] = (records.size and 0xFF).toByte() - - return header + records.reduce { acc, r -> acc + r } -} - -internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray { - val nameBytes = encodeName(name) - val rr = ByteArray(10) - rr[0] = 0x00; rr[1] = type.toByte() - rr[2] = 0x00; rr[3] = 0x01 - rr[4] = 0x00; rr[5] = 0x00; rr[6] = 0x00; rr[7] = 0x78 - rr[8] = (data.size shr 8).toByte() - rr[9] = (data.size and 0xFF).toByte() - return nameBytes + rr + data -} - -internal fun encodeName(name: String): ByteArray { - val parts = name.trimEnd('.').split(".") - val bytes = mutableListOf() - for (p in parts) { - bytes += p.length.toByte() - bytes += p.encodeToByteArray().toList() - } - bytes += 0 - return bytes.toByteArray() -} - -internal fun encodeTxt(records: List): ByteArray { - val bytes = mutableListOf() - for (txt in records) { - val txtBytes = txt.encodeToByteArray() - if (txtBytes.size > 255) continue - bytes += txtBytes.size.toByte() - bytes += txtBytes.toList() - } - return bytes.toByteArray() -} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt b/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt deleted file mode 100644 index 7068c1d..0000000 --- a/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/29/25 - */ - - -package cn.rtast.nmdns - -/** - * block current thread - * unit: second - */ - -public expect fun sleep1(time: Int) \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt b/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt deleted file mode 100644 index ef4ed85..0000000 --- a/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - -@file:Suppress("unused") - - -package cn.rtast.nmdns - -public expect class Socket1 internal constructor() { - /** - * bind to ip - */ - public fun bind(ip: String, port: Int): Socket1 - - /** - * send packet - */ - public fun send(packet: ByteArray) - - /** - * close - */ - public fun destroy() -} - -internal fun createSocket(): Socket1 = Socket1() - -public fun htons(value: UShort): UShort = - (((value.toInt() and 0xFF) shl 8) or ((value.toInt() ushr 8) and 0xFF)).toUShort() - -public fun htonl(value: UInt): UInt = - ((value and 0xFFu) shl 24) or - ((value and 0xFF00u) shl 8) or - ((value and 0xFF0000u) shr 8) or - ((value and 0xFF000000u) shr 24) \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/util/byte_util.kt b/src/commonMain/kotlin/cn/rtast/nmdns/util/byte_util.kt new file mode 100644 index 0000000..e860f1b --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/util/byte_util.kt @@ -0,0 +1,12 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +package cn.rtast.nmdns.util + + +public fun htons(value: UShort): UShort = + (((value.toInt() and 0xFF) shl 8) or ((value.toInt() ushr 8) and 0xFF)).toUShort() \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/util/bytearray_builder.kt b/src/commonMain/kotlin/cn/rtast/nmdns/util/bytearray_builder.kt new file mode 100644 index 0000000..932d68b --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/util/bytearray_builder.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +package cn.rtast.nmdns.util + +internal class ByteArrayBuilder { + private val buffer = mutableListOf() + + fun appendByte(b: Int) = apply { buffer.add(b.toByte()) } + + fun appendShort(value: Int) = apply { + buffer.add(((value shr 8) and 0xFF).toByte()) + buffer.add((value and 0xFF).toByte()) + } + + fun appendInt(value: Int) = apply { + buffer.add(((value shr 24) and 0xFF).toByte()) + buffer.add(((value shr 16) and 0xFF).toByte()) + buffer.add(((value shr 8) and 0xFF).toByte()) + buffer.add((value and 0xFF).toByte()) + } + + fun appendBytes(bytes: ByteArray) = apply { buffer.addAll(bytes.toList()) } + + fun appendName(name: String): ByteArrayBuilder { + val labels = name.split(".") + for (label in labels) { + appendByte(label.length) + appendBytes(label.encodeToByteArray()) + } + appendByte(0) + return this + } + + fun build(): ByteArray = buffer.toByteArray() +} + +internal fun dnsPacket(block: ByteArrayBuilder.() -> Unit) = + ByteArrayBuilder().apply(block).build() \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt b/src/commonMain/kotlin/cn/rtast/nmdns/util/mac.kt similarity index 83% rename from src/commonMain/kotlin/cn/rtast/nmdns/mac.kt rename to src/commonMain/kotlin/cn/rtast/nmdns/util/mac.kt index 3034afa..68091ce 100644 --- a/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt +++ b/src/commonMain/kotlin/cn/rtast/nmdns/util/mac.kt @@ -5,17 +5,15 @@ */ -package cn.rtast.nmdns +package cn.rtast.nmdns.util import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.CName import kotlin.random.Random /** * generate random mac address */ @OptIn(ExperimentalNativeApi::class) -@CName("random_mac_address") public fun randomMacAddress(): String { val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() } return bytes.joinToString(":") { it.toHexString() } diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/util/mdns_packet.kt b/src/commonMain/kotlin/cn/rtast/nmdns/util/mdns_packet.kt new file mode 100644 index 0000000..4ca0c3c --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/util/mdns_packet.kt @@ -0,0 +1,53 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +package cn.rtast.nmdns.util + +internal fun buildMDNSResponse( + host: String, + service: String, + ip: String, + port: Int, + txtRecords: Map, +): ByteArray = dnsPacket { + // HEADER + appendShort(0); appendShort(0x8400) + appendShort(0); appendShort(4) + appendShort(0); appendShort(0) + + // PTR + appendName("_airplay._tcp.local"); appendShort(12) + appendShort(0x8001); appendInt(4500) + val ptr = dnsPacket ptr@{ this@ptr.appendName(service) } + appendShort(ptr.size); appendBytes(ptr) + + // SRV + appendName(service); appendShort(33) + appendShort(0x8001); appendInt(120); + appendShort(0); appendShort(0) + appendShort(port) + val srv = dnsPacket srv@{ this@srv.appendName(host) } + appendShort(srv.size); appendBytes(srv) + + // TXT + appendName(service); appendShort(16) + appendShort(0x8001); appendInt(4500) + val txt = dnsPacket txt@{ + for ((k, v) in txtRecords) { + val entry = "$k=$v".encodeToByteArray() + this@txt.appendByte(entry.size); this@txt.appendBytes(entry) + } + } + appendShort(txt.size); appendBytes(txt) + + // A + appendName(host); appendShort(1) + appendShort(0x8001); appendInt(120) + val ipParts = ip.split(".").map { it.toInt() and 0xff } + appendShort(4); appendByte(ipParts[0]); appendByte(ipParts[1]) + appendByte(ipParts[2]); appendByte(ipParts[3]) +} diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/util/multicast_socket.kt b/src/commonMain/kotlin/cn/rtast/nmdns/util/multicast_socket.kt new file mode 100644 index 0000000..ea61df0 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/util/multicast_socket.kt @@ -0,0 +1,40 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +package cn.rtast.nmdns.util + +public expect class MulticastSocket internal constructor() { + /** + * bind to multicast addr and port + */ + public fun bind(port: Int) + + /** + * join multicast group + */ + public fun joinMulticastGroup(group: String) + + /** + * leave multicast group + */ + public fun leaveMulticastGroup(group: String) + + /** + * send binary packet + */ + public fun send(data: ByteArray, host: String, port: Int) + + /** + * receive packet from multicast group + */ + public fun receive(buf: ByteArray): Long + + /** + * close socket + */ + public fun close() +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt b/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt deleted file mode 100644 index 6253fd8..0000000 --- a/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/29/25 - */ - -package cn.rtast.nmdns - -public actual fun sleep1(time: Int) { - Thread.sleep(time * 1000L) -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt b/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt deleted file mode 100644 index fd3dc7d..0000000 --- a/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - - -package cn.rtast.nmdns - -import java.net.DatagramPacket -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.MulticastSocket - -public actual class Socket1 internal actual constructor() { - private lateinit var socket: MulticastSocket - private val group: InetAddress = InetAddress.getByName("224.0.0.251") - - public actual fun bind(ip: String, port: Int): Socket1 { - socket = MulticastSocket(InetSocketAddress(ip, port)) - socket.joinGroup(group) - return this - } - - public actual fun send(packet: ByteArray) { - socket.send(DatagramPacket(packet, packet.size, group, 5353)) - } - - public actual fun destroy() { - socket.close() - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/nmdns/util/multicast_socket.jvm.kt b/src/jvmMain/kotlin/cn/rtast/nmdns/util/multicast_socket.jvm.kt new file mode 100644 index 0000000..7239711 --- /dev/null +++ b/src/jvmMain/kotlin/cn/rtast/nmdns/util/multicast_socket.jvm.kt @@ -0,0 +1,53 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +package cn.rtast.nmdns.util + +import java.net.DatagramPacket +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.NetworkInterface +import java.net.MulticastSocket as JvmMulticastSocket + +public actual class MulticastSocket actual constructor() { + + private var socket: JvmMulticastSocket? = null + + public actual fun bind(port: Int) { + socket = JvmMulticastSocket(port) + socket?.reuseAddress = true + } + + public actual fun joinMulticastGroup(group: String) { + val groupAddr = InetAddress.getByName(group) + val ni = NetworkInterface.getByName(null) + socket?.joinGroup(InetSocketAddress(groupAddr, 0), ni) + } + + public actual fun send(data: ByteArray, host: String, port: Int) { + val addr = InetAddress.getByName(host) + val packet = DatagramPacket(data, data.size, addr, port) + socket?.send(packet) + } + + public actual fun receive(buf: ByteArray): Long { + val packet = DatagramPacket(buf, buf.size) + socket?.receive(packet) + return packet.length.toLong() + } + + public actual fun close() { + socket?.close() + socket = null + } + + public actual fun leaveMulticastGroup(group: String) { + val groupAddr = InetAddress.getByName(group) + val ni = NetworkInterface.getByName(null) + socket?.leaveGroup(InetSocketAddress(groupAddr, 0), ni) + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/test/TestJvm.kt b/src/jvmTest/kotlin/test/TestJvm.kt index e650301..3cd729c 100644 --- a/src/jvmTest/kotlin/test/TestJvm.kt +++ b/src/jvmTest/kotlin/test/TestJvm.kt @@ -7,7 +7,7 @@ package test -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import cn.rtast.nmdns.registerService import cn.rtast.nmdns.sleep1 import org.junit.Test diff --git a/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt b/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt deleted file mode 100644 index f68adf8..0000000 --- a/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - -@file:OptIn(ExperimentalForeignApi::class) -@file:Suppress("unused") - -package cn.rtast.nmdns - -import kotlinx.cinterop.* -import platform.linux.inet_addr -import platform.posix.* - -public actual class Socket1 internal actual constructor() { - /** - * socket fd - */ - private var socket: Int = -1 - - public actual fun bind(ip: String, port: Int): Socket1 { - socket = socket(AF_INET, SOCK_DGRAM, 0) - if (socket < 0) perror("Failed to create udp socket") - memScoped { - val reuse = alloc() - reuse.value = 1 - if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf().convert()) < 0) { - perror("Failed to setsocketopt SO_REUSEADDR") - } - } - memScoped { - val addr = alloc() - addr.sin_family = AF_INET.convert() - addr.sin_port = htons(port.toUShort()) - addr.sin_addr.s_addr = inet_addr(ip) - if (bind(socket, addr.ptr.reinterpret(), sizeOf().convert()) < 0) { - perror("Failed to bind") - close(socket) - } - } - memScoped { - val mreq = alloc() - mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251") - mreq.imr_interface.s_addr = htonl(INADDR_ANY) - if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf().convert()) < 0) { - perror("setsockopt IP_ADD_MEMBERSHIP") - } - } - - return this - } - - public actual fun send(packet: ByteArray) { - memScoped { - val addr = alloc() - addr.sin_family = AF_INET.convert() - addr.sin_port = htons(5353u) - addr.sin_addr.s_addr = inet_addr("224.0.0.251") - val cbuf = packet.refTo(0).getPointer(this) - sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf().convert()) - } - } - - public actual fun destroy() { - close(socket) - } -} \ No newline at end of file diff --git a/src/linuxMain/kotlin/cn/rtast/nmdns/util/multicast_socket.linux.kt b/src/linuxMain/kotlin/cn/rtast/nmdns/util/multicast_socket.linux.kt new file mode 100644 index 0000000..c7c9eef --- /dev/null +++ b/src/linuxMain/kotlin/cn/rtast/nmdns/util/multicast_socket.linux.kt @@ -0,0 +1,64 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.nmdns.util + +import kotlinx.cinterop.* +import platform.linux.inet_addr +import platform.posix.* + +public actual class MulticastSocket actual constructor() { + private var fd = -1 + + public actual fun bind(port: Int): Unit = memScoped { + fd = socket(fd, SOCK_DGRAM, 0) + val opt = alloc() + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf().convert()) + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = INADDR_ANY + bind(fd, addr.ptr.reinterpret(), sizeOf().convert()) + } + + public actual fun joinMulticastGroup(group: String): Unit = memScoped { + val merq = alloc() + merq.imr_multiaddr.s_addr = inet_addr(group) + merq.imr_interface.s_addr = INADDR_ANY + setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, merq.ptr, sizeOf().convert()) + } + + public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = inet_addr(host) + data.usePinned { + sendto( + fd, it.addressOf(0), + data.size.convert(), 0, + addr.ptr.reinterpret(), + sizeOf().convert() + ) + } + } + + public actual fun receive(buf: ByteArray): Long = memScoped { + buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) } + } + + public actual fun close(): Unit = run { if (fd > 0) close(fd) } + + public actual fun leaveMulticastGroup(group: String): Unit = memScoped { + val merq = alloc() + merq.imr_multiaddr.s_addr = inet_addr(group) + merq.imr_interface.s_addr = INADDR_ANY + setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, merq.ptr, sizeOf().convert()) + } +} \ No newline at end of file diff --git a/src/linuxTest/kotlin/test/TestLinux.kt b/src/linuxTest/kotlin/test/TestLinux.kt index b75b869..341217a 100644 --- a/src/linuxTest/kotlin/test/TestLinux.kt +++ b/src/linuxTest/kotlin/test/TestLinux.kt @@ -10,7 +10,7 @@ package test import cn.rtast.nmdns.onBroadcast import cn.rtast.nmdns.onRegister import cn.rtast.nmdns.onUnregistered -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import cn.rtast.nmdns.registerService import cn.rtast.nmdns.sleep1 import kotlin.test.Test diff --git a/src/linuxTest/kotlin/test/TestMacAddr.kt b/src/linuxTest/kotlin/test/TestMacAddr.kt index db82df6..6c43ea2 100644 --- a/src/linuxTest/kotlin/test/TestMacAddr.kt +++ b/src/linuxTest/kotlin/test/TestMacAddr.kt @@ -7,7 +7,7 @@ package test -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import kotlin.test.Test class TestMacAddr { diff --git a/src/linuxTest/kotlin/test/TestNative.kt b/src/linuxTest/kotlin/test/TestNative.kt index 2264e55..b143b1c 100644 --- a/src/linuxTest/kotlin/test/TestNative.kt +++ b/src/linuxTest/kotlin/test/TestNative.kt @@ -9,7 +9,7 @@ package test import cn.rtast.nmdns.buildPacket import cn.rtast.nmdns.createSocket -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import cn.rtast.nmdns.sleep1 import kotlin.test.Test diff --git a/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt b/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt deleted file mode 100644 index ada0da5..0000000 --- a/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - -@file:OptIn(ExperimentalForeignApi::class) -@file:Suppress("unused") - -package cn.rtast.nmdns - -import kotlinx.cinterop.* -import platform.posix.* -import platform.windows.IPPROTO_UDP -import platform.windows.SOCK_DGRAM -import platform.windows.WSAStartup -import platform.windows.inet_addr -import platform.windows.setsockopt - -public actual class Socket1 internal actual constructor() { - - /** - * socket fd in windows - */ - private var sock: SOCKET = INVALID_SOCKET - private var bound: Boolean = false - - public actual fun bind(ip: String, port: Int): Socket1 = memScoped { - val wsaData = nativeHeap.alloc() - if (WSAStartup(0x202u, wsaData.ptr) != 0) { - perror("Failed to create socket") - } - sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) - if (sock == INVALID_SOCKET) { - perror("Failed to create socket") - WSACleanup() - } - - val result = setsockopt( - sock, - SOL_SOCKET, - SO_REUSEADDR, - "1", - sizeOf().convert() - ) - if (result != 0) println("setsockopt failed") - - val addr = alloc() - addr.sin_family = AF_INET.convert() - addr.sin_port = htons(port.toUShort()) - addr.sin_addr.S_un.S_addr = inet_addr(ip) - - if (bind(sock, addr.ptr.reinterpret(), sizeOf().convert()) == SOCKET_ERROR) { - println("Bind failed") - closesocket(sock) - WSACleanup() - } - - bound = true - this@Socket1 - } - - public actual fun send(packet: ByteArray): Unit = memScoped { - if (!bound) throw IllegalStateException("Socket not bound") - val dest = alloc() - dest.sin_family = AF_INET.convert() - dest.sin_port = htons(5353u) - dest.sin_addr.S_un.S_addr = inet_addr("224.0.0.251") - packet.usePinned { pinned -> - val sent = sendto( - sock, pinned.addressOf(0), - packet.size.convert(), - 0, dest.ptr.reinterpret(), - sizeOf().convert() - ) - if (sent == SOCKET_ERROR) perror("Send failed") - } - } - - public actual fun destroy() { - if (sock != INVALID_SOCKET) { - closesocket(sock) - sock = INVALID_SOCKET - } - WSACleanup() - } -} \ No newline at end of file diff --git a/src/mingwMain/kotlin/cn/rtast/nmdns/util/multicast_socket.mingw.kt b/src/mingwMain/kotlin/cn/rtast/nmdns/util/multicast_socket.mingw.kt new file mode 100644 index 0000000..935ed97 --- /dev/null +++ b/src/mingwMain/kotlin/cn/rtast/nmdns/util/multicast_socket.mingw.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/3/13 + */ + + +@file:OptIn(ExperimentalForeignApi::class) + +package cn.rtast.nmdns.util + +import kotlinx.cinterop.* +import platform.posix.* +import platform.windows.AF_INET +import platform.windows.IPPROTO_UDP +import platform.windows.SOCK_DGRAM +import platform.windows.WSAStartup +import platform.windows.setsockopt + +public actual class MulticastSocket actual constructor() { + + private var fd: SOCKET = INVALID_SOCKET + + public actual fun bind(port: Int): Unit = memScoped { + val wsaData = alloc() + if (WSAStartup(0x202u, wsaData.ptr) != 0) perror("Failed to create socket") + fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, "1", sizeOf().convert()) + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.S_un.S_addr = INADDR_ANY + bind(fd, addr.ptr.reinterpret(), sizeOf().convert()) + } + + public actual fun joinMulticastGroup(group: String): Unit = memScoped { + // ref: clibs/win32_join_multicast.c:7 + win32_join_multicast_group.join_multicast_group(fd, group) + } + + public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.S_un.S_addr = inet_addr(host) + data.usePinned { + // ref: clibs/win32_join_multicast.c:18 + win32_join_multicast_group.socket_sendto( + fd, it.addressOf(0).reinterpret(), + data.size, host, port.toUShort() + ) + } + } + + public actual fun receive(buf: ByteArray): Long = memScoped { + buf.usePinned { recv(fd, it.addressOf(0), buf.size.convert(), 0) }.toLong() + } + + public actual fun close() { + closesocket(fd) + WSACleanup() + } + + public actual fun leaveMulticastGroup(group: String) { + // ref: clibs/win32_join_multicast.c:31 + win32_join_multicast_group.leave_multicast_group(fd, group) + } +} \ No newline at end of file diff --git a/src/mingwTest/kotlin/test/TestWindows.kt b/src/mingwTest/kotlin/test/TestWindows.kt index 501d06e..d049195 100644 --- a/src/mingwTest/kotlin/test/TestWindows.kt +++ b/src/mingwTest/kotlin/test/TestWindows.kt @@ -7,7 +7,7 @@ package test -import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.util.randomMacAddress import cn.rtast.nmdns.registerService import cn.rtast.nmdns.sleep1 import kotlin.test.Test diff --git a/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt b/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt deleted file mode 100644 index cbbdf57..0000000 --- a/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/30/25 - */ - - -@file:OptIn(ExperimentalNativeApi::class) - -package cn.rtast.nmdns - -import kotlin.experimental.ExperimentalNativeApi - -/** - * create an empty list in c - */ -@CExport -@CName("create_txt_records") -public fun createTXTRecords(): List = listOf() - -/** - * add txt record to list - */ -@CExport -@CName("add_txt_record") -public fun addTxtRecord(list: List, value: String) { - list.toMutableList().add(value) -} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt b/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt deleted file mode 100644 index 27057da..0000000 --- a/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/29/25 - */ - -package cn.rtast.nmdns - -import platform.posix.sleep -import kotlin.experimental.ExperimentalNativeApi - -@OptIn(ExperimentalNativeApi::class) -@CName("sleep1") -public actual fun sleep1(time: Int) { - sleep(time.toUInt()) -} \ No newline at end of file