Skip to content

KTOR-8490 Add SocketAddress::address #4857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions ktor-network/api/ktor-network.api
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ public final class io/ktor/network/sockets/InetSocketAddress : io/ktor/network/s
public final fun getHostname ()Ljava/lang/String;
public final fun getPort ()I
public fun hashCode ()I
public final fun resolveAddress ()[B
public fun toString ()Ljava/lang/String;
}

Expand Down
1 change: 1 addition & 0 deletions ktor-network/api/ktor-network.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ final class io.ktor.network.sockets/InetSocketAddress : io.ktor.network.sockets/
final fun copy(kotlin/String = ..., kotlin/Int = ...): io.ktor.network.sockets/InetSocketAddress // io.ktor.network.sockets/InetSocketAddress.copy|copy(kotlin.String;kotlin.Int){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // io.ktor.network.sockets/InetSocketAddress.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // io.ktor.network.sockets/InetSocketAddress.hashCode|hashCode(){}[0]
final fun resolveAddress(): kotlin/ByteArray? // io.ktor.network.sockets/InetSocketAddress.resolveAddress|resolveAddress(){}[0]
final fun toString(): kotlin/String // io.ktor.network.sockets/InetSocketAddress.toString|toString(){}[0]
}

Expand Down
12 changes: 12 additions & 0 deletions ktor-network/common/src/io/ktor/network/sockets/SocketAddress.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ public expect class InetSocketAddress(
*/
public val port: Int

/**
* Returns the raw IP address bytes of this socket address.
*
* The returned array is 4 bytes for IPv4 addresses and 16 bytes for IPv6 addresses.
* Returns null if the address cannot be resolved or is not a valid IP address.
*
* Always returns null for wasm/js targets.
*
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.network.sockets.InetSocketAddress.address)
*/
public fun resolveAddress(): ByteArray?

/**
* The hostname of the socket address.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
package io.ktor.network.sockets

internal actual fun isUnixSocketSupported(): Boolean = false

internal actual fun InetSocketAddress.platformResolveAddress(): ByteArray? {
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public actual class InetSocketAddress internal constructor(

public actual val port: Int get() = address.port

public actual fun resolveAddress(): ByteArray? = address.address?.address

public actual constructor(hostname: String, port: Int) :
this(java.net.InetSocketAddress(hostname, port))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public actual class InetSocketAddress actual constructor(
public actual val hostname: String,
public actual val port: Int
) : SocketAddress() {
public actual fun resolveAddress(): ByteArray? {
return platformResolveAddress()
}

/**
* Create a copy of [InetSocketAddress].
*
Expand Down Expand Up @@ -100,3 +104,5 @@ public actual class UnixSocketAddress actual constructor(
}

internal expect fun isUnixSocketSupported(): Boolean

internal expect fun InetSocketAddress.platformResolveAddress(): ByteArray?
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.network.sockets

import io.ktor.network.util.NativeIPv4SocketAddress
import io.ktor.network.util.NativeIPv6SocketAddress
import io.ktor.network.util.resolve

internal actual fun InetSocketAddress.platformResolveAddress(): ByteArray? {
return this.resolve().firstOrNull()?.let {
when (it) {
is NativeIPv4SocketAddress -> {
try {
val octets = it.ipString.split('.', limit = 4).map { o ->
o.toUByte().toByte()
}

List(4) { i ->
octets.getOrElse(i) { 0 }
}.toByteArray()
Comment on lines +20 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can skip creating intermediate list here:

ByteArray(4) { index ->
    octets.getOrElse(index) { 0 }
}

} catch (_: Throwable) {
null
}
}
is NativeIPv6SocketAddress -> {
try {
val groups = it.ipString.split(':', limit = 8)
val emptyGroups = 8 - groups.count { g -> g.isNotEmpty() }

val bytes = groups.flatMap { g ->
if (g.isEmpty()) {
List(emptyGroups * 2) { 0 }
} else {
val int = g.toInt(16)
listOf((int shr 8).toByte(), int.toByte())
}
}
List(16) { i -> bytes.getOrElse(i) { 0 } }.toByteArray()
Comment on lines +28 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move IPv4 and IPv6 parsing logic into internal functions so they can be tested? And could you also add these tests? You can take CookieDateParserTest as an example of parametrized tests in non-JVM source set.

} catch (_: Throwable) {
null
}
}
Comment on lines +27 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical issue: IPv6 compression handling is incorrect.

The current IPv6 parsing logic has a fundamental flaw in handling IPv6 address compression (::). The emptyGroups calculation and expansion logic doesn't correctly handle the :: notation.

Issues:

  1. emptyGroups = 8 - groups.count { g -> g.isNotEmpty() } incorrectly assumes all empty strings represent compressed groups
  2. IPv6 :: creates multiple empty strings when split, but represents a single compression sequence
  3. The flat mapping expands every empty group independently, which can create incorrect byte arrays

Consider this corrected approach:

-                    val groups = it.ipString.split(':', limit = 8)
-                    val emptyGroups = 8 - groups.count { g -> g.isNotEmpty() }
-
-                    val bytes = groups.flatMap { g ->
-                        if (g.isEmpty()) {
-                            List(emptyGroups * 2) { 0 }
-                        } else {
-                            val int = g.toInt(16)
-                            listOf((int shr 8).toByte(), int.toByte())
-                        }
-                    }
+                    val groups = it.ipString.split(':')
+                    val doubleColonIndex = it.ipString.indexOf("::")
+                    
+                    val bytes = if (doubleColonIndex >= 0) {
+                        // Handle compression
+                        val beforeCompression = it.ipString.substring(0, doubleColonIndex).split(':').filter { it.isNotEmpty() }
+                        val afterCompression = it.ipString.substring(doubleColonIndex + 2).split(':').filter { it.isNotEmpty() }
+                        val compressedGroups = 8 - beforeCompression.size - afterCompression.size
+                        
+                        (beforeCompression + List(compressedGroups) { "0" } + afterCompression).flatMap { g ->
+                            val int = g.toInt(16)
+                            listOf((int shr 8).toByte(), int.toByte())
+                        }
+                    } else {
+                        // No compression
+                        groups.flatMap { g ->
+                            val int = g.toInt(16)
+                            listOf((int shr 8).toByte(), int.toByte())
+                        }
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
is NativeIPv6SocketAddress -> {
try {
val groups = it.ipString.split(':', limit = 8)
val emptyGroups = 8 - groups.count { g -> g.isNotEmpty() }
val bytes = groups.flatMap { g ->
if (g.isEmpty()) {
List(emptyGroups * 2) { 0 }
} else {
val int = g.toInt(16)
listOf((int shr 8).toByte(), int.toByte())
}
}
List(16) { i -> bytes.getOrElse(i) { 0 } }.toByteArray()
} catch (_: Throwable) {
null
}
}
is NativeIPv6SocketAddress -> {
try {
val groups = it.ipString.split(':')
val doubleColonIndex = it.ipString.indexOf("::")
val bytes = if (doubleColonIndex >= 0) {
// Handle compression
val beforeCompression = it.ipString
.substring(0, doubleColonIndex)
.split(':')
.filter { it.isNotEmpty() }
val afterCompression = it.ipString
.substring(doubleColonIndex + 2)
.split(':')
.filter { it.isNotEmpty() }
val compressedGroups = 8 - beforeCompression.size - afterCompression.size
(beforeCompression + List(compressedGroups) { "0" } + afterCompression)
.flatMap { g ->
val int = g.toInt(16)
listOf((int shr 8).toByte(), int.toByte())
}
} else {
// No compression
groups.flatMap { g ->
val int = g.toInt(16)
listOf((int shr 8).toByte(), int.toByte())
}
}
List(16) { i -> bytes.getOrElse(i) { 0 } }.toByteArray()
} catch (_: Throwable) {
null
}
}
🤖 Prompt for AI Agents
In ktor-network/posix/src/io/ktor/network/sockets/SocketAddress.nonJvm.posix.kt
between lines 27 and 44, the IPv6 address parsing incorrectly handles the
compression (::) by miscounting empty groups and expanding each empty string
separately, leading to invalid byte arrays. To fix this, revise the logic to
detect the single compression sequence represented by ::, calculate the exact
number of zero groups to insert, and expand only once at the compression point
instead of expanding every empty string independently. Adjust the splitting and
reconstruction of the byte array accordingly to produce a correct 16-byte IPv6
address representation.

else -> null
}
}
}
Comment on lines +11 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify IPv6 parsing with comprehensive test cases.

The IPv6 parsing logic should be thoroughly tested with various IPv6 formats to ensure correctness.


🏁 Script executed:

#!/bin/bash
# Search for existing IPv6 test cases in the codebase to understand expected behavior
echo "Searching for IPv6 test cases..."
rg -A 10 -B 5 "IPv6|::.*:" --type kt

echo "Searching for socket address tests..."
rg -A 10 -B 5 "InetSocketAddress.*test|test.*InetSocketAddress" --type kt

Length of output: 328


Here’s a more robust search across all Kotlin files for IPv6 patterns and existing socket‐address tests:


🏁 Script executed:

#!/bin/bash
echo "Searching for IPv6 literals or references in Kotlin sources..."
rg -n "::|IPv6" -g '*.kt'

echo "Searching for any tests or usages of InetSocketAddress in Kotlin sources..."
rg -n "InetSocketAddress" -g '*.kt'

Length of output: 68033


Add comprehensive IPv6 parsing tests for platformResolveAddress

No existing tests cover the POSIX implementation of InetSocketAddress.platformResolveAddress() in SocketAddress.nonJvm.posix.kt. Please add unit tests to validate correct byte‐array output for a variety of IPv6 inputs, including:

  • Full 8-group addresses (e.g. “2001:db8:0:85a3:0:0:8a2e:370:7334”)
  • Compressed zeros (“::”, “::1”, “2001:db8::”)
  • Mixed IPv4-embedded addresses (“::ffff:192.0.2.128”)
  • Leading, trailing or multiple zero blocks
  • Invalid strings (ensure null is returned)

Suggested test locations:
ktor-network/posix/src/io/ktor/network/sockets/SocketAddress.nonJvm.posix.kt (implementation)
• New test file under ktor-network/jvmAndPosix/test/io/ktor/network/sockets/SocketAddressPosixTest.kt

🤖 Prompt for AI Agents
In ktor-network/posix/src/io/ktor/network/sockets/SocketAddress.nonJvm.posix.kt
around lines 11 to 48, the platformResolveAddress function's IPv6 parsing lacks
comprehensive test coverage. Add unit tests in a new test file, e.g.,
ktor-network/jvmAndPosix/test/io/ktor/network/sockets/SocketAddressPosixTest.kt,
to validate correct byte-array outputs for various IPv6 formats including full
8-group addresses, compressed zeros, mixed IPv4-embedded addresses, and cases
with leading, trailing, or multiple zero blocks. Also include tests for invalid
IPv6 strings to confirm the function returns null as expected.