From eb19b3e36abe9576c35a45e266755c9f152b743e Mon Sep 17 00:00:00 2001 From: Andrew Golovashevich Date: Thu, 20 Mar 2025 02:41:44 +0300 Subject: [PATCH] Stub of ping object --- .gitmodules | 3 + libs/int-serializers | 1 + .../networks0/build_script/Dependencies.kt | 2 + .../bgtu/networks0/build_script/Versions.kt | 2 + modules/icmp/build.gradle.kts | 27 +++++ .../bgtu/networks0/icmp/IcmpHeader.kt | 10 ++ .../bgtu/networks0/icmp/IcmpPingHeader.kt | 31 +++++ .../bgtu/networks0/icmp/Ipv4Checksum.kt | 56 +++++++++ .../bgtu/networks0/icmp/PingSocket.kt | 112 ++++++++++++++++++ .../bgtu/networks0/icmp/entry_points.kt | 6 + .../low_level/sockets/EpollSocketEventLoop.kt | 14 ++- .../sockets/NonblockingIcmpSocketImpl.kt | 7 +- .../bgtu/networks0/utilities/auto_close.kt | 3 +- programs/test/build.gradle.kts | 1 + programs/test/src/linuxX64Main/kotlin/main.kt | 43 +------ settings.gradle.kts | 4 + 16 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 .gitmodules create mode 160000 libs/int-serializers create mode 100644 modules/icmp/build.gradle.kts create mode 100644 modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpHeader.kt create mode 100644 modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpPingHeader.kt create mode 100644 modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/Ipv4Checksum.kt create mode 100644 modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/PingSocket.kt create mode 100644 modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/entry_points.kt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d0a50c1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/int-serializers"] + path = libs/int-serializers + url = https://git.landgrafhomyak.ru/xomrk/int-serializers.kt diff --git a/libs/int-serializers b/libs/int-serializers new file mode 160000 index 0000000..1d4b797 --- /dev/null +++ b/libs/int-serializers @@ -0,0 +1 @@ +Subproject commit 1d4b797c31e0fa622c2fb411c3e202de85b3aadc diff --git a/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Dependencies.kt b/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Dependencies.kt index 7a3fc3d..1ec0b34 100644 --- a/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Dependencies.kt +++ b/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Dependencies.kt @@ -3,4 +3,6 @@ package ru.landgrafhomyak.bgtu.networks0.build_script object Dependencies { val kotlin_atomicfu = "org.jetbrains.kotlinx:atomicfu:${Versions.kotlin_atomicfu}" val kotlin_coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlin_coroutines_core}" + val int_serializers = "ru.landgrafhomyak.utility:int-serializers:${Versions.int_serializers}" + val kotlin_datetime = "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.kotlin_datetime}" } \ No newline at end of file diff --git a/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Versions.kt b/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Versions.kt index 8721e35..9f7390f 100644 --- a/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Versions.kt +++ b/modules/build-script/src/main/kotlin/ru/landgrafhomyak/bgtu/networks0/build_script/Versions.kt @@ -4,4 +4,6 @@ package ru.landgrafhomyak.bgtu.networks0.build_script object Versions { val kotlin_atomicfu = "0.27.0" val kotlin_coroutines_core = "1.10.1" + val int_serializers = "1.1" + val kotlin_datetime = "0.6.2" } \ No newline at end of file diff --git a/modules/icmp/build.gradle.kts b/modules/icmp/build.gradle.kts new file mode 100644 index 0000000..4e7f291 --- /dev/null +++ b/modules/icmp/build.gradle.kts @@ -0,0 +1,27 @@ +import ru.landgrafhomyak.bgtu.networks0.build_script.Dependencies +import ru.landgrafhomyak.kotlin.kmp_gradle_build_helper.defineAllMultiplatformTargets + +plugins { + kotlin("multiplatform") +} + +repositories { + mavenCentral() + maven("https://maven.landgrafhomyak.ru/") +} + + +kotlin { + defineAllMultiplatformTargets() + + sourceSets { + commonMain { + dependencies { + implementation(Dependencies.kotlin_coroutines_core) + implementation(Dependencies.kotlin_datetime) + implementation(Dependencies.int_serializers) + implementation(project(":modules:low-level:sockets")) + } + } + } +} \ No newline at end of file diff --git a/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpHeader.kt b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpHeader.kt new file mode 100644 index 0000000..6063e8e --- /dev/null +++ b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpHeader.kt @@ -0,0 +1,10 @@ +package ru.landgrafhomyak.bgtu.networks0.icmp + +@OptIn(ExperimentalUnsignedTypes::class) +interface IcmpHeader { + val type: UByte + val code: UByte + fun checksum(withData: UByteArray): UShort + + fun serialize(withData: UByteArray): UByteArray +} \ No newline at end of file diff --git a/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpPingHeader.kt b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpPingHeader.kt new file mode 100644 index 0000000..2f290d3 --- /dev/null +++ b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/IcmpPingHeader.kt @@ -0,0 +1,31 @@ +package ru.landgrafhomyak.bgtu.networks0.icmp + +import ru.landgrafhomyak.utility.IntSerializers + +@OptIn(ExperimentalUnsignedTypes::class) +class IcmpPingHeader( + val identifier_be: UShort, + val seqNo_be: UShort, +) : IcmpHeader { + override val type: UByte get() = 8u + override val code: UByte get() = 0u + + override fun checksum(withData: UByteArray): UShort = Ipv4Checksum.calculate { c -> + c.addFirstByte(this.type) + c.addSecondByte(this.code) + c.addWord(this.identifier_be) + c.addWord(this.seqNo_be) + c.addTrailingData(withData) + } + + override fun serialize(withData: UByteArray): UByteArray { + val serialized = UByteArray(withData.size + 8) + IntSerializers.encode8Bu(serialized, 0, this.type) + IntSerializers.encode8Bu(serialized, 1, this.code) + IntSerializers.encode16beHu(serialized, 2, this.checksum(withData)) + IntSerializers.encode16beHu(serialized, 4, this.identifier_be) + IntSerializers.encode16beHu(serialized, 6, this.seqNo_be) + withData.copyInto(serialized, 8) + return serialized + } +} \ No newline at end of file diff --git a/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/Ipv4Checksum.kt b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/Ipv4Checksum.kt new file mode 100644 index 0000000..2014836 --- /dev/null +++ b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/Ipv4Checksum.kt @@ -0,0 +1,56 @@ +package ru.landgrafhomyak.bgtu.networks0.icmp + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@OptIn(ExperimentalUnsignedTypes::class) +internal class Ipv4Checksum() { + private var _accumulator: UInt = 0u + + fun addFirstByte(b: UByte) { + this._accumulator += (b.toUInt() shl 8) + } + + fun addSecondByte(b: UByte) { + this._accumulator += b + } + + fun addWord(b: UShort) { + this._accumulator += b + } + + fun addLastByte(b: UByte) { + this._accumulator += b + } + + fun addTrailingData(data: UByteArray) { + for (i in data.indices step 2) { + this.addFirstByte(data[i]) + this.addSecondByte(data[i + 1]) + } + if (data.size % 2 != 0) + this.addLastByte(data.last()) + } + + fun export(): UShort { + var out: UInt = this._accumulator + while (out > 0xFFFFu) { + out = (out and 0xFFFFu) + (out shr 16); + } + return out.toUShort().inv() + } + + companion object { + @OptIn(ExperimentalContracts::class) + inline fun calculate(builder: (Ipv4Checksum) -> Unit): UShort { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + val b = Ipv4Checksum() + builder(b) + return b.export() + } + } +} \ No newline at end of file diff --git a/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/PingSocket.kt b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/PingSocket.kt new file mode 100644 index 0000000..30c1ceb --- /dev/null +++ b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/PingSocket.kt @@ -0,0 +1,112 @@ +package ru.landgrafhomyak.bgtu.networks0.icmp + +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeout +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.coroutines.sync.Mutex as CMutex +import ru.landgrafhomyak.bgtu.networks0.low_level.sockets.IcmpSocket +import ru.landgrafhomyak.bgtu.networks0.utilities.safeAutoClose1 +import ru.landgrafhomyak.bgtu.networks0.utilities.safeAutoClose2 +import ru.landgrafhomyak.utility.IntSerializers + + +@OptIn(ExperimentalUnsignedTypes::class) +class PingSocket : AutoCloseable { + private val _rawSocket: IcmpSocket + private val _mapSync: CMutex + private val _resumeMap: HashMap> + private val _recvCoro: Job + + internal constructor(rawSocket: IcmpSocket) { + safeAutoClose2(onAbort = rawSocket::close) { + this._rawSocket = rawSocket + this._resumeMap = HashMap() + this._mapSync = CMutex() + this._recvCoro = CoroutineScope(Dispatchers.Default).launch { this@PingSocket._recvRoutine() } + } + } + + private class ShortKeyPair(val identifier: UShort, val seqNo: UShort) { + override fun equals(other: Any?): Boolean { + if (other !is ShortKeyPair) return false + return this.identifier == other.identifier && this.seqNo == other.seqNo + } + + override fun hashCode(): Int { + return ((this.identifier.toUInt() shl 16) or (this.seqNo).toUInt()).toInt() + } + + override fun toString(): String = "" + } + + private fun _generateKeyPair(): ShortKeyPair { + val timestamp = Clock.System.now() + return ShortKeyPair( + identifier = (timestamp.epochSeconds ushr 13).toULong().toUShort(), + seqNo = ((timestamp.epochSeconds shl 3) or ((((timestamp.nanosecondsOfSecond ushr 27) and 7).toLong())) and 0xFFFF).toULong().toUShort(), + ) + } + + suspend fun ping(timeoutMillis: UInt): UInt? { + var isLocked = true + val start: Instant + this._mapSync.lock() + safeAutoClose1(finally = { if (isLocked) this._mapSync.unlock() }) { + val key = this._generateKeyPair() + val header = IcmpPingHeader(identifier_be = key.identifier, seqNo_be = key.seqNo) + val serialized = header.serialize(PingSocket.randomData) + start = Clock.System.now() + this._rawSocket.sendRaw(serialized) + try { + withTimeout(timeoutMillis.toULong().toLong()) { + suspendCancellableCoroutine { continuation -> + if (key in this@PingSocket._resumeMap) + throw RuntimeException("Key duplication (shouldn't happen)") + this@PingSocket._resumeMap[key] = continuation + this@PingSocket._mapSync.unlock() + isLocked = false + } + } + } catch (_: TimeoutCancellationException) { + return null + } + } + + val td = Clock.System.now() - start + return td.inWholeMilliseconds.toUInt() + } + + private suspend fun _recvRoutine() { + try { + // todo handle errors + while (true) { + val raw = this._rawSocket.recvRaw() + if (raw.size < 8) continue + val key = ShortKeyPair(identifier = IntSerializers.decode16beHu(raw, 4), seqNo = IntSerializers.decode16beHu(raw, 6)) + val continuation = this._mapSync.withLock { this._resumeMap.remove(key) } + if (continuation == null) continue + continuation.resume(Unit) + } + } catch (e: Throwable) { + e.printStackTrace() + } + } + + override fun close() { + TODO("Not yet implemented") + } + + companion object { + @Suppress("SpellCheckingInspection") + private val randomData = "abcdefghijklmnopqrstuvwabcdefghi".encodeToByteArray().asUByteArray() + } +} \ No newline at end of file diff --git a/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/entry_points.kt b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/entry_points.kt new file mode 100644 index 0000000..df5ad64 --- /dev/null +++ b/modules/icmp/src/commonMain/kotlin/ru/landgrafhomyak/bgtu/networks0/icmp/entry_points.kt @@ -0,0 +1,6 @@ +package ru.landgrafhomyak.bgtu.networks0.icmp + +import ru.landgrafhomyak.bgtu.networks0.low_level.sockets.SocketEventLoopScope + +fun SocketEventLoopScope.pingSocket_IPv4(addr: String): PingSocket = PingSocket(this.icmp_IPv4(addr)) +fun SocketEventLoopScope.pingSocket_IPv6(addr: String): PingSocket = PingSocket(this.icmp_IPv6(addr)) \ No newline at end of file diff --git a/modules/low-level/sockets/src/linuxMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/EpollSocketEventLoop.kt b/modules/low-level/sockets/src/linuxMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/EpollSocketEventLoop.kt index 811c2e5..fde3b74 100644 --- a/modules/low-level/sockets/src/linuxMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/EpollSocketEventLoop.kt +++ b/modules/low-level/sockets/src/linuxMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/EpollSocketEventLoop.kt @@ -12,6 +12,9 @@ import kotlinx.cinterop.cValue import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr +import kotlinx.coroutines.suspendCancellableCoroutine +import platform.linux.EPOLLERR +import platform.linux.EPOLLHUP import platform.linux.EPOLLIN import platform.linux.EPOLLONESHOT import platform.linux.EPOLLOUT @@ -32,6 +35,7 @@ import ru.landgrafhomyak.bgtu.networks0.low_level.multithreading.withLock import ru.landgrafhomyak.bgtu.networks0.utilities.CloseableRefCounter import ru.landgrafhomyak.bgtu.networks0.utilities.safeAutoClose1 import ru.landgrafhomyak.bgtu.networks0.utilities.safeAutoClose2 +import ru.landgrafhomyak.bgtu.networks0.utilities.safeAutoClose2e @OptIn(ExperimentalForeignApi::class) @Suppress("JoinDeclarationAndAssignment", "ConvertSecondaryConstructorToPrimary", "FunctionName") @@ -66,7 +70,7 @@ class EpollSocketEventLoop : SocketEventLoopScope.Closeable { TODO("Not yet implemented") } - private fun _waitFor(metadata: _SocketMetadata) { + private fun _schedule(metadata: _SocketMetadata) { val ee = cValue { this.events = ((if (metadata.read == null) 0 else EPOLLIN) or (if (metadata.write == null) 0 else EPOLLOUT) or EPOLLONESHOT).toUInt() this.data.fd = metadata.fd @@ -96,7 +100,7 @@ class EpollSocketEventLoop : SocketEventLoopScope.Closeable { if (metadata.read != null) throw IllegalStateException("Socket already waiting for read") metadata.read = continuation - this._waitFor(metadata) + this._schedule(metadata) } } @@ -106,7 +110,7 @@ class EpollSocketEventLoop : SocketEventLoopScope.Closeable { if (metadata.write != null) throw IllegalStateException("Socket already waiting for write") metadata.write = continuation - this._waitFor(metadata) + this._schedule(metadata) } } @@ -118,11 +122,11 @@ class EpollSocketEventLoop : SocketEventLoopScope.Closeable { metadata.read = null } if ((ev.events and EPOLLOUT.toUInt()) != 0u) { - (metadata.write ?: throw RuntimeException("Socket ready for read, but nobody requests")).resume(Unit) + (metadata.write ?: throw RuntimeException("Socket ready for write, but nobody requests")).resume(Unit) metadata.write = null } if (metadata.read != null || metadata.write != null) - this._waitFor(metadata) + this._schedule(metadata) } } diff --git a/modules/low-level/sockets/src/posixMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/NonblockingIcmpSocketImpl.kt b/modules/low-level/sockets/src/posixMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/NonblockingIcmpSocketImpl.kt index 913548e..0d65dc0 100644 --- a/modules/low-level/sockets/src/posixMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/NonblockingIcmpSocketImpl.kt +++ b/modules/low-level/sockets/src/posixMain/kotlin/ru/landgrafhomyak/bgtu/networks0/low_level/sockets/NonblockingIcmpSocketImpl.kt @@ -121,6 +121,7 @@ internal abstract class NonblockingIcmpSocketImpl : IcmpSocket { this._socketFd, null, 0u, MSG_NOSIGNAL or MSG_PEEK or MSG_TRUNC ) + if (bytesAvailable == 0L) continue@polling if (bytesAvailable < 0) { when (errno) { @@ -130,16 +131,16 @@ internal abstract class NonblockingIcmpSocketImpl : IcmpSocket { } val data = UByteArray(bytesAvailable.toInt()) - var sentCount = data.usePinned { pinnedData -> + var receivedCount = data.usePinned { pinnedData -> return@usePinned recv_lowlevel(this._socketFd, pinnedData.addressOf(0), data.size.toULong(), MSG_NOSIGNAL) } - if (sentCount < 0) { + if (receivedCount < 0) { when (errno) { EAGAIN, EWOULDBLOCK -> continue@polling else -> PosixUtilities.throwErrno { d -> RuntimeException("Failed to send message over ICMP socket: $d") } } } - return data + return data.sliceArray(((data[0] and 0xFu).toUInt().toInt() + 15).. safeAutoClose3e( if (!wasError) { if (crossReturned) onCrossReturn() - onSuccess() + else + onSuccess() } } return ret diff --git a/programs/test/build.gradle.kts b/programs/test/build.gradle.kts index 645050a..ebae051 100644 --- a/programs/test/build.gradle.kts +++ b/programs/test/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { dependencies { implementation(project(":modules:low-level:multithreading")) implementation(project(":modules:low-level:sockets")) + implementation(project(":modules:icmp")) } } } diff --git a/programs/test/src/linuxX64Main/kotlin/main.kt b/programs/test/src/linuxX64Main/kotlin/main.kt index 472e82f..c7dfbea 100644 --- a/programs/test/src/linuxX64Main/kotlin/main.kt +++ b/programs/test/src/linuxX64Main/kotlin/main.kt @@ -1,6 +1,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import ru.landgrafhomyak.bgtu.networks0.icmp.pingSocket_IPv4 import ru.landgrafhomyak.bgtu.networks0.low_level.multithreading.Thread import ru.landgrafhomyak.bgtu.networks0.low_level.sockets.EpollSocketEventLoop @@ -24,48 +25,14 @@ fun main() { val loopThread = Thread(EventLoopRoutine(loop)) loopThread.start() - val googleSock = loop.icmp_IPv4("8.8.8.8") - val yandexSock = loop.icmp_IPv4("5.255.255.70") - val phoneSock = loop.icmp_IPv4("192.168.43.1") - val pingData = ubyteArrayOf( - 0x08u, 0x00u, 0x4Cu, 0xE4u, 0x00u, 0x01u, 0x00u, 0x77u, - 0x61u, 0x62u, 0x63u, 0x64u, 0x65u, 0x66u, 0x67u, 0x68u, - 0x69u, 0x6Au, 0x6Bu, 0x6Cu, 0x6Du, 0x6Eu, 0x6Fu, 0x70u, - 0x71u, 0x72u, 0x73u, 0x74u, 0x75u, 0x76u, 0x77u, 0x61u, - 0x62u, 0x63u, 0x64u, 0x65u, 0x66u, 0x67u, 0x68u, 0x69u - ) + val googleSock = loop.pingSocket_IPv4("8.8.8.8") runBlocking { - launch { - while (true) { - googleSock.sendRaw(pingData) - print("sent to google\n") - googleSock.recvRaw() - print("got from google\n") - delay(1000) - } - } - launch { - while (true) { - yandexSock.sendRaw(pingData) - print("sent to yandex\n") - yandexSock.recvRaw() - print("got from yandex\n") - delay(2500) - } - } - launch { - while (true) { - phoneSock.sendRaw(pingData) - print("sent to phone\n") - phoneSock.recvRaw() - print("got from phone\n") - delay(5500) - } + while (true) { + print("google: ${googleSock.ping(1000u)}ms\n") + delay(1000) } } - - } catch (e: Throwable) { e.printStackTrace() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3822174..1cd10e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,8 +21,12 @@ pluginManagement { } } } + + +includeBuild("./libs/int-serializers") include(":modules:utilities") include(":modules:low-level:c-interop-utilities") include(":modules:low-level:multithreading") include(":modules:low-level:sockets") +include(":modules:icmp") include(":programs:test") \ No newline at end of file