commit 92c2285c3ab614414b1a14db99e83fbc4fb571f7
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-05-26T20:09:08Z |
| subject | idle timeout for tcp_route |
commit 92c2285c3ab614414b1a14db99e83fbc4fb571f7
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-05-26T20:09:08Z
idle timeout for tcp_route
---
TODO.md | 3 -
tcp_route.c | 20 +++++-
tests/run_tests.py | 2 +
tests/test_tcp_idle_timeout.py | 152 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 173 insertions(+), 4 deletions(-)
diff --git a/TODO.md b/TODO.md
index 2e256a1..954d35e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,6 +3,3 @@
## Forwarding
* IPv6 <-> IPv4
-
-## options
-* udp timeout
diff --git a/tcp_route.c b/tcp_route.c
index c84184d..62d8d5d 100644
--- a/tcp_route.c
+++ b/tcp_route.c
@@ -85,7 +85,13 @@ static void event_cb(struct bufferevent *bev, short events, void *arg) {
return;
}
- if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR | BEV_EVENT_TIMEOUT)) {
+ if (events & BEV_EVENT_TIMEOUT) {
+ LOG_WARN("connection timed out");
+ free_conn(conn);
+ return;
+ }
+
+ if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
LOG_ERROR("connection error", "err", _LOGV(evutil_socket_error_to_string(err)));
@@ -97,6 +103,17 @@ 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) {
@@ -155,6 +172,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);
if (bufferevent_socket_connect(
conn->upstream,
(struct sockaddr *)&upstream_addr,
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 8d3a523..3ee68b7 100644
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -11,6 +11,7 @@ from .support import (
run_default_udp_tinyproxy,
)
from . import test_tcp_basic
+from . import test_tcp_idle_timeout
from . import test_tcp_stress
from . import test_udp_basic
from . import test_haproxy_proxy_v2
@@ -29,6 +30,7 @@ UDP_PROXY_TEST_MODULES = [
STANDALONE_TEST_MODULES = [
test_haproxy_proxy_v2,
test_udp_proxy_v2,
+ test_tcp_idle_timeout,
]
diff --git a/tests/test_tcp_idle_timeout.py b/tests/test_tcp_idle_timeout.py
new file mode 100644
index 0000000..fe3080f
--- /dev/null
+++ b/tests/test_tcp_idle_timeout.py
@@ -0,0 +1,152 @@
+import asyncio
+import os
+
+from .support import (
+ LISTEN_HOST,
+ BACKEND_PORT,
+ PROXY_PORT,
+ SkipTest,
+ run_tinyproxy_with_conf,
+)
+
+
+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()
+
+
+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 = 3.0) -> bytes:
+ try:
+ return await asyncio.wait_for(reader.read(1), timeout=timeout)
+ except ConnectionResetError:
+ return b""
+
+
+async def test_tcp_idle_timeout_closes_idle_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"{LISTEN_HOST}:{BACKEND_PORT} "
+ f"tcp idle_timeout=1\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"before idle timeout\n"
+
+ writer.write(payload)
+ await writer.drain()
+
+ got = await asyncio.wait_for(
+ reader.readexactly(len(payload)),
+ timeout=3.0,
+ )
+ assert got == payload, (
+ f"initial roundtrip mismatch: got={got!r} expected={payload!r}"
+ )
+
+ await asyncio.sleep(2.0)
+
+ got = await read_one_or_eof(reader, timeout=3.0)
+ assert got == b"", (
+ f"expected idle connection to close, got={got!r}"
+ )
+ finally:
+ await close_writer(writer)
+ finally:
+ backend_server.close()
+ await backend_server.wait_closed()
+
+
+async def test_tcp_idle_timeout_keeps_active_connection_open() -> 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"{LISTEN_HOST}:{BACKEND_PORT} "
+ f"tcp 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:
+ for i in range(3):
+ payload = f"still-active-{i}\n".encode()
+
+ writer.write(payload)
+ await writer.drain()
+
+ got = await asyncio.wait_for(
+ reader.readexactly(len(payload)),
+ timeout=3.0,
+ )
+ assert got == payload, (
+ f"active roundtrip {i} mismatch: "
+ f"got={got!r} expected={payload!r}"
+ )
+
+ await asyncio.sleep(1.0)
+ finally:
+ await close_writer(writer)
+ finally:
+ backend_server.close()
+ await backend_server.wait_closed()
+
+
+TESTS = [
+ ("test_tcp_idle_timeout_closes_idle_connection", test_tcp_idle_timeout_closes_idle_connection),
+ ("test_tcp_idle_timeout_keeps_active_connection_open", test_tcp_idle_timeout_keeps_active_connection_open),
+]
\ No newline at end of file