7 Commits
Author SHA1 Message Date
RTAkland 4a45474e21 refactor basic
WIP
2026-03-14 00:12:51 +08:00
RTAkland f6d9d897f6 Fix 2025-11-04 15:27:23 +08:00
RTAkland 896883a3e0 Support event hooking 2025-10-31 06:51:13 +08:00
RTAkland 2328bf7755 Add unregister function 2025-10-29 11:12:17 +08:00
RTAkland 6ab5e551e2 Update README.md 2025-10-27 10:46:51 +08:00
RTAkland 33239c6591 Update README 2025-09-30 18:02:03 +08:00
RTAkland 42651a7f81 Published to maven central 2025-09-30 17:51:11 +08:00
36 changed files with 640 additions and 621 deletions

No files matched your search

+1
View File
@@ -46,3 +46,4 @@ bin/
.idea/ .idea/
/src/nativeInterop/cinterop/def/ /src/nativeInterop/cinterop/def/
/src/cinterop/win32_join_multicast_group.def
+39 -14
View File
@@ -2,9 +2,12 @@
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 language calling, see [c++ example](#cpp) 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)
Supported platforms: Supported platforms:
1. windows64(mingwx64) 1. windows64(mingwx64)
2. linuxX64 2. linuxX64
3. linuxArm64 3. linuxArm64
@@ -12,9 +15,26 @@ Supported platforms:
5. macosX64 5. macosX64
6. jvm(11 or later) 6. jvm(11 or later)
Supported srv type
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 1. A
2. PTR 2. PTR
3. SRV
4. TXT
Supported features
1. Event hooking(Kotlin only)
> AAAA is not supported > AAAA is not supported
@@ -22,16 +42,7 @@ AirPlay2(_airplay._tcp.local.) is tested
# How to use # How to use
Add maven repository > Make sure you added the mavenCentral repository in your project
```kotlin
repositories {
mavenCentral()
maven("https://repo.maven.rtast.cn/releases")
}
```
Add dependency
```kotlin ```kotlin
kotlin { kotlin {
@@ -58,10 +69,24 @@ val service = registerService(
bindAddress = "192.168.10.104", bindAddress = "192.168.10.104",
mdnsPort = 5356, mdnsPort = 5356,
txtRecords = listOf("key1=value1", "key2=value2") txtRecords = listOf("key1=value1", "key2=value2")
) ) {
// This block is optional
// Event hooking in this block
onBroadcast {
println("broadcasted")
}
onRegister {
println("reg")
}
onUnregistered {
println("unreg")
}
}
while (true) { while (true) {
service.broadcast() service.broadcast()
delay(2000L) // or use cn.rtast.nmdns.sleep1(2) sleep 2 seconds delay(2000L) // or use cn.rtast.nmdns.sleep1(2) sleep 2 seconds, this function is not a suspended function
} }
``` ```
+59 -4
View File
@@ -1,12 +1,17 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
kotlin("multiplatform") version "2.2.0" kotlin("multiplatform") version "2.3.10"
id("maven-publish") 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
group = "cn.rtast.nmdns" group = "cn.rtast.nmdns"
version = "0.1.0" version = libVersion
repositories { repositories {
mavenCentral() mavenCentral()
@@ -16,7 +21,14 @@ kotlin {
val nativeTargets = listOf( val nativeTargets = listOf(
linuxArm64(), linuxArm64(),
linuxX64(), linuxX64(),
mingwX64(), mingwX64 {
compilations["main"].cinterops {
@Suppress("UNUSED")
val joinMulticastGroup by creating {
definitionFile = file("src/cinterop/win32_join_multicast_group.def")
}
}
},
macosArm64(), macosArm64(),
macosX64() macosX64()
) )
@@ -48,6 +60,7 @@ kotlin {
} }
} }
mavenPublishing {
publishing { publishing {
repositories { repositories {
mavenLocal() mavenLocal()
@@ -58,6 +71,43 @@ publishing {
password = System.getenv("PUBLISH_TOKEN") password = System.getenv("PUBLISH_TOKEN")
} }
} }
mavenCentral()
}
}
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
if (System.getenv("PUBLISH_TOKEN") == null) signAllPublications()
coordinates("cn.rtast.nmdns", project.name, libVersion)
pom {
name = project.name
description = "mDNS in kotlin multiplatform"
url = "https://github.com/RTAkland/native-mdns"
developers {
developer {
id = "rtakland"
name = "RTAkland"
email = "rtakland@outlook.com"
}
}
licenses {
license {
name = "The Apache License, Version 2.0"
url = "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
scm {
url = "https://github.com/RTAkland/native-mdns"
connection = "scm:git:git://github.com/RTAkland/native-mdns.git"
developerConnection = "scm:git:ssh://git@github.com/RTAkland/native-mdns.git"
}
}
}
if (System.getenv("PUBLISH_TOKEN") == null) {
signing {
useGpgCmd()
sign(publishing.publications)
} }
} }
@@ -84,3 +134,8 @@ tasks.register("linkSharedLibrary") {
dependsOn(tasks.named("linkReleaseSharedMacosArm64")) dependsOn(tasks.named("linkReleaseSharedMacosArm64"))
dependsOn(tasks.named("linkReleaseSharedMingwX64")) dependsOn(tasks.named("linkReleaseSharedMingwX64"))
} }
kdef {
outputDir = file("src/cinterop")
defFiles.add(file("src/cinterop/template/win32_join_multicast_group.def"))
}
+40
View File
@@ -0,0 +1,40 @@
#include "win32_join_multicast.h"
#include <stdint.h>
#include <winsock2.h>
#include <ws2tcpip.h>
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;
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef WIN32_JOIN_MULTICAST_H
#define WIN32_JOIN_MULTICAST_H
#include <stdint.h>
#include <winsock2.h>
#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
+1
View File
@@ -1,2 +1,3 @@
kotlin.code.style=official kotlin.code.style=official
kotlin.native.enableKlibsCrossCompilation=true kotlin.native.enableKlibsCrossCompilation=true
libVersion=0.1.3
@@ -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<IntVar>()
reuse.value = 1
if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf<IntVar>().convert()) < 0) {
perror("Failed to setsocketopt SO_REUSEADDR")
}
}
memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert()) < 0) {
perror("Failed to bind")
close(socket)
}
}
memScoped {
val mreq = alloc<ip_mreq>()
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<ip_mreq>().convert()) < 0) {
perror("setsockopt IP_ADD_MEMBERSHIP")
}
}
return this
}
public actual fun send(packet: ByteArray) {
memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert())
}
}
public actual fun destroy() {
close(socket)
}
}
@@ -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<IntVar>()
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf<IntVar>().convert())
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert())
}
public actual fun joinMulticastGroup(group: String): Unit = memScoped {
val merq = alloc<ip_mreq>()
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<ip_mreq>().convert())
}
public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().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<ip_mreq>()
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<ip_mreq>().convert())
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test package test
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1 import cn.rtast.nmdns.sleep1
import kotlin.test.Test import kotlin.test.Test
Binary file not shown.
@@ -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
@@ -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
@@ -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<String, String> = emptyMap(),
)
@@ -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() {
}
}
@@ -1,205 +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 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<String>,
/**
* mdns socket server
*/
internal val server: Socket1,
/**
* srv packet
*/
internal val packet: ByteArray,
) {
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.server.send(packet)
}
}
/**
* export for c
*/
@CExport
@CName("broadcast")
public fun broadcast1(server: NMDNSAnnouncer) {
server.server.send(server.packet)
}
/**
* 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<String>,
): NMDNSAnnouncer {
val server = createSocket()
.bind(bindAddress, port)
return NMDNSAnnouncer(
serviceType,
serviceName,
hostname,
ipAddress,
port,
mdnsPort,
txtRecords,
server,
buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords)
)
}
internal fun buildPacket(
serviceType: String,
serviceName: String,
hostname: String,
ip: String,
port: Int,
txtRecords: List<String>,
): ByteArray {
val records = mutableListOf<ByteArray>()
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<Byte>()
for (p in parts) {
bytes += p.length.toByte()
bytes += p.encodeToByteArray().toList()
}
bytes += 0
return bytes.toByteArray()
}
internal fun encodeTxt(records: List<String>): ByteArray {
val bytes = mutableListOf<Byte>()
for (txt in records) {
val txtBytes = txt.encodeToByteArray()
if (txtBytes.size > 255) continue
bytes += txtBytes.size.toByte()
bytes += txtBytes.toList()
}
return bytes.toByteArray()
}
@@ -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)
@@ -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)
@@ -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()
@@ -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<Byte>()
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()
@@ -5,17 +5,15 @@
*/ */
package cn.rtast.nmdns package cn.rtast.nmdns.util
import kotlin.experimental.ExperimentalNativeApi import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.CName
import kotlin.random.Random import kotlin.random.Random
/** /**
* generate random mac address * generate random mac address
*/ */
@OptIn(ExperimentalNativeApi::class) @OptIn(ExperimentalNativeApi::class)
@CName("random_mac_address")
public fun randomMacAddress(): String { public fun randomMacAddress(): String {
val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() } val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() }
return bytes.joinToString(":") { it.toHexString() } return bytes.joinToString(":") { it.toHexString() }
@@ -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<String, String>,
): 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])
}
@@ -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()
}
@@ -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)
}
@@ -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()
}
}
@@ -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)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test package test
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1 import cn.rtast.nmdns.sleep1
import org.junit.Test import org.junit.Test
@@ -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<IntVar>()
reuse.value = 1
if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf<IntVar>().convert()) < 0) {
perror("Failed to setsocketopt SO_REUSEADDR")
}
}
memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert()) < 0) {
perror("Failed to bind")
close(socket)
}
}
memScoped {
val mreq = alloc<ip_mreq>()
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<ip_mreq>().convert()) < 0) {
perror("setsockopt IP_ADD_MEMBERSHIP")
}
}
return this
}
public actual fun send(packet: ByteArray) {
memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert())
}
}
public actual fun destroy() {
close(socket)
}
}
@@ -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<IntVar>()
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt.ptr, sizeOf<IntVar>().convert())
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().convert())
}
public actual fun joinMulticastGroup(group: String): Unit = memScoped {
val merq = alloc<ip_mreq>()
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<ip_mreq>().convert())
}
public actual fun send(data: ByteArray, host: String, port: Int): Unit = memScoped {
val addr = alloc<sockaddr_in>()
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<sockaddr_in>().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<ip_mreq>()
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<ip_mreq>().convert())
}
}
+17 -2
View File
@@ -7,7 +7,10 @@
package test package test
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.onBroadcast
import cn.rtast.nmdns.onRegister
import cn.rtast.nmdns.onUnregistered
import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1 import cn.rtast.nmdns.sleep1
import kotlin.test.Test import kotlin.test.Test
@@ -30,7 +33,19 @@ class TestLinux {
"srcvers=220.68", "srcvers=220.68",
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
) )
) ) {
onBroadcast {
println(111)
}
onRegister {
println("reg")
}
onUnregistered {
println("unreg")
}
}
while (true) { while (true) {
service.broadcast() service.broadcast()
sleep1(2) sleep1(2)
+1 -1
View File
@@ -7,7 +7,7 @@
package test package test
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.util.randomMacAddress
import kotlin.test.Test import kotlin.test.Test
class TestMacAddr { class TestMacAddr {
+1 -1
View File
@@ -9,7 +9,7 @@ package test
import cn.rtast.nmdns.buildPacket import cn.rtast.nmdns.buildPacket
import cn.rtast.nmdns.createSocket import cn.rtast.nmdns.createSocket
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.sleep1 import cn.rtast.nmdns.sleep1
import kotlin.test.Test import kotlin.test.Test
@@ -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<WSADATA>()
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<IntVar>().convert()
)
if (result != 0) println("setsockopt failed")
val addr = alloc<SOCKADDR_IN>()
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<SOCKADDR_IN>().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<SOCKADDR_IN>()
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<SOCKADDR_IN>().convert()
)
if (sent == SOCKET_ERROR) perror("Send failed")
}
}
public actual fun destroy() {
if (sock != INVALID_SOCKET) {
closesocket(sock)
sock = INVALID_SOCKET
}
WSACleanup()
}
}
@@ -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<WSADATA>()
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<IntVar>().convert())
val addr = alloc<SOCKADDR_IN>()
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<SOCKADDR_IN>().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<SOCKADDR_IN>()
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)
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
package test package test
import cn.rtast.nmdns.randomMacAddress import cn.rtast.nmdns.util.randomMacAddress
import cn.rtast.nmdns.registerService import cn.rtast.nmdns.registerService
import cn.rtast.nmdns.sleep1 import cn.rtast.nmdns.sleep1
import kotlin.test.Test import kotlin.test.Test
@@ -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<String> = listOf()
/**
* add txt record to list
*/
@CExport
@CName("add_txt_record")
public fun addTxtRecord(list: List<String>, value: String) {
list.toMutableList().add(value)
}
@@ -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())
}