diff --git a/lib/tcp_io.c b/lib/tcp_io.c index 6aea666d..2ea83f23 100644 --- a/lib/tcp_io.c +++ b/lib/tcp_io.c @@ -118,7 +118,12 @@ void tcp_conn_destroy(struct tcp_conn* tc) { DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_pending=%d fin_local=%d closed=%d", (int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_pending, tc->fin_local, tc->closed); - if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; } + if (tc->connect_timer) { + if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK) + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: destroy could not cancel connect timer fd=%d timer=%p", + (int)tc->sock, tc->connect_timer); + tc->connect_timer = NULL; + } if (tc->socket_id) { uasync_remove_socket_t(tc->ua, tc->sock); @@ -149,7 +154,12 @@ void tcp_conn_destroy(struct tcp_conn* tc) { static void tcp_conn_handle_error(struct tcp_conn* tc, int err) { tc->error = 1; - if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; } + if (tc->connect_timer) { + if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK) + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: error cleanup could not cancel connect timer fd=%d timer=%p err=%d", + (int)tc->sock, tc->connect_timer, err); + tc->connect_timer = NULL; + } queue_waiter_cancel(tc->read_queue, &tc->read_waiter); queue_set_empty_callback(tc->read_queue, NULL, NULL); @@ -398,7 +408,12 @@ static void write_cb(socket_t sock, void* arg) { socklen_t len = sizeof(err); if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&err, &len) == 0 && err == 0) { tc->connected = 1; - if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; } + if (tc->connect_timer) { + if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK) + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connected socket could not cancel connect timer fd=%d timer=%p", + (int)tc->sock, tc->connect_timer); + tc->connect_timer = NULL; + } DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock); if (tc->on_connected) tc->on_connected(tc, tc->arg); if (tc->destroyed) return; diff --git a/src/proxy/tcp_proxy_server.c b/src/proxy/tcp_proxy_server.c index eba13533..47e69d1c 100644 --- a/src/proxy/tcp_proxy_server.c +++ b/src/proxy/tcp_proxy_server.c @@ -329,7 +329,7 @@ int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry* } rc->next = ctx->conns; ctx->conns = rc; - if (ret == 0) { rc->tc->connected = 1; on_connected_cb(rc->tc, rc); } + if (ret == 0) rc->tc->connected = 1; int timeout_rc = tcp_conn_set_connect_timeout(rc->tc, g->tcp_proxy_server_connect_timeout_ms); if (timeout_rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: cannot set connect timeout sid=%08x timeout=%d rc=%d", @@ -339,6 +339,8 @@ int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry* return -1; } rc->diag_timer = uasync_set_timeout(rc->ua, 10000, rc, diag_timer_cb, "tps_diag"); + /* CONNECTED может синхронно закрыть exit через loopback; все ресурсы уже должны принадлежать rc. */ + if (ret == 0) on_connected_cb(rc->tc, rc); queue_dgram_free(entry); queue_entry_free(entry); return 0; diff --git a/tests/test_tcp_io.c b/tests/test_tcp_io.c index 12596a93..e6ea34d1 100644 --- a/tests/test_tcp_io.c +++ b/tests/test_tcp_io.c @@ -57,6 +57,8 @@ static int tests_failed = 0; static int g_fin_count = 0; static int g_error_count = 0; static int g_last_error = 0; +static int g_error_had_timer = 0; +static int g_destroy_on_error = 0; static struct tcp_conn* g_last_fin_tc = NULL; static int g_fin_sent_count = 0; static int g_closed_count = 0; @@ -72,6 +74,8 @@ static void on_error(struct tcp_conn* tc, int err, void* arg) { (void)arg; g_error_count++; g_last_error = err; + g_error_had_timer = tc->connect_timer != NULL; + if (g_destroy_on_error) tcp_conn_destroy(tc); } static void on_fin_sent_cb(struct tcp_conn* tc, void* arg) { @@ -88,6 +92,8 @@ static void reset_counters(void) { g_fin_count = 0; g_error_count = 0; g_last_error = 0; + g_error_had_timer = 0; + g_destroy_on_error = 0; g_last_fin_tc = NULL; g_fin_sent_count = 0; g_closed_count = 0; @@ -513,6 +519,42 @@ static void test_connect_timer_destroy(void) { TEST_PASS(); } +/* Порт зарезервирован, но не слушает: реальный ECONNREFUSED до истечения connect-таймера. */ +static void test_connect_error_cancels_timer(void) { + TEST_START("Connect error cancels timer before owner destroys connection"); + size_t allocated = u_get_allocated_count(); + uasync_t* ua = uasync_create(); + ASSERT_TRUE(ua != NULL, "uasync_create failed"); + reset_counters(); + int reserved = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(reserved >= 0, "reserved socket failed"); + struct sockaddr_in addr = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}; + ASSERT_EQ(bind(reserved, (struct sockaddr*)&addr, sizeof(addr)), 0, "reserve port failed"); + socklen_t len = sizeof(addr); + ASSERT_EQ(getsockname(reserved, (struct sockaddr*)&addr, &len), 0, "getsockname failed"); + int fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(fd >= 0, "socket failed"); + ASSERT_EQ(socket_set_nonblocking(fd), 0, "nonblocking failed"); + struct tcp_conn* tc = tcp_conn_create(ua, fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); + ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); + ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 60000), 0, "arm connect timer"); + g_destroy_on_error = 1; + int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); + ASSERT_TRUE(ret < 0 && (errno == EINPROGRESS || errno == ECONNREFUSED), "expected refused connect"); + uint64_t deadline = get_time_tb() + 5000; + while (!g_error_count && get_time_tb() < deadline) uasync_poll(ua, 10); + ASSERT_EQ(g_error_count, 1, "connect error did not call on_error"); + ASSERT_EQ(g_last_error, ECONNREFUSED, "wrong connect error"); + ASSERT_EQ(g_error_had_timer, 0, "connect timer still owned when on_error ran"); + ASSERT_EQ(active_timers(ua), 0, "connect error left active timers"); + close(reserved); + uasync_drain_immediate(ua); + ASSERT_EQ(ua->timer_alloc_count, ua->timer_free_count, "timer allocations leaked"); + uasync_destroy(ua, 0); + ASSERT_EQ(u_get_allocated_count(), allocated, "on_error destroy leaked memory"); + TEST_PASS(); +} + // ==================================================================== // FIN / Close через очередь — новые тесты // ==================================================================== @@ -814,6 +856,7 @@ int main(void) { test_error_callback(); test_connect_timeout(); test_connect_timer_destroy(); + test_connect_error_cancels_timer(); test_push_fin(); test_push_close(); test_fin_data_ordering();