tcp_proxy: fix throughput (tx_queue backpressure) + connect timeout + integration test

- Router: unified service delivery format [svc_id][src][dst][payload] (end-to-end src/dst from SVC_ROUTE header)
- tcp_proxy_client: replace single tx_buf drop-on-backpressure with bounded tx_queue + send_q waiter + retry timer (no data loss, bulk window reopen — removes 62.5ms delayed-ACK stall)
- tcp_io: configurable connect timeout (tcp_conn_set_connect_timeout)
- tcp_proxy_server: connect_timeout config option (ms, default 2000, 0=off)
- check.sh: run tcp_proxy_full integration test with sudo (skip if passwordless sudo unavailable)
- test_tcp_io: connect timeout unit test; docs (lwip_nuances.md)
This commit is contained in:
evgeny committed 2026-09-03 13:49:09 +03:00
1 parent acfaae2801
commit f47ad7d87f
36 files changed
+758 -269

No files matched your search

+5
View File
@@ -460,3 +460,8 @@ check-local: $(check_PROGRAMS)
echo "Run 'cat $(TEST_LOG_DIR)/<test>.log' to see details"; \
exit 1; \
fi
# Интеграционный тест tcp_proxy_full (требует sudo, TUN, iptables). Запускается из check.sh.
.PHONY: check-proxy
check-proxy:
@cd $(srcdir)/tcp_proxy_full && bash run_test.sh
+6 -4
View File
@@ -1,5 +1,5 @@
[global]
my_node_id=0xAAAA000000000002
my_node_id=0x5a77374f79310824
my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f
my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01
tun_ip=10.200.20.1/24
@@ -12,14 +12,14 @@ type=public
[client: c1]
keepalive=1
link=s1:127.0.0.1:15001
peer_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a
link=s1:127.0.0.1:15003
peer_public_key=23ea42d345a0efbcfaab4f47c27bf6da05e3682e27a667ba60a16645ce2c9024
[tcp_proxy_client]
enabled=yes
tun_name=tun_test_proxy
tun_ip=10.200.30.1
via_node=0xAAAA000000000001
via_node=0x4239ac368c0df079
[debug]
connection=info
@@ -27,3 +27,5 @@ socket=info
general=info
traffic=info
tun=info
proxy=debug
etcp_route=debug
+3 -1
View File
@@ -1,5 +1,5 @@
[global]
my_node_id=0xAAAA000000000001
my_node_id=0x4239ac368c0df079
my_private_key=38240cb82199e504686507f11f6eaa4f740fde6f0c425c495e49a523019a5d68
my_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a
tun_ip=10.200.10.1/24
@@ -20,3 +20,5 @@ enabled=yes
connection=info
socket=info
traffic=info
proxy=debug
etcp_route=debug
+28
View File
@@ -0,0 +1,28 @@
[global]
my_node_id=0x628471cde1291456
my_private_key=2012fcb6f33004ee64c899a229c54619ed89f83d8fe7e003c4d2eca43c453b58
my_public_key=23ea42d345a0efbcfaab4f47c27bf6da05e3682e27a667ba60a16645ce2c9024
tun_ip=10.200.40.1/24
tun_ifname=tun_test_inter
debug_level=error
[server: s1]
addr=127.0.0.1:15003
type=public
[allowed_keys]
allow_all=1
[client: c1]
keepalive=1
link=s1:127.0.0.1:15001
peer_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a
[debug]
connection=info
socket=info
general=info
traffic=info
etcp_route=debug
proxy=debug
+22 -10
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# run_test.sh — интеграционный тест tcp_proxy (transparent proxy)
#
# Трафик:
# Топология (3 узла, трафик идёт через промежуточный узел транзитом):
# tcp_client.py (SO_MARK=1, dest=10.200.100.N) → table 100 → tun_test_proxy
# → tcp_proxy(lwIP) → ETCP → exit(remote_proxy)
# → tcp_proxy(lwIP, клиент) → ETCP → intermediate (транзит) → ETCP → exit
# → connect(10.200.100.N) → iptables DNAT (!mark=1) → 127.0.0.1 → echo → обратно
#
# Запуск: sudo ./run_test.sh
@@ -41,6 +41,9 @@ setup_net() {
iptables -t nat -C OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT" 2>/dev/null \
|| iptables -t nat -A OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT"
done
# conn_refused: .99 → порт без слушателя, exit получает ECONNREFUSED (иначе SYN уходит в tun_test_proxy и зацикливается)
iptables -t nat -C OUTPUT -d 10.200.100.99 -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$REFUSED_PORT" 2>/dev/null \
|| iptables -t nat -A OUTPUT -d 10.200.100.99 -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$REFUSED_PORT"
echo " Done"
}
@@ -63,6 +66,7 @@ cleanup_net() {
for i in $(seq 1 $N_IP); do
iptables -t nat -D OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT" 2>/dev/null || true
done
iptables -t nat -D OUTPUT -d 10.200.100.99 -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$REFUSED_PORT" 2>/dev/null || true
ip rule del fwmark 1 priority 199 table 100 2>/dev/null || true
ip route del 10.200.100.0/24 dev tun_test_proxy table 100 2>/dev/null || true
ip route del 10.200.100.0/24 dev tun_test_proxy 2>/dev/null || true
@@ -75,18 +79,22 @@ cleanup_net() {
cleanup() {
echo ""; echo "=== Cleanup ==="
kill $EXIT_PID 2>/dev/null || true
kill $INTER_PID 2>/dev/null || true
kill $CLIENT_PID 2>/dev/null || true
kill $ECHO_PID 2>/dev/null || true
wait $EXIT_PID 2>/dev/null || true
wait $INTER_PID 2>/dev/null || true
wait $CLIENT_PID 2>/dev/null || true
wait $ECHO_PID 2>/dev/null || true
sleep 0.5; cleanup_net
}
wait_for_etcp() {
for i in $(seq 1 30); do
grep -q "Connection established\|initialized and marked as UP (client)" "$LOG_DIR/exit_utun.log" 2>/dev/null && return 0
grep -q "initialized and marked as UP (client)" "$LOG_DIR/client_utun.log" 2>/dev/null && return 0
for i in $(seq 1 40); do
grep -q "Connection UP" "$LOG_DIR/exit_utun.log" 2>/dev/null \
&& grep -q "Connection UP" "$LOG_DIR/intermediate_utun.log" 2>/dev/null \
&& grep -q "Connection UP" "$LOG_DIR/client_utun.log" 2>/dev/null \
&& return 0
sleep 0.5
done
return 1
@@ -202,6 +210,10 @@ echo "Starting utun exit ..."
"$UTUN_BIN" -c "$SCRIPT_DIR/exit.conf" -f -l "$LOG_DIR/exit_utun.log" >"$LOG_DIR/exit_stdout.log" 2>&1 &
EXIT_PID=$!
echo "Starting utun intermediate ..."
"$UTUN_BIN" -c "$SCRIPT_DIR/intermediate.conf" -f -l "$LOG_DIR/intermediate_utun.log" >"$LOG_DIR/intermediate_stdout.log" 2>&1 &
INTER_PID=$!
echo "Starting utun client ..."
"$UTUN_BIN" -c "$SCRIPT_DIR/client.conf" -f -l "$LOG_DIR/client_utun.log" >"$LOG_DIR/client_stdout.log" 2>&1 &
CLIENT_PID=$!
@@ -216,15 +228,15 @@ echo "ETCP ready"; sleep 1
echo ""; echo "=== Running tests ==="
# 1: stress first (catch resource leaks)
run_stress && ((++PASS)) || ((++FAIL))
sleep 2
# 2: basic_1mb
# 1: basic_1mb СНАЧАЛА (изолируем от стресса, чтобы понять — падает ли 1MB сам по себе)
run_test basic_1mb --host 10.200.100.1 --port "$ECHO_PORT" --size 1048576 --verify --timeout 10 \
&& ((++PASS)) || ((++FAIL))
sleep 1
# 2: stress (catch resource leaks)
run_stress && ((++PASS)) || ((++FAIL))
sleep 2
# 3: concurrent_5
run_concurrent concurrent_5 5 204800 && ((++PASS)) || ((++FAIL))
sleep 1
+9 -9
View File
@@ -248,23 +248,23 @@ static int send_one_pkt(uint32_t seq, int data_len) {
// ======================== Server handler ========================
static void srv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
// Restart notification: conn=NULL, len=9, format [svc_id:1][node_id:8]
if (!conn && entry && entry->dgram && entry->len == 9) {
// Restart/close notification: [svc_id][src][dst] без payload
if (entry && entry->dgram && entry->len == ROUTER_SVC_HDR_SIZE) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "srv_handler: restart notification, resetting expected_seq from %u to 0", g_expected_seq);
g_expected_seq = 0;
queue_dgram_free(entry); queue_entry_free(entry);
return;
}
if (!entry || !entry->dgram || entry->len < 10) {
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 9) {
printf("[FAIL] srv_handler: bad entry len=%zu conn=%p\n", entry ? entry->len : 0, (void*)conn);
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
g_fail = 4; g_done = -1;
return;
}
uint8_t subcmd = entry->dgram[1];
uint32_t seq = 0; memcpy(&seq, entry->dgram + 2, 4);
uint32_t data_len = 0; memcpy(&data_len, entry->dgram + 6, 4);
uint8_t* payload = entry->dgram + 10;
uint8_t subcmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF];
uint32_t seq = 0; memcpy(&seq, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, 4);
uint32_t data_len = 0; memcpy(&data_len, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 5, 4);
uint8_t* payload = entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 9;
if (subcmd != 0x01) {
printf("[FAIL] srv_handler: unexpected subcmd=%u seq=%u\n", subcmd, seq);
@@ -325,9 +325,9 @@ static void srv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
static int loop_rcvd = 0, loop_ok = 0;
static void loop_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void)conn;
if (entry && entry->dgram && entry->len >= 6) {
if (entry && entry->dgram && entry->len >= ROUTER_SVC_HDR_SIZE + 3) {
loop_rcvd++;
if (entry->dgram[1] == 0xAA && entry->dgram[2] == 0xBB && entry->dgram[3] == 0xCC) loop_ok = 1;
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == 0xAA && entry->dgram[ROUTER_SVC_PAYLOAD_OFF + 1] == 0xBB && entry->dgram[ROUTER_SVC_PAYLOAD_OFF + 2] == 0xCC) loop_ok = 1;
}
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
}
+6 -6
View File
@@ -128,14 +128,14 @@ static void cleanup(void) { test_unlink(a_conf); test_unlink(b_conf); test_unlin
static void recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !entry->dgram) { fprintf(stderr, "RECV: null entry\n"); g_test_ok=-1; g_fail_code=10; return; }
if (entry->len == 9) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (entry->len < 7) { fprintf(stderr, "RECV: short entry len=%u\n", (unsigned)entry->len); g_test_ok=-1; g_fail_code=10; queue_dgram_free(entry); queue_entry_free(entry); return; }
uint32_t seq=0; memcpy(&seq,entry->dgram+1,4);
uint16_t sz=0; memcpy(&sz, entry->dgram+5,2);
if (entry->len != (size_t)(7+sz)) { fprintf(stderr, "FAIL: size mismatch seq=%u\n", seq); g_test_ok=-1; g_fail_code=11; queue_dgram_free(entry); queue_entry_free(entry); return; }
if (entry->len == ROUTER_SVC_HDR_SIZE) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (entry->len < ROUTER_SVC_PAYLOAD_OFF + 6) { fprintf(stderr, "RECV: short entry len=%u\n", (unsigned)entry->len); g_test_ok=-1; g_fail_code=10; queue_dgram_free(entry); queue_entry_free(entry); return; }
uint32_t seq=0; memcpy(&seq,entry->dgram+ROUTER_SVC_PAYLOAD_OFF,4);
uint16_t sz=0; memcpy(&sz, entry->dgram+ROUTER_SVC_PAYLOAD_OFF+4,2);
if (entry->len != (size_t)(ROUTER_SVC_PAYLOAD_OFF+6+sz)) { fprintf(stderr, "FAIL: size mismatch seq=%u\n", seq); g_test_ok=-1; g_fail_code=11; queue_dgram_free(entry); queue_entry_free(entry); return; }
if (seq != g_expected_seq) { fprintf(stderr, "FAIL: seq broken seq=%u expected=%u\n", seq, g_expected_seq); g_test_ok=-1; g_fail_code=12; queue_dgram_free(entry); queue_entry_free(entry); return; }
uint8_t exp[MAX_PAYLOAD]; gen_payload(seq, exp, sz);
if (memcmp(entry->dgram+7, exp, sz) != 0) { fprintf(stderr, "FAIL: corrupt seq=%u\n", seq); g_test_ok=-1; g_fail_code=13; queue_dgram_free(entry); queue_entry_free(entry); return; }
if (memcmp(entry->dgram+ROUTER_SVC_PAYLOAD_OFF+6, exp, sz) != 0) { fprintf(stderr, "FAIL: corrupt seq=%u\n", seq); g_test_ok=-1; g_fail_code=13; queue_dgram_free(entry); queue_entry_free(entry); return; }
g_expected_seq++; g_rcvd_total++;
queue_dgram_free(entry); queue_entry_free(entry);
}
+4 -4
View File
@@ -38,18 +38,18 @@ static int g_notify_count = 0;
static void test_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void)conn;
if (!entry || !entry->dgram || entry->len < 2) {
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_HDR_SIZE) {
g_notify_count++;
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
// close/restart: len==9 (svc_id + node_id), conn==NULL
if (entry->len == 9) {
// close/restart: [svc_id][src][dst] без payload
if (entry->len == ROUTER_SVC_HDR_SIZE) {
g_notify_count++;
queue_dgram_free(entry); queue_entry_free(entry);
return;
}
if (entry->dgram[1] != rx.marker) { rx.errors++; } else { rx.delivered++; rx.last_seq = rx.expected_seq; rx.expected_seq++; }
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] != rx.marker) { rx.errors++; } else { rx.delivered++; rx.last_seq = rx.expected_seq; rx.expected_seq++; }
queue_dgram_free(entry); queue_entry_free(entry);
}
+6 -6
View File
@@ -82,16 +82,16 @@ static void* g_to_id = NULL;
static void cli_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void)conn;
if (!entry || !entry->dgram || entry->len < ICMP_PROXY_HDR_SIZE + 1) {
if (!entry || !entry->dgram || entry->len < ICMP_PROXY_RECV_HDR_SIZE + 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return;
}
if (entry->dgram[1] == ICMP_PROXY_SUBCMD_REPLY) {
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == ICMP_PROXY_SUBCMD_REPLY) {
uint16_t rid, rseq;
memcpy(&rid, entry->dgram + 18, 2);
memcpy(&rseq, entry->dgram + 20, 2);
memcpy(&rid, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 9, 2);
memcpy(&rseq, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 11, 2);
if (rid == test_echo_id && rseq == test_echo_seq) {
size_t payload_len = entry->len - ICMP_PROXY_HDR_SIZE;
uint8_t* payload = entry->dgram + ICMP_PROXY_HDR_SIZE;
size_t payload_len = entry->len - ICMP_PROXY_RECV_HDR_SIZE;
uint8_t* payload = entry->dgram + ICMP_PROXY_RECV_HDR_SIZE;
if (payload_len == ICMP_PAYLOAD_SIZE && memcmp(payload, send_buf, ICMP_PAYLOAD_SIZE) == 0) {
reply_rcvd = 1; g_done = 1; g_ok = 1;
} else {
+4 -6
View File
@@ -35,7 +35,7 @@
#define TEST_TIMEOUT_MS 15000
static uint64_t g_provider_node_id = 0;
static uint64_t g_client_node_id = 0;
#define NAT_SVC_HDR_SIZE 9 // svc_id(1) + src_node_id(8)
#define NAT_SVC_HDR_SIZE 1 // send: svc_id(1) + ip_data (src/dst добавляет роутер)
static struct UTUN_INSTANCE* inst_provider = NULL;
static struct UTUN_INSTANCE* inst_client = NULL;
@@ -293,14 +293,13 @@ static int test_provider_egress(void) {
}
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Client conn peer_node_id=0x%016llx", (unsigned long long)client_conn->peer_node_id);
// Build ETCP_RT_ID_NAT packet via etcp_route_send (new format: svc_id + src_node_id + ip_data)
// Build ETCP_RT_ID_NAT packet via etcp_route_send (src/dst node_id добавляет роутер)
size_t ip_len;
uint8_t* raw_ip = build_udp_pkt(0x0A0000FE, 40000, 0x08080808, 53, &ip_len);
size_t total = NAT_SVC_HDR_SIZE + ip_len;
uint8_t* dgram = u_malloc(total);
dgram[0] = ETCP_RT_ID_NAT;
memcpy(dgram + 1, &inst_client->nat_tr.self_node_id, 8);
memcpy(dgram + 9, raw_ip, ip_len);
memcpy(dgram + 1, raw_ip, ip_len);
free(raw_ip);
struct ll_entry* entry = queue_entry_new(0);
@@ -469,8 +468,7 @@ static int test_full_roundtrip(void) {
size_t total = NAT_SVC_HDR_SIZE + ip_len;
uint8_t* dgram = u_malloc(total);
dgram[0] = ETCP_RT_ID_NAT;
memcpy(dgram + 1, &inst_client->nat_tr.self_node_id, 8);
memcpy(dgram + 9, raw_ip, ip_len);
memcpy(dgram + 1, raw_ip, ip_len);
free(raw_ip);
struct ll_entry* entry = queue_entry_new(0);
+47
View File
@@ -307,6 +307,52 @@ static void test_error_callback(void) {
TEST_PASS();
}
static void test_connect_timeout(void) {
TEST_START("Connect timeout on black hole (200ms)");
#ifdef _WIN32
TEST_PASS();
return;
#else
uasync_t* ua = uasync_create();
ASSERT_TRUE(ua != NULL, "uasync_create failed");
reset_counters();
int fd = socket(AF_INET, SOCK_STREAM, 0);
ASSERT_TRUE(fd >= 0, "socket failed");
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(81);
addr.sin_addr.s_addr = htonl(0xC0000201); // 192.0.2.1 (TEST-NET-1, RFC 5737) — чёрная дыра
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS");
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(tc->connected, 0, "should not be connected");
tcp_conn_set_connect_timeout(tc, 200);
// Крутим цикл (1 мс × 500) пока таймаут не сработает
int iterations = 0;
while (g_error_count == 0 && iterations < 500) {
uasync_poll(ua, 10);
iterations++;
}
ASSERT_EQ(g_error_count, 1, "on_error not called on connect timeout");
ASSERT_EQ(g_last_error, ETIMEDOUT, "on_error should report ETIMEDOUT");
ASSERT_EQ(tc->connected, 0, "connection should still be not connected");
ASSERT_EQ(tc->error, 1, "tc->error should be set");
tcp_conn_destroy(tc);
uasync_destroy(ua, 0);
TEST_PASS();
#endif
}
// ====================================================================
// FIN / Close через очередь — новые тесты
// ====================================================================
@@ -552,6 +598,7 @@ int main(void) {
test_high_water_pause();
test_connect_detection();
test_error_callback();
test_connect_timeout();
test_push_fin();
test_push_close();
test_fin_data_ordering();
+4 -5
View File
@@ -92,13 +92,12 @@ static void udp_echo_cb(socket_t sock, void* arg) {
static void cli_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
(void)conn;
if (!entry || !entry->dgram || entry->len < UDP_PROXY_HDR_SIZE + 1) {
if (!entry || !entry->dgram || entry->len < UDP_PROXY_RECV_HDR_SIZE + 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return;
}
// svc_id(1) + subcmd(1) + sender_node_id(8) + src_ip(4) + src_port(2) + dst_ip(4) + dst_port(2) + payload
size_t payload_len = entry->len - UDP_PROXY_HDR_SIZE;
if (entry->dgram[1] == UDP_PROXY_SUBCMD_DATA) {
uint8_t* payload = entry->dgram + UDP_PROXY_HDR_SIZE;
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == UDP_PROXY_SUBCMD_DATA) {
uint8_t* payload = entry->dgram + UDP_PROXY_RECV_HDR_SIZE;
if (payload_len == PAYLOAD_SIZE && memcmp(payload, send_buf, PAYLOAD_SIZE) == 0) {
reply_rcvd = 1; g_done = 1; g_ok = 1;
} else {