commit 122d4bcbc83561c8b4f134d47fd7b6694c8e5846
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-27T07:24:34Z |
| subject | connect timeout for tcp_route |
commit 122d4bcbc83561c8b4f134d47fd7b6694c8e5846
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-27T07:24:34Z
connect timeout for tcp_route
---
tcp_route.c | 37 ++++++---
tests/run_tests.py | 2 +
tests/test_tcp_connect_timeout.py | 171 ++++++++++++++++++++++++++++++++++++++
3 files changed, 198 insertions(+), 12 deletions(-)
diff --git a/tcp_route.c b/tcp_route.c
index a6f9088..a249930 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -103,10 +103,34 @@ static void pipe_write_cb(struct bufferevent *dst, void *arg)
}
}
+static void set_connect_timeout(conn_t *conn, const struct route *r)
+{
+ struct timeval connect_timeout = {
+ .tv_sec = r->opts.connect_timeout_sec,
+ .tv_usec = 0,
+ };
+
+ bufferevent_set_timeouts(conn->upstream, NULL, &connect_timeout);
+}
+
+static void set_idle_timeouts(conn_t *conn, const struct route *r)
+{
+ struct timeval idle_timeout = {
+ .tv_sec = r->opts.idle_timeout_sec,
+ .tv_usec = 0,
+ };
+
+ bufferevent_set_timeouts(conn->client, &idle_timeout, &idle_timeout);
+ bufferevent_set_timeouts(conn->upstream, &idle_timeout, &idle_timeout);
+}
+
+
static void event_cb(struct bufferevent *bev, short events, void *arg) {
conn_t *conn = arg;
if (events & BEV_EVENT_CONNECTED) {
+ set_idle_timeouts(conn, conn->route);
+
bufferevent_enable(conn->client, EV_READ | EV_WRITE);
bufferevent_enable(conn->upstream, EV_READ | EV_WRITE);
return;
@@ -130,17 +154,6 @@ static void event_cb(struct bufferevent *bev, short events, void *arg) {
(void)bev;
}
-static void set_idle_timeouts(conn_t *conn, const struct route *r)
-{
- struct timeval idle_timeout = {
- .tv_sec = r->opts.idle_timeout_sec,
- .tv_usec = 0,
- };
-
- bufferevent_set_timeouts(conn->client, &idle_timeout, &idle_timeout);
- bufferevent_set_timeouts(conn->upstream, &idle_timeout, &idle_timeout);
-}
-
static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac) {
conn_t *conn = calloc(1, sizeof(*conn));
if (conn == NULL) {
@@ -202,7 +215,7 @@ static void worker_adopt_client_fd(struct worker *w, struct accepted_client *ac)
*/
bufferevent_disable(conn->client, EV_READ);
- set_idle_timeouts(conn, r);
+ set_connect_timeout(conn, r);
if (bufferevent_socket_connect(
conn->upstream,
(struct sockaddr *)&upstream_addr,
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 11a738d..a48b5a3 100644
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -12,6 +12,7 @@ from .support import (
)
from . import test_tcp_basic
from . import test_tcp_idle_timeout
+from . import test_tcp_connect_timeout
from . import test_tcp_stress
from . import test_tcp_backpressure
from . import test_udp_basic
@@ -34,6 +35,7 @@ STANDALONE_TEST_MODULES = [
test_udp_proxy_v2,
test_tcp_backpressure,
test_tcp_idle_timeout,
+ test_tcp_connect_timeout,
test_udp_idle_timeout,
]
diff --git a/tests/test_tcp_connect_timeout.py b/tests/test_tcp_connect_timeout.py
new file mode 100644
index 0000000..7d754fe
--- /dev/null
+++ b/tests/test_tcp_connect_timeout.py
@@ -0,0 +1,171 @@
+import asyncio
+import os
+import time
+
+from .support import (
+ LISTEN_HOST,
+ PROXY_PORT,
+ SkipTest,
+ run_tinyproxy_with_conf,
+)
+
+
+BLACKHOLE_HOST = "192.0.2.1"
+BLACKHOLE_PORT = 65000
+
+
+async def close_writer(writer: asyncio.StreamWriter) -> None:
+ try:
+ writer.close()
+ await writer.wait_closed()
+ except ConnectionResetError:
+ pass
+
+
+async def read_one_or_eof(reader: asyncio.StreamReader, timeout: float = 5.0) -> bytes:
+ try:
+ return await asyncio.wait_for(reader.read(1), timeout=timeout)
+ except ConnectionResetError:
+ return b""
+
+
+async def test_tcp_connect_timeout_closes_client_connection() -> None:
+ proxy_bin = os.environ.get("TINYPROXY_BIN")
+ if not proxy_bin:
+ raise SkipTest("TINYPROXY_BIN is not set")
+
+ conf_text = (
+ f"{LISTEN_HOST}:{PROXY_PORT} "
+ f"{BLACKHOLE_HOST}:{BLACKHOLE_PORT} "
+ f"tcp connect_timeout=1,idle_timeout=30\n"
+ )
+
+ async with run_tinyproxy_with_conf(
+ proxy_bin=proxy_bin,
+ conf_text=conf_text,
+ listen_host=LISTEN_HOST,
+ listen_port=PROXY_PORT,
+ proto="tcp",
+ ):
+ reader, writer = await asyncio.open_connection(LISTEN_HOST, PROXY_PORT)
+
+ try:
+ start = time.monotonic()
+
+ writer.write(b"hello before upstream connect\n")
+ await writer.drain()
+
+ got = await read_one_or_eof(reader, timeout=5.0)
+ elapsed = time.monotonic() - start
+
+ assert got == b"", (
+ f"expected proxy to close client after connect timeout, got={got!r}"
+ )
+
+ assert elapsed >= 0.8, (
+ f"connect timeout fired too early: elapsed={elapsed:.3f}s"
+ )
+
+ assert elapsed < 4.0, (
+ f"connect timeout took too long: elapsed={elapsed:.3f}s"
+ )
+ finally:
+ await close_writer(writer)
+
+
+async def test_tcp_connect_timeout_does_not_replace_idle_timeout_after_connect() -> None:
+ proxy_bin = os.environ.get("TINYPROXY_BIN")
+ if not proxy_bin:
+ raise SkipTest("TINYPROXY_BIN is not set")
+
+ async def echo_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
+ try:
+ while True:
+ data = await reader.read(65536)
+ if not data:
+ break
+
+ writer.write(data)
+ await writer.drain()
+ finally:
+ writer.close()
+ await writer.wait_closed()
+
+ from .support import BACKEND_PORT
+
+ conf_text = (
+ f"{LISTEN_HOST}:{PROXY_PORT} "
+ f"{LISTEN_HOST}:{BACKEND_PORT} "
+ f"tcp connect_timeout=1,idle_timeout=2\n"
+ )
+
+ backend_server = await asyncio.start_server(
+ echo_handler,
+ LISTEN_HOST,
+ BACKEND_PORT,
+ backlog=128,
+ )
+
+ try:
+ async with run_tinyproxy_with_conf(
+ proxy_bin=proxy_bin,
+ conf_text=conf_text,
+ listen_host=LISTEN_HOST,
+ listen_port=PROXY_PORT,
+ proto="tcp",
+ ):
+ reader, writer = await asyncio.open_connection(LISTEN_HOST, PROXY_PORT)
+
+ try:
+ payload = b"connected path still works\n"
+
+ writer.write(payload)
+ await writer.drain()
+
+ got = await asyncio.wait_for(
+ reader.readexactly(len(payload)),
+ timeout=3.0,
+ )
+ assert got == payload, (
+ f"roundtrip mismatch: got={got!r} expected={payload!r}"
+ )
+
+ await asyncio.sleep(1.2)
+
+ payload = b"still alive after connect timeout duration\n"
+
+ writer.write(payload)
+ await writer.drain()
+
+ got = await asyncio.wait_for(
+ reader.readexactly(len(payload)),
+ timeout=3.0,
+ )
+ assert got == payload, (
+ f"connection died as if connect_timeout remained active: "
+ f"got={got!r} expected={payload!r}"
+ )
+
+ await asyncio.sleep(2.5)
+
+ got = await read_one_or_eof(reader, timeout=3.0)
+ assert got == b"", (
+ f"expected idle timeout to close established connection, got={got!r}"
+ )
+ finally:
+ await close_writer(writer)
+ finally:
+ backend_server.close()
+ await backend_server.wait_closed()
+
+
+TESTS = [
+ (
+ "test_tcp_connect_timeout_closes_client_connection",
+ test_tcp_connect_timeout_closes_client_connection,
+ ),
+ (
+ "test_tcp_connect_timeout_does_not_replace_idle_timeout_after_connect",
+ test_tcp_connect_timeout_does_not_replace_idle_timeout_after_connect,
+ ),
+]