Skip to content

Commit 2b53b6e

Browse files
authored
Merge pull request #677 from openziti/ziti-socket-timeout-stall-676
Ziti socket timeout stall
2 parents 93742f9 + 4195742 commit 2b53b6e

File tree

14 files changed

+412
-92
lines changed

14 files changed

+412
-92
lines changed

ziti/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ testing {
135135
val integrationTest by registering(JvmTestSuite::class) {
136136
dependencies {
137137
implementation(libs.kotlin.test)
138+
implementation(libs.kotlin.coroutines.lib)
138139
implementation(libs.kotlin.coroutines.test)
139140
implementation(libs.slf4j.simple)
140141
implementation(project(":management-api"))
@@ -172,7 +173,7 @@ tasks.register("start-quickstart") {
172173
val pb = ProcessBuilder().apply {
173174
command(
174175
zitiCLI.toString(),
175-
"edge", "quickstart", "--home", quickstartHome.asFile.absolutePath)
176+
"edge", "quickstart", "--verbose", "--home", quickstartHome.asFile.absolutePath)
176177
redirectOutput(qsLog)
177178
redirectError(errLog)
178179
}

ziti/src/integrationTest/kotlin/org/openziti/api/ControllerTests.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,7 @@ class ControllerTests: BaseTest() {
4343
Ziti.setApplicationInfo(info.displayName, appVersion)
4444

4545
idName = "id-${info.displayName}-${System.nanoTime()}"
46-
val token = createIdentity(idName)
47-
48-
val enrollment = Ziti.createEnrollment(token)
49-
assertEquals(enrollment.getMethod(), Enrollment.Method.ott)
50-
51-
cfg = enrollment.enroll()
46+
cfg = createIdentity(idName)
5247
}
5348

5449
@Test

ziti/src/integrationTest/kotlin/org/openziti/integ/BaseTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package org.openziti.integ
1818

19+
import org.junit.jupiter.api.Assertions
1920
import org.junit.jupiter.api.BeforeEach
2021
import org.junit.jupiter.api.TestInfo
22+
import org.openziti.Enrollment
23+
import org.openziti.IdentityConfig
24+
import org.openziti.Ziti
2125

2226
abstract class BaseTest {
2327
protected lateinit var info: TestInfo
@@ -27,4 +31,12 @@ abstract class BaseTest {
2731
info = testInfo
2832
}
2933

34+
fun createIdentity(name: String = "id-${info.displayName}-${System.nanoTime()}"): IdentityConfig {
35+
val token = ManagementHelper.createIdentity(name)
36+
37+
val enrollment = Ziti.createEnrollment(token)
38+
Assertions.assertEquals(enrollment.getMethod(), Enrollment.Method.ott)
39+
40+
return enrollment.enroll()
41+
}
3042
}

ziti/src/integrationTest/kotlin/org/openziti/integ/ManagementHelper.kt

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ package org.openziti.integ
1818

1919
import kotlinx.coroutines.Dispatchers
2020
import kotlinx.coroutines.asExecutor
21+
import okhttp3.internal.notifyAll
22+
import org.openziti.api.InterceptConfig
23+
import org.openziti.api.InterceptV1Cfg
2124
import org.openziti.management.ApiClient
25+
import org.openziti.management.JSON
2226
import org.openziti.management.api.AuthenticationApi
27+
import org.openziti.management.api.ConfigApi
2328
import org.openziti.management.api.EnrollmentApi
2429
import org.openziti.management.api.IdentityApi
2530
import org.openziti.management.api.InformationalApi
26-
import org.openziti.management.model.Authenticate
27-
import org.openziti.management.model.EnrollmentCreate
28-
import org.openziti.management.model.IdentityCreate
29-
import org.openziti.management.model.IdentityType
31+
import org.openziti.management.api.ServiceApi
32+
import org.openziti.management.api.ServicePolicyApi
33+
import org.openziti.management.model.*
3034
import org.openziti.util.fingerprint
3135
import org.openziti.util.parsePKCS7
3236
import java.net.InetAddress
@@ -69,8 +73,10 @@ internal object ManagementHelper {
6973
parsePKCS7(pkcs7).makeSSL()
7074
}
7175

72-
internal val identityApi by lazy { IdentityApi(api) }
73-
internal val enrollmentApi by lazy { EnrollmentApi(api) }
76+
private val identityApi by lazy { IdentityApi(api) }
77+
private val enrollmentApi by lazy { EnrollmentApi(api) }
78+
private val serviceApi by lazy { ServiceApi(api) }
79+
private val spApi by lazy { ServicePolicyApi(api) }
7480

7581
private object TrustAll : X509TrustManager {
7682
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
@@ -143,4 +149,48 @@ internal object ManagementHelper {
143149
internal fun <T> CompletableFuture<T>.waitFor(timeout: Duration = Duration.of(5, ChronoUnit.SECONDS)): T =
144150
this.get(timeout.toMillis(), TimeUnit.MILLISECONDS)
145151

152+
internal fun createService(
153+
srvName: String = "service-${System.nanoTime()}",
154+
dialRoles: List<String> = listOf("#all"),
155+
bindRoles: List<String> = listOf("#all"),
156+
configs: Map<String, Any>,
157+
): String {
158+
159+
val cfgIds = mutableListOf<String>()
160+
configs.forEach { (type, v) ->
161+
val typeId = ConfigApi(api).listConfigTypes(1,0, """name = "$type" """).waitFor().data.first().id
162+
val data = JSON.getDefault().mapper.convertValue(v, Map::class.java) as Map<String, *>
163+
val id = ConfigApi(api).createConfig(ConfigCreate().apply {
164+
name("$srvName-$type")
165+
configTypeId(typeId)
166+
data(data)
167+
}).waitFor().data!!.id!!
168+
cfgIds.add(id)
169+
}
170+
171+
val srvId = serviceApi.createService(ServiceCreate().apply {
172+
name = srvName
173+
encryptionRequired = true
174+
configs(cfgIds)
175+
}).waitFor().data!!.id
176+
177+
// dial policy
178+
spApi.createServicePolicy(ServicePolicyCreate().apply{
179+
name("$srvName-dial")
180+
type(DialBind.DIAL)
181+
serviceRoles(listOf("@$srvId"))
182+
identityRoles(dialRoles)
183+
semantic(Semantic.ANY_OF)
184+
}).waitFor()
185+
186+
// dial policy
187+
spApi.createServicePolicy(ServicePolicyCreate().apply{
188+
name("$srvName-bind")
189+
type(DialBind.BIND)
190+
serviceRoles(listOf("@$srvId"))
191+
identityRoles(bindRoles)
192+
semantic(Semantic.ANY_OF)
193+
}).waitFor()
194+
return srvName
195+
}
146196
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright (c) 2018-2025 NetFoundry Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openziti.net
18+
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.flow.filter
22+
import kotlinx.coroutines.flow.first
23+
import kotlinx.coroutines.launch
24+
import kotlinx.coroutines.test.runTest
25+
import kotlinx.coroutines.withContext
26+
import org.junit.jupiter.api.*
27+
import org.openziti.IdentityConfig
28+
import org.openziti.Ziti
29+
import org.openziti.ZitiAddress
30+
import org.openziti.ZitiContext
31+
import org.openziti.api.DNSName
32+
import org.openziti.api.InterceptConfig
33+
import org.openziti.api.InterceptV1Cfg
34+
import org.openziti.api.PortRange
35+
import org.openziti.edge.model.DialBind
36+
import org.openziti.integ.BaseTest
37+
import org.openziti.integ.ManagementHelper
38+
import org.openziti.net.nio.acceptSuspend
39+
import org.openziti.net.nio.readSuspend
40+
import org.openziti.net.nio.writeSuspend
41+
import java.net.ConnectException
42+
import java.net.InetSocketAddress
43+
import java.net.SocketTimeoutException
44+
import java.nio.ByteBuffer
45+
import java.nio.channels.InterruptedByTimeoutException
46+
import java.util.concurrent.ExecutionException
47+
import java.util.concurrent.TimeUnit
48+
import kotlin.test.assertContentEquals
49+
import kotlin.test.assertEquals
50+
import kotlin.test.assertFalse
51+
import kotlin.test.assertTrue
52+
import kotlin.time.Duration.Companion.seconds
53+
54+
class ConnectionTests: BaseTest() {
55+
56+
private val hostname = "test${System.nanoTime()}.ziti"
57+
private val port = 5000
58+
private lateinit var service: String
59+
private lateinit var cfg: IdentityConfig
60+
private lateinit var ztx: ZitiContext
61+
62+
@BeforeEach
63+
fun before() {
64+
service = ManagementHelper.createService(
65+
configs = mapOf(
66+
InterceptV1Cfg to InterceptConfig(
67+
protocols = setOf(Protocol.TCP),
68+
addresses = setOf(DNSName(hostname)),
69+
portRanges = sortedSetOf(PortRange(port, port)),
70+
)
71+
)
72+
)
73+
cfg = createIdentity()
74+
ztx = Ziti.newContext(cfg)
75+
}
76+
77+
@AfterEach
78+
fun after() {
79+
ztx.destroy()
80+
}
81+
82+
@Test
83+
fun `test dial without terminator`() = runTest(timeout = 10.seconds) {
84+
val srv = assertDoesNotThrow {
85+
ztx.serviceUpdates().filter { it.service.name == service }.first().service
86+
}
87+
88+
assertFalse(srv.config.isEmpty())
89+
assertThrows<ConnectException> {
90+
ztx.dial(service)
91+
}.run {
92+
assert(message!!.contains("has no terminators"))
93+
}
94+
95+
assertThrows<ExecutionException> {
96+
val ch = ztx.open()
97+
ch.connect(ZitiAddress.Dial(service)).get()
98+
}.run {
99+
assert(cause!!.message!!.contains("has no terminators"))
100+
}
101+
assertThrows<ExecutionException> {
102+
val ch = ztx.open()
103+
ch.connect(InetSocketAddress.createUnresolved(hostname, port)).get()
104+
}.run {
105+
assert(cause!!.message!!.contains("has no terminators"))
106+
}
107+
108+
assertThrows<ConnectException> {
109+
ztx.connect(hostname, port)
110+
}.run {
111+
assertTrue(message!!.contains("has no terminators"))
112+
}
113+
}
114+
115+
@Test
116+
fun `test bind-connect-read-timeout`() = runTest(timeout = 10.seconds) {
117+
val greeting = "Hello from Ziti".toByteArray()
118+
val s = assertDoesNotThrow {
119+
ztx.serviceUpdates().filter { it.service.name == service }.first().service
120+
}
121+
assertTrue(s.permissions.contains(DialBind.DIAL))
122+
assertTrue(s.permissions.contains(DialBind.BIND))
123+
124+
ztx.openServer().use { srv ->
125+
126+
srv.bind(ZitiAddress.Bind(service))
127+
128+
// wait for binding -- test dispatcher skips delays
129+
withContext(Dispatchers.Default) {
130+
val zrv = srv as ZitiServerSocketChannel
131+
while (zrv.state != ZitiServerSocketChannel.State.bound) {
132+
delay(50)
133+
}
134+
}
135+
136+
launch(Dispatchers.IO) {
137+
val c = srv.acceptSuspend()
138+
c.writeSuspend(ByteBuffer.wrap(greeting))
139+
delay(1000)
140+
c.close()
141+
}
142+
143+
ztx.open().use { clt ->
144+
clt.connect(ZitiAddress.Dial(service)).get(1, TimeUnit.SECONDS)
145+
146+
val buf = ByteBuffer.allocate(1024)
147+
148+
// read 1: return greeting
149+
val read1 = clt.readSuspend(buf, 100, TimeUnit.MILLISECONDS)
150+
assertEquals(read1, greeting.size)
151+
val readMsg = ByteArray(read1)
152+
buf.flip().get(readMsg)
153+
assertContentEquals(greeting, readMsg)
154+
assertFalse(buf.hasRemaining())
155+
156+
buf.clear()
157+
// read 2: should time out
158+
assertThrows<InterruptedByTimeoutException> {
159+
clt.readSuspend(buf, 600, TimeUnit.MILLISECONDS)
160+
}
161+
162+
buf.clear()
163+
// read 3: should get EOF
164+
assertEquals(-1, clt.readSuspend(buf, 600, TimeUnit.MILLISECONDS))
165+
}
166+
}
167+
}
168+
169+
@Test
170+
fun `test socket-connect-read-timeout`() = runTest(timeout = 10.seconds) {
171+
val greeting = "Hello from Ziti".toByteArray()
172+
val s = assertDoesNotThrow {
173+
ztx.serviceUpdates().filter { it.service.name == service }.first().service
174+
}
175+
assertTrue(s.permissions.contains(DialBind.DIAL))
176+
assertTrue(s.permissions.contains(DialBind.BIND))
177+
178+
ztx.openServer().use { srv ->
179+
180+
srv.bind(ZitiAddress.Bind(service))
181+
182+
// wait for binding -- test dispatcher skips delays
183+
withContext(Dispatchers.Default) {
184+
val zrv = srv as ZitiServerSocketChannel
185+
while (zrv.state != ZitiServerSocketChannel.State.bound) {
186+
delay(50)
187+
}
188+
}
189+
190+
launch(Dispatchers.IO) {
191+
val c = srv.acceptSuspend()
192+
c.writeSuspend(ByteBuffer.wrap(greeting))
193+
val b = ByteBuffer.allocate(1024)
194+
c.readSuspend(b)
195+
}
196+
197+
ztx.connect(hostname, port).use { clt ->
198+
assertTrue(clt.isConnected)
199+
val buf = ByteArray(1024)
200+
clt.soTimeout = 500
201+
val input = clt.getInputStream()
202+
203+
// read 1: return greeting
204+
val read1 = input.read(buf)
205+
assertEquals(read1, greeting.size)
206+
val readMsg = buf.sliceArray(0..<read1)
207+
assertContentEquals(greeting, readMsg)
208+
209+
// other reads would get a timeout
210+
for (i in 0 .. 10) {
211+
assertThrows<SocketTimeoutException> {
212+
input.read(buf)
213+
}
214+
}
215+
}
216+
}
217+
}
218+
219+
}

ziti/src/main/kotlin/org/openziti/api/Controller.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ import kotlin.reflect.full.memberFunctions
4646
internal class Controller(endpoint: String, sslContext: SSLContext):
4747
Logged by ZitiLog() {
4848

49+
companion object {
50+
val ALL_CONFIGS = listOf("all")
51+
}
52+
4953
private val pageSize = 100
5054

5155
private val http = HttpClient.newBuilder()
@@ -142,7 +146,7 @@ internal class Controller(endpoint: String, sslContext: SSLContext):
142146

143147
return pagingApiRequest {
144148
limit, offset -> serviceApi.listServices(limit, offset,
145-
null, null, null, null)
149+
null, ALL_CONFIGS, null, null)
146150
}
147151
}
148152

@@ -277,7 +281,7 @@ internal class Controller(endpoint: String, sslContext: SSLContext):
277281
.os(info.os)
278282
.osRelease(info.osRelease)
279283
.osVersion(info.osVersion)
280-
configTypes = listOf(InterceptV1Cfg, ClientV1Cfg)
284+
configTypes = ALL_CONFIGS
281285
}
282286

283287
internal inner class ReqInterceptor(val session: ApiSession? = null): Consumer<HttpRequest.Builder> {

0 commit comments

Comments
 (0)