Compare commits

...

5 Commits

  1. 1
      .gitignore
  2. 33
      BUGS_proxy.txt
  3. 61
      doc/proxy_protocol.md
  4. 14
      lib/tcp_io.c
  5. 1
      lib/tcp_io.h
  6. 2
      src/Makefile.am
  7. 119
      src/proxy/icmp_proxy.c
  8. 4
      src/proxy/icmp_proxy.h
  9. 157
      src/proxy/proxy_protocol.h
  10. 272
      src/proxy/socks_proxy.c
  11. 5
      src/proxy/socks_proxy.h
  12. 221
      src/proxy/tcp_proxy_client.c
  13. 2
      src/proxy/tcp_proxy_client.h
  14. 296
      src/proxy/tcp_proxy_server.c
  15. 29
      src/proxy/tcp_proxy_server.h
  16. 69
      src/proxy/udp_proxy.c
  17. 5
      src/proxy/udp_proxy.h
  18. 2
      src/utun_instance.h
  19. 9
      tests/Makefile.am
  20. 292
      tests/test_proxy_regressions.c
  21. 2
      tests/test_udp_proxy.c
  22. 7
      tools/lightsout-android/.gitignore
  23. 49
      tools/lightsout-android/app/build.gradle.kts
  24. 20
      tools/lightsout-android/app/src/main/AndroidManifest.xml
  25. 77
      tools/lightsout-android/app/src/main/java/com/utun/lightsout/LightsOutGame.kt
  26. 206
      tools/lightsout-android/app/src/main/java/com/utun/lightsout/MainActivity.kt
  27. 4
      tools/lightsout-android/app/src/main/res/values/strings.xml
  28. 4
      tools/lightsout-android/app/src/main/res/values/themes.xml
  29. 4
      tools/lightsout-android/build.gradle.kts
  30. 49
      tools/lightsout-android/build.sh
  31. 3
      tools/lightsout-android/gradle.properties
  32. BIN
      tools/lightsout-android/gradle/wrapper/gradle-wrapper.jar
  33. 7
      tools/lightsout-android/gradle/wrapper/gradle-wrapper.properties
  34. 249
      tools/lightsout-android/gradlew
  35. 18
      tools/lightsout-android/settings.gradle.kts
  36. 22
      tools/lightsout/CMakeLists.txt
  37. 8
      tools/lightsout/build.sh
  38. 141
      tools/lightsout/src/boardwidget.cpp
  39. 54
      tools/lightsout/src/boardwidget.h
  40. 21
      tools/lightsout/src/main.cpp
  41. 73
      tools/lightsout/src/mainwindow.cpp
  42. 26
      tools/lightsout/src/mainwindow.h

1
.gitignore vendored

@ -3,6 +3,7 @@
build-win/
tools/chatgui/build/
tools/chatgui/ffmpeg/ffmpeg_build/
tools/lightsout/build/
# Build outputs
*.o

33
BUGS_proxy.txt

@ -0,0 +1,33 @@
Ниже — подтверждённые по коду проблемы; отдельно указал, что воспроизведено запуском.
[P1] Exit смешивает TCP-потоки разных клиентов. Поиск соединения использует только stream_id, хотя каждый клиент начинает нумерацию заново. После подключения второго клиента с таким же ID данные первого могут уйти в соединение второго; FIN/CLOSE также закрывают чужой поток. Нужен ключ (src_node_id, stream_id) во всех обработчиках, включая проверку повторного CONNECT. src/proxy/tcp_proxy_server.c:342
[P1] Клиент не проверяет отправителя ответов. DATA/FIN/CLOSE принимаются по одному stream_id, без сравнения источника с via_node_id. Обработка CLOSE_ALL читает peer_id, но затем уничтожает вообще все клиентские соединения. Пакеты другого узла, доставленные этому сервису, могут повредить или закрыть существующие потоки. src/proxy/tcp_proxy_client.c:668
[P1, воспроизведено] UDP-ответы в TUN имеют неправильную checksum. Обнуляется только IP-заголовок; байты UDP checksum остаются заполненными аллокатором значением 0xAAAA. Проверка сформированного пакета дала сумму 0xC1E2 вместо 0xFFFF. Такие ответы будут отбрасываться принимающим стеком. Нужно рассчитывать checksum либо явно устанавливать ноль для IPv4. src/proxy/udp_proxy.c:188
[P1, воспроизведено] ICMP с нечётной длиной payload получает неверную checksum. Цикл читает последнее 16-битное слово целиком, захватывая байт за пределами выделенного пользовательского буфера. Ошибка есть и на exit, и при восстановлении ответа клиенту. Для payload длиной 2 проверочная сумма получилась FFFF, длиной 3 — FF10. Отправка (src/proxy/icmp_proxy.c:80), ответ в TUN (src/proxy/icmp_proxy.c:285)
[P1] UDP/ICMP-контексты глобальные, а ядро поддерживает несколько instances. Следующий init() перезаписывает g_udp_ctx/g_icmp_ctx; обработчики старого экземпляра используют новый контекст, а destroy(inst) освобождает его без проверки владельца. Дополнительно UDP различает запрос и ответ только по глобальному is_exit: узел, одновременно работающий клиентом и exit, трактует входящий ответ как новый исходящий запрос. src/proxy/udp_proxy.c:137, инициализация (src/proxy/udp_proxy.c:237), src/proxy/icmp_proxy.c:321
[P1] Нет сквозного ограничения потока при медленном получателе. Принятые ETCP-данные без ограничения добавляются в write_queue сокета или to_lwip. Заполненность этих очередей не останавливает доставку и подтверждение данных маршрутизатором. Медленный destination или локальный клиент приводит к росту памяти; при отказе выделения TCP-данные просто теряются. src/proxy/tcp_proxy_server.c:424, src/proxy/tcp_proxy_client.c:584
[P1] TUN-клиент может уничтожить ещё не отправленные данные при FIN. Если локальный FIN уже получен, но tx_queue остаётся заблокированной, входящий FIN от exit вызывает conn_finish() без проверки этой очереди. Она освобождается вместе с данными. Завершать поток нужно после опустошения очередей обоих направлений. src/proxy/tcp_proxy_client.c:642
[P1] Exit может отправить CLOSE раньше остатка ответа. В on_fin_cb() проверка tc->fin_local имеет приоритет над pend_r. Если клиент уже закрыл свою половину, а ответ destination остался в tx_buf из-за backpressure, сервер отправляет CLOSE и освобождает остаток. Ветка on_flushed_cb() также не проверяет ожидающие отправки данные ответа. src/proxy/tcp_proxy_server.c:108
[P1] HTTP CONNECT зависит от границ TCP-чтений. Парсер запускает туннель после первой \r\n, не дожидаясь \r\n\r\n. Если заголовки приходят следующим чтением, они пересылаются destination как содержимое туннеля — например, перед TLS ClientHello. Если вместе с заголовками уже пришли данные туннеля, они уничтожаются обнулением buf_len. src/proxy/socks_proxy.c:195
[P1] HTTP POST во время DNS может потерять весь накопленный запрос. dns_pending накапливается до 65535 байт, затем отправляется одним DATA. Exit принимает максимум 8192 байта в одном сообщении и отбрасывает превышение. При переполнении самого dns_pending очередные данные также теряются без прекращения потока. Нужны ограниченная очередь и отправка частями. src/proxy/socks_proxy.c:339, накопление (src/proxy/socks_proxy.c:430), ограничение exit (src/proxy/tcp_proxy_server.c:438)
[P2] FIN теряется в двух штатных сценариях. SOCKS/HTTP при непустой очереди ответа вызывает tcp_conn_set_flushed(tc, NULL) — продолжение, которое должно переслать FIN, отсутствует. Exit молча игнорирует FIN, пришедший до завершения TCP connect. Протоколы, ожидающие EOF перед ответом, могут зависнуть. src/proxy/socks_proxy.c:538, src/proxy/tcp_proxy_server.c:492
[P2] SOCKS5-парсер некорректно обрабатывает запросы. После greeting/CONNECT сбрасывается весь буфер, включая следующие байты; всегда выбирается метод NO AUTH, даже если клиент его не предлагал. IPv6 вместо отказа превращается в IPv4 из неправильного смещения buf + 12. Доменный CONNECT длиной менее 10 байт бесконечно ожидает продолжения. src/proxy/socks_proxy.c:132
[P2] SOCKS/HTTP сообщают об успешном соединении до подключения exit. Ответ SOCKS success или HTTP 200 формируется даже до send_connect(). Подтверждения успешного TCP connect от exit в протоколе нет. При отказе подключения клиент сначала получает успех, затем закрытие вместо корректной ошибки. src/proxy/socks_proxy.c:316
[P2] После RST TUN-соединение остаётся в списке. tcp_proxy_client_err_cb() обнуляет pcb и выставляет error, но не освобождает pc. Очистка предусмотрена в poll callback уже уничтоженного PCB, который больше не вызовется. src/proxy/tcp_proxy_client.c:446
[P2] Неправильно разбираются IP options и фрагменты в TUN. UDP/ICMP используют фиксированные смещения от 20-байтового IPv4-заголовка, игнорируя IHL и fragment offset. Пакет с options или последующий IP-фрагмент превращается в запрос с неверными портами/данными. src/proxy/tcp_proxy_client.c:158
[P2] ICMP-ответы разных клиентов могут перепутаться. Exit сохраняет исходные echo_id/echo_seq и ищет ответ только по этой паре, без уникального преобразования ID и проверки адреса отправителя. Совпадающие ping-запросы разных клиентов получают чужие ответы. src/proxy/icmp_proxy.c:56

61
doc/proxy_protocol.md

@ -0,0 +1,61 @@
# Proxy: потоки TCP и датаграммы
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
## TCP
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
- DATA (3): от 1 до 4096 байт.
- CLOSE (4), ERROR (5): прекращение потока.
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
не является подтверждением потребления данных конечным TCP.
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
или backpressure роутера. Закрытие одного направления не прекращает другое.
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
накопленные HTTP headers/body передаются частями размером DATA.
## UDP и ICMP
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
## Проверки и ограничения
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
Windows/FreeBSD в этом прогоне не проверялись.
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.

14
lib/tcp_io.c

@ -282,6 +282,8 @@ static int flush_write_buf(struct tcp_conn* tc) {
return 0;
}
// Последний send может опустошить очередь при уже выключенном POLLOUT.
// Уведомляем потребителя здесь, не рассчитывая на ещё один write event.
static void notify_flushed(struct tcp_conn* tc) {
if (tc->destroyed || tc->write_buf || tc->write_queue->head || !tc->on_flushed) return;
void (*cb)(struct tcp_conn*, void*) = tc->on_flushed;
@ -316,7 +318,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
if (!e->dgram) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: CLOSE fd=%d", (int)tc->sock);
queue_entry_free(e); queue_resume_callback(q);
uasync_remove_socket_t(tc->ua, tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
socket_close_wrapper(tc->sock);
tc->socket_id = NULL; tc->sock = SOCKET_INVALID; tc->closed = 1;
if (tc->on_closed) tc->on_closed(tc, tc->arg);
@ -394,6 +396,8 @@ static void write_cb(socket_t sock, void* arg) {
tc->connected = 1;
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, 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;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect fail fd=%d err=%d", (int)tc->sock, err);
tcp_conn_handle_error(tc, err ? err : -1);
@ -426,6 +430,14 @@ static void error_cb(socket_t sock, void* arg) {
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
if (tc->fin_remote && tc->fin_local) {
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
tc->socket_id = NULL; tc->write_monitor = 0;
return;
}
if (tc->fin_remote) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
(int)tc->sock);

1
lib/tcp_io.h

@ -70,6 +70,7 @@ struct tcp_conn {
struct queue_waiter_handle read_waiter;
// Коллбэки
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)

2
src/Makefile.am

@ -67,6 +67,7 @@ utun_CORE_SOURCES = \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \
@ -171,6 +172,7 @@ libutun_a_SOURCES = \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \

119
src/proxy/icmp_proxy.c

@ -13,6 +13,7 @@
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <openssl/rand.h>
#ifndef _WIN32
#include <unistd.h>
#include <netinet/in.h>
@ -48,11 +49,11 @@ struct ip {
#define ICMP_DEF_TTL 64
#define ICMP_TIMEOUT_TB 50000 // 5s for echo reply
struct icmp_proxy_ctx* g_icmp_ctx = NULL;
static void req_expire_timer_cb(void* arg);
static void req_expire(struct icmp_proxy_ctx* ctx);
// Сумма по сетевому порядку; нечётный хвост дополняется нулём без чтения за границей.
static uint16_t icmp_checksum(const uint8_t* data, size_t len) {
uint32_t sum = 0;
while (len >= 2) { sum += ((uint16_t)data[0] << 8) | data[1]; data += 2; len -= 2; }
@ -61,9 +62,9 @@ static uint16_t icmp_checksum(const uint8_t* data, size_t len) {
return htons((uint16_t)~sum);
}
static struct icmp_request* req_find_by_id(struct icmp_request* head, uint16_t echo_id, uint16_t echo_seq) {
static struct icmp_request* req_find_by_id(struct icmp_request* head, uint16_t echo_id, uint16_t echo_seq, uint32_t src_ip) {
struct icmp_request* r;
for (r = head; r; r = r->next) if (r->echo_id == echo_id && r->echo_seq == echo_seq) return r;
for (r = head; r; r = r->next) if (r->wire_id == echo_id && r->wire_seq == echo_seq && r->dst_ip == src_ip) return r;
return NULL;
}
@ -71,17 +72,25 @@ static struct icmp_request* req_find_by_id(struct icmp_request* head, uint16_t e
static int exit_send_echo(struct UTUN_INSTANCE* inst, uint64_t client_node_id,
uint32_t dst_ip, uint32_t orig_src_ip, uint16_t echo_id, uint16_t echo_seq,
const uint8_t* payload, size_t payload_len) {
if (!g_icmp_ctx || g_icmp_ctx->raw_sock == SOCKET_INVALID) return -1;
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
if (!ctx || ctx->raw_sock == SOCKET_INVALID) return -1;
if (payload_len > 65507) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: payload too large len=%zu", payload_len); return -1; }
size_t icmp_len = ICMP_MINLEN + payload_len;
uint8_t* buf = u_malloc(icmp_len);
if (!buf) return -1;
struct icmp_request* r = u_calloc(1, sizeof(*r));
if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: request allocation failed"); u_free(buf); return -1; }
do {
uint32_t token = ++ctx->next_token;
r->wire_id = htons((uint16_t)(token >> 16)); r->wire_seq = htons((uint16_t)token);
} while (req_find_by_id(ctx->pending, r->wire_id, r->wire_seq, dst_ip));
struct icmp* icmp_hdr = (struct icmp*)buf;
memset(icmp_hdr, 0, icmp_len);
icmp_hdr->icmp_type = ICMP_ECHO;
icmp_hdr->icmp_code = 0;
icmp_hdr->icmp_id = echo_id;
icmp_hdr->icmp_seq = echo_seq;
icmp_hdr->icmp_id = r->wire_id;
icmp_hdr->icmp_seq = r->wire_seq;
if (payload_len > 0) memcpy(icmp_hdr->icmp_data, payload, payload_len);
icmp_hdr->icmp_cksum = 0;
icmp_hdr->icmp_cksum = icmp_checksum((const uint8_t*)icmp_hdr, icmp_len);
@ -89,66 +98,68 @@ static int exit_send_echo(struct UTUN_INSTANCE* inst, uint64_t client_node_id,
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto dst=0x%08x id=0x%04x seq=%u len=%zu",
dst_ip, echo_id, echo_seq, icmp_len);
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_addr = {.s_addr = dst_ip}};
ssize_t n = sendto(g_icmp_ctx->raw_sock, buf, icmp_len, 0, (struct sockaddr*)&addr, sizeof(addr));
ssize_t n = sendto(ctx->raw_sock, buf, icmp_len, 0, (struct sockaddr*)&addr, sizeof(addr));
u_free(buf);
if (n < 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto failed: %s", strerror(errno)); return -1; }
if (n < 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto failed: %s", strerror(errno)); u_free(r); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: sendto sent %zd bytes", n);
struct icmp_request* r = u_calloc(1, sizeof(struct icmp_request));
if (!r) return 0;
r->client_node_id = client_node_id; r->dst_ip = dst_ip; r->orig_src_ip = orig_src_ip;
r->echo_id = echo_id; r->echo_seq = echo_seq;
r->payload_len = payload_len > sizeof(r->payload) ? sizeof(r->payload) : payload_len;
if (payload_len > 0) memcpy(r->payload, payload, r->payload_len);
r->sent_tb = get_time_tb();
r->next = g_icmp_ctx->pending; g_icmp_ctx->pending = r;
if (!g_icmp_ctx->expire_timer)
g_icmp_ctx->expire_timer = uasync_set_timeout(g_icmp_ctx->ua, g_icmp_ctx->request_timeout_tb, g_icmp_ctx, req_expire_timer_cb, "icmp_expire");
r->next = ctx->pending; ctx->pending = r;
if (!ctx->expire_timer)
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->request_timeout_tb, ctx, req_expire_timer_cb, "icmp_expire");
return 0;
}
// Чтение echo ответа из raw сокета, сопоставление по echo_id
static void raw_read_cb(socket_t sock, void* arg) {
(void)sock; (void)arg;
if (!g_icmp_ctx || g_icmp_ctx->raw_sock == SOCKET_INVALID) return;
(void)sock;
struct icmp_proxy_ctx* ctx = arg;
if (!ctx || ctx->raw_sock == SOCKET_INVALID) return;
uint8_t buf[65536];
struct sockaddr_in from; socklen_t flen = sizeof(from);
ssize_t n = recvfrom(g_icmp_ctx->raw_sock, buf, sizeof(buf), 0, (struct sockaddr*)&from, &flen);
ssize_t n = recvfrom(ctx->raw_sock, buf, sizeof(buf), 0, (struct sockaddr*)&from, &flen);
if (n <= 0) { if (n < 0) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: recvfrom error: %s", strerror(errno)); return; }
if (n < (ssize_t)(sizeof(struct ip) + ICMP_MINLEN)) return;
struct ip* ip_hdr = (struct ip*)buf;
if (ip_hdr->ip_p != IPPROTO_ICMP) return;
size_t ip_hdr_len = ip_hdr->ip_hl * 4;
if (n < (ssize_t)(ip_hdr_len + ICMP_MINLEN)) return;
if (ip_hdr_len < 20 || n < (ssize_t)(ip_hdr_len + ICMP_MINLEN)) return;
struct icmp* icmp_hdr = (struct icmp*)(buf + ip_hdr_len);
if (icmp_hdr->icmp_type != ICMP_ECHOREPLY) return;
if (icmp_hdr->icmp_type != ICMP_ECHOREPLY || icmp_hdr->icmp_code != 0) return;
if (icmp_checksum((const uint8_t*)icmp_hdr, n - ip_hdr_len) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: invalid reply checksum"); return;
}
struct icmp_request* r = req_find_by_id(g_icmp_ctx->pending, icmp_hdr->icmp_id, icmp_hdr->icmp_seq);
struct icmp_request* r = req_find_by_id(ctx->pending, icmp_hdr->icmp_id, icmp_hdr->icmp_seq, from.sin_addr.s_addr);
if (!r) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: unclaimed echo reply id=0x%04x seq=%u from=0x%08x",
icmp_hdr->icmp_id, icmp_hdr->icmp_seq, from.sin_addr.s_addr); return; }
{ struct icmp_request** rp = &g_icmp_ctx->pending;
{ struct icmp_request** rp = &ctx->pending;
while (*rp) { if (*rp == r) { *rp = r->next; break; } rp = &(*rp)->next; } }
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: echo reply id=0x%04x seq=%u from=0x%08x",
icmp_hdr->icmp_id, icmp_hdr->icmp_seq, from.sin_addr.s_addr);
size_t payload_len = n - ip_hdr_len - ICMP_MINLEN;
if (payload_len > 1500) payload_len = 1500;
uint8_t* payload = (payload_len > 0) ? (uint8_t*)(icmp_hdr->icmp_data) : NULL;
struct ll_entry* e = queue_entry_new(0);
if (!e) return;
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: reply entry allocation failed"); u_free(r); return; }
e->dgram = u_malloc(ICMP_PROXY_HDR_SIZE + payload_len);
if (!e->dgram) { queue_entry_free(e); return; }
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: reply allocation failed"); queue_entry_free(e); u_free(r); return; }
e->dgram[0] = ETCP_RT_ID_ICMP_PROXY;
e->dgram[1] = ICMP_PROXY_SUBCMD_REPLY;
memcpy(e->dgram + 2, &r->dst_ip, 4);
memcpy(e->dgram + 6, &r->orig_src_ip, 4);
memcpy(e->dgram + 10, &icmp_hdr->icmp_id, 2);
memcpy(e->dgram + 12, &icmp_hdr->icmp_seq, 2);
memcpy(e->dgram + 10, &r->echo_id, 2);
memcpy(e->dgram + 12, &r->echo_seq, 2);
if (payload_len > 0) memcpy(e->dgram + ICMP_PROXY_HDR_SIZE, payload, payload_len);
e->len = ICMP_PROXY_HDR_SIZE + payload_len;
int ret = etcp_route_send(g_icmp_ctx->inst, TOPO_GROUP_UTUN, r->client_node_id, e, 0, 0);
int ret = etcp_route_send(ctx->inst, TOPO_GROUP_UTUN, r->client_node_id, e, 0, 0);
if (ret != 0) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: etcp_route_send reply failed: %d", ret);
else DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: reply forwarded to client %016llx", (unsigned long long)r->client_node_id);
u_free(r);
@ -158,8 +169,9 @@ static void raw_read_cb(socket_t sock, void* arg) {
// Exit узел: принять REQUEST, отправить echo через raw сокет
// ====================================================================
static void exit_handle_request(struct ETCP_CONN* conn, struct ll_entry* entry) {
struct UTUN_INSTANCE* inst = conn ? conn->instance : (g_icmp_ctx ? g_icmp_ctx->inst : NULL);
if (!inst || !g_icmp_ctx || entry->len < ICMP_PROXY_RECV_HDR_SIZE + 1) goto drop;
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
if (!inst || !ctx || !inst->tcp_proxy_server.enabled || entry->len < ICMP_PROXY_RECV_HDR_SIZE) goto drop;
uint64_t client_node_id; memcpy(&client_node_id, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
uint32_t dst_ip; memcpy(&dst_ip, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, 4);
@ -169,9 +181,9 @@ static void exit_handle_request(struct ETCP_CONN* conn, struct ll_entry* entry)
uint8_t* payload = entry->dgram + ICMP_PROXY_RECV_HDR_SIZE;
size_t payload_len = entry->len - ICMP_PROXY_RECV_HDR_SIZE;
if (g_icmp_ctx->raw_sock != SOCKET_INVALID) {
if (ctx->raw_sock != SOCKET_INVALID) {
exit_send_echo(inst, client_node_id, dst_ip, orig_src_ip, echo_id, echo_seq, payload, payload_len);
} else if (g_icmp_ctx->test_loopback) {
} else if (ctx->test_loopback) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: test loopback reply to 0x%08x", dst_ip);
struct ll_entry* e = queue_entry_new(0);
if (e) {
@ -200,7 +212,7 @@ drop:
// Сторона клиента: принять REPLY, доставить echo ответ в TUN
// ====================================================================
static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry->len < ICMP_PROXY_RECV_HDR_SIZE + 1) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (entry->len < ICMP_PROXY_RECV_HDR_SIZE) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint32_t src_ip;
uint32_t orig_src_ip;
uint16_t echo_id, echo_seq;
@ -210,7 +222,7 @@ static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry)
memcpy(&echo_seq, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 11, 2);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: client got reply id=0x%04x seq=%u from=0x%08x dst=0x%08x",
echo_id, echo_seq, src_ip, orig_src_ip);
struct UTUN_INSTANCE* inst = conn ? conn->instance : (g_icmp_ctx ? g_icmp_ctx->inst : NULL);
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
icmp_proxy_deliver_reply(inst, orig_src_ip, src_ip, echo_id, echo_seq,
entry->dgram + ICMP_PROXY_RECV_HDR_SIZE, entry->len - ICMP_PROXY_RECV_HDR_SIZE);
queue_dgram_free(entry); queue_entry_free(entry);
@ -220,13 +232,20 @@ static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry)
// Единый etcp_router коллбэк
// ====================================================================
void icmp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_HDR_SIZE) {
if (!entry || !entry->dgram || entry->len <= ROUTER_SVC_HDR_SIZE) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
uint8_t subcmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF];
if (subcmd == ICMP_PROXY_SUBCMD_REQUEST) { exit_handle_request(conn, entry); return; }
if (subcmd == ICMP_PROXY_SUBCMD_REPLY) { client_handle_reply(conn, entry); return; }
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
uint64_t peer; memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
if (subcmd == ICMP_PROXY_SUBCMD_REQUEST && inst && inst->tcp_proxy_server.enabled) {
exit_handle_request(conn, entry); return;
}
if (subcmd == ICMP_PROXY_SUBCMD_REPLY && inst && inst->tcp_proxy_client && peer == inst->tcp_proxy_client->via_node_id) {
client_handle_reply(conn, entry); return;
}
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: rejected subcmd=%u peer=%016llx", subcmd, (unsigned long long)peer);
queue_dgram_free(entry); queue_entry_free(entry);
}
@ -236,7 +255,7 @@ void icmp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
int icmp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id,
uint32_t dst_ip, uint32_t orig_src_ip, uint16_t echo_id, uint16_t echo_seq,
const uint8_t* payload, size_t payload_len) {
if (!inst) return -1;
if (!inst || payload_len > 65507) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "icmp_proxy: invalid send"); return -1; }
struct ll_entry* e = queue_entry_new(0);
if (!e) return -1;
e->dgram = u_malloc(ICMP_PROXY_HDR_SIZE + payload_len);
@ -301,8 +320,8 @@ int icmp_proxy_deliver_reply(struct UTUN_INSTANCE* inst,
}
void icmp_proxy_set_test_loopback(struct UTUN_INSTANCE* inst, int enabled) {
(void)inst;
if (g_icmp_ctx) g_icmp_ctx->test_loopback = enabled;
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
if (ctx) ctx->test_loopback = enabled;
}
static void req_expire_timer_cb(void* arg) {
@ -327,6 +346,7 @@ static void req_expire(struct icmp_proxy_ctx* ctx) {
// ====================================================================
int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
if (!inst) return -1;
if (inst->icmp_proxy) return 0;
struct icmp_proxy_ctx* ctx = u_calloc(1, sizeof(struct icmp_proxy_ctx));
if (!ctx) return -1;
ctx->inst = inst; ctx->ua = ua; ctx->raw_sock = SOCKET_INVALID;
@ -334,15 +354,19 @@ int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
ctx->is_exit = inst->tcp_proxy_server.enabled;
ctx->test_loopback = 0;
ctx->expire_timer = NULL;
g_icmp_ctx = ctx;
if (RAND_bytes((unsigned char*)&ctx->next_token, sizeof(ctx->next_token)) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: token initialization failed"); u_free(ctx); return -1;
}
inst->icmp_proxy = ctx;
if (ctx->is_exit) {
ctx->raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (ctx->raw_sock == SOCKET_INVALID)
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw socket(SOCK_RAW) failed: %s", strerror(errno));
else {
socket_set_nonblocking(ctx->raw_sock);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy: raw socket created fd=%d", ctx->raw_sock);
ctx->raw_read_id = uasync_add_socket_t(ua, ctx->raw_sock, raw_read_cb, NULL, NULL, "icmp_raw", NULL);
ctx->raw_read_id = uasync_add_socket_t(ua, ctx->raw_sock, raw_read_cb, NULL, NULL, "icmp_raw", ctx);
if (!ctx->raw_read_id) { socket_close_wrapper(ctx->raw_sock); ctx->raw_sock = SOCKET_INVALID; }
}
}
@ -354,15 +378,16 @@ int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
}
void icmp_proxy_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || !g_icmp_ctx) return;
struct icmp_proxy_ctx* ctx = inst ? inst->icmp_proxy : NULL;
if (!ctx) return;
etcp_router_unbind(inst, ETCP_RT_ID_ICMP_PROXY);
if (g_icmp_ctx->expire_timer) { uasync_cancel_timeout(g_icmp_ctx->ua, g_icmp_ctx->expire_timer); g_icmp_ctx->expire_timer = NULL; }
if (g_icmp_ctx->raw_sock != SOCKET_INVALID) {
if (g_icmp_ctx->raw_read_id) { uasync_remove_socket_t(g_icmp_ctx->ua, g_icmp_ctx->raw_sock); g_icmp_ctx->raw_read_id = NULL; }
socket_close_wrapper(g_icmp_ctx->raw_sock);
if (ctx->expire_timer) { uasync_cancel_timeout(ctx->ua, ctx->expire_timer); ctx->expire_timer = NULL; }
if (ctx->raw_sock != SOCKET_INVALID) {
if (ctx->raw_read_id) { uasync_remove_socket_t(ctx->ua, ctx->raw_sock); ctx->raw_read_id = NULL; }
socket_close_wrapper(ctx->raw_sock);
}
struct icmp_request* r = g_icmp_ctx->pending;
struct icmp_request* r = ctx->pending;
while (r) { struct icmp_request* n = r->next; u_free(r); r = n; }
u_free(g_icmp_ctx); g_icmp_ctx = NULL;
u_free(ctx); inst->icmp_proxy = NULL;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "icmp_proxy destroyed");
}

4
src/proxy/icmp_proxy.h

@ -35,6 +35,8 @@ struct icmp_request {
uint32_t orig_src_ip; // IP исходного отправителя (чтобы вернуть reply правильному адресату)
uint16_t echo_id;
uint16_t echo_seq;
uint16_t wire_id;
uint16_t wire_seq;
uint8_t payload[1500];
size_t payload_len;
uint64_t sent_tb;
@ -49,12 +51,12 @@ struct icmp_proxy_ctx {
socket_t raw_sock; // один SOCK_RAW для всего ICMP на exit
void* raw_read_id;
struct icmp_request* pending;
uint32_t next_token;
uint64_t request_timeout_tb;
int test_loopback; // 1 = виртуальный loopback без raw сокета (только тесты)
void* expire_timer; // периодический таймер очистки истёкших запросов
};
extern struct icmp_proxy_ctx* g_icmp_ctx;
int icmp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua);
void icmp_proxy_destroy(struct UTUN_INSTANCE* inst);

157
src/proxy/proxy_protocol.h

@ -0,0 +1,157 @@
// Общий TCP proxy протокол. DATA ограничен окном каждого потока; WINDOW возвращает
// кредит после потребления данных локальным TCP. FIN следует за всеми DATA своего
// направления. CONNECTED подтверждает реальный connect exit → destination.
#ifndef PROXY_PROTOCOL_H
#define PROXY_PROTOCOL_H
#include <stdint.h>
#include <string.h>
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node.h"
#include "../../lib/u_async.h"
#include "../../lib/ll_queue.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#define TCP_PROXY_SUBCMD_CONNECT 0x01
#define TCP_PROXY_SUBCMD_CONNECTED 0x02
#define TCP_PROXY_SUBCMD_DATA 0x03
#define TCP_PROXY_SUBCMD_CLOSE 0x04
#define TCP_PROXY_SUBCMD_ERROR 0x05
#define TCP_PROXY_SUBCMD_FIN 0x06
#define TCP_PROXY_SUBCMD_WINDOW 0x07
#define TCP_PROXY_HDR_SIZE 6
#define TCP_PROXY_CONNECT_HDR_SIZE 12
#define TCP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 5)
#define TCP_PROXY_CHUNK 4096
#define TCP_PROXY_WINDOW 65536u
struct proxy_flow {
struct UTUN_INSTANCE* inst;
struct UASYNC* ua;
uint64_t peer;
uint32_t sid;
uint8_t svc;
uint8_t ready;
uint8_t connected_pending;
uint8_t fin_pending;
uint8_t fin_sent;
uint8_t fin_received;
uint32_t tx_credit;
uint32_t rx_credit;
uint32_t consumed;
void* retry;
void (*wake)(void* arg);
void* arg;
};
// Сообщение целиком передаётся маршрутизатору, который освобождает entry при любом результате.
static inline int proxy_send(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t svc, uint8_t cmd,
uint32_t sid, const uint8_t* data, size_t len, int force) {
if (len > TCP_PROXY_CHUNK) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: oversized message sid=%08x cmd=%u len=%zu", sid, cmd, len);
return -1;
}
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: entry allocation failed sid=%08x", sid); return -1; }
e->dgram = u_malloc(TCP_PROXY_HDR_SIZE + len);
if (!e->dgram) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: payload allocation failed sid=%08x", sid);
queue_entry_free(e); return -1;
}
e->dgram[0] = svc; e->dgram[1] = cmd;
memcpy(e->dgram + 2, &sid, 4);
if (len) memcpy(e->dgram + TCP_PROXY_HDR_SIZE, data, len);
e->len = TCP_PROXY_HDR_SIZE + len;
return etcp_route_send(inst, TOPO_GROUP_UTUN, peer, e, force, 0);
}
static inline void proxy_flow_flush(struct proxy_flow* f);
// Повторяем только недоставленные control-сообщения; DATA остаётся у владельца потока.
static inline void proxy_flow_retry(void* arg) {
struct proxy_flow* f = arg;
f->retry = NULL;
proxy_flow_flush(f);
if (f->wake) f->wake(f->arg);
}
static inline void proxy_flow_init(struct proxy_flow* f, struct UTUN_INSTANCE* inst, struct UASYNC* ua,
uint64_t peer, uint8_t svc, uint32_t sid, void (*wake)(void*), void* arg) {
memset(f, 0, sizeof(*f));
f->inst = inst; f->ua = ua; f->peer = peer; f->svc = svc; f->sid = sid;
f->tx_credit = TCP_PROXY_WINDOW; f->rx_credit = TCP_PROXY_WINDOW;
f->wake = wake; f->arg = arg;
}
static inline void proxy_flow_destroy(struct proxy_flow* f) {
if (f->retry) { uasync_cancel_timeout(f->ua, f->retry); f->retry = NULL; }
}
// Сначала CONNECTED, затем WINDOW и FIN. Force обходит лишь очередь router, не окно потока.
static inline void proxy_flow_flush(struct proxy_flow* f) {
if (f->connected_pending) {
if (proxy_send(f->inst, f->peer, f->svc, TCP_PROXY_SUBCMD_CONNECTED, f->sid, NULL, 0, 1) < 0) goto retry;
f->connected_pending = 0; f->ready = 1;
}
if (f->consumed) {
uint32_t credit = f->consumed;
// Резервируем до синхронной loopback-доставки.
f->consumed = 0; f->rx_credit += credit;
if (proxy_send(f->inst, f->peer, f->svc, TCP_PROXY_SUBCMD_WINDOW, f->sid, (uint8_t*)&credit, 4, 1) < 0) {
f->consumed += credit; f->rx_credit -= credit; goto retry;
}
}
if (f->fin_pending && f->ready) {
if (proxy_send(f->inst, f->peer, f->svc, TCP_PROXY_SUBCMD_FIN, f->sid, NULL, 0, 1) < 0) goto retry;
f->fin_pending = 0; f->fin_sent = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy: FIN queued peer=%016llx sid=%08x", (unsigned long long)f->peer, f->sid);
}
return;
retry:
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy: control retry sid=%08x connected=%u credit=%u fin=%u",
f->sid, f->connected_pending, f->consumed, f->fin_pending);
if (!f->retry) f->retry = uasync_set_timeout(f->ua, 5000, f, proxy_flow_retry, "proxy_control");
}
// 0 = отправлено, -2 = ждём окно/CONNECTED, -1 = router backpressure/ошибка выделения.
static inline int proxy_flow_send(struct proxy_flow* f, const uint8_t* data, size_t len, int force) {
if (!f->ready || len > f->tx_credit) return -2;
if (!len || len > TCP_PROXY_CHUNK || f->fin_sent) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: invalid DATA sid=%08x len=%zu fin=%u", f->sid, len, f->fin_sent);
return -1;
}
f->tx_credit -= len;
int ret = proxy_send(f->inst, f->peer, f->svc, TCP_PROXY_SUBCMD_DATA, f->sid, data, len, force);
if (ret < 0) f->tx_credit += len;
return ret;
}
static inline int proxy_flow_receive(struct proxy_flow* f, size_t len) {
if (!len || len > TCP_PROXY_CHUNK || len > f->rx_credit || f->fin_received) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: invalid receive sid=%08x len=%zu credit=%u fin=%u",
f->sid, len, f->rx_credit, f->fin_received);
return -1;
}
f->rx_credit -= len;
return 0;
}
static inline int proxy_flow_window(struct proxy_flow* f, const uint8_t* data, size_t len) {
uint32_t credit = 0;
if (len == 4) memcpy(&credit, data, 4);
if (!credit || credit > TCP_PROXY_WINDOW - f->tx_credit) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: invalid WINDOW sid=%08x len=%zu credit=%u tx=%u", f->sid, len, credit, f->tx_credit);
return -1;
}
f->tx_credit += credit;
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "proxy: WINDOW sid=%08x add=%u available=%u", f->sid, credit, f->tx_credit);
return 0;
}
// Вызывать только после передачи байтов локальному TCP, не после получения ETCP DATA.
static inline void proxy_flow_consume(struct proxy_flow* f, uint32_t len) {
f->consumed += len;
proxy_flow_flush(f);
}
#endif

272
src/proxy/socks_proxy.c

@ -23,6 +23,8 @@
#include <arpa/inet.h>
#endif
static void socks_flow_wake(void* arg);
static void socks_flushed_cb(struct tcp_conn* tc, void* arg);
static void on_accept_cb(socket_t sock, void* arg);
static void on_read_cb(struct ll_queue* q, void* arg);
static void on_fin_cb(struct tcp_conn* tc, void* arg);
@ -58,24 +60,10 @@ struct listen_ctx {
// ====================================================================
// Отправка сообщений через ETCP
// ====================================================================
static int send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd, uint32_t sid, const uint8_t* data, size_t len, int force) {
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: queue_entry_new failed subcmd=%02x sid=%08x", subcmd, sid); return -1; }
e->dgram = u_malloc(TCP_PROXY_HDR_SIZE + len);
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: malloc(%zu) failed", TCP_PROXY_HDR_SIZE + len); queue_entry_free(e); return -1; }
e->dgram[0] = ETCP_RT_ID_TCP_PROXY_SERVER;
e->dgram[1] = subcmd;
memcpy(e->dgram + 2, &sid, 4);
if (len > 0) memcpy(e->dgram + TCP_PROXY_HDR_SIZE, data, len);
if (TCP_PROXY_HDR_SIZE + len > UINT16_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: msg too large len=%zu subcmd=%02x", len, subcmd);
queue_dgram_free(e); queue_entry_free(e); return -1;
}
e->len = (uint16_t)(TCP_PROXY_HDR_SIZE + len);
int ret = etcp_route_send(inst, group_id, dst, e, force, 0);
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "SOCKS send_msg subcmd=%02x sid=%08x len=%zu force=%d → ret=%d",
subcmd, sid, len, force, ret);
return ret;
static int send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd,
uint32_t sid, const uint8_t* data, size_t len, int force) {
(void)group_id;
return proxy_send(inst, dst, ETCP_RT_ID_TCP_PROXY_SERVER, subcmd, sid, data, len, force);
}
static int send_connect(struct socks_proxy_conn* c) {
@ -88,7 +76,7 @@ static int send_connect(struct socks_proxy_conn* c) {
}
static int send_data(struct socks_proxy_conn* c, const uint8_t* data, uint16_t len, int force) {
return send_msg(c->inst, TOPO_GROUP_UTUN, c->via_node_id, TCP_PROXY_SUBCMD_DATA, c->stream_id, data, len, force);
return proxy_flow_send(&c->flow, data, len, force);
}
static void send_close(struct socks_proxy_conn* c) {
@ -97,9 +85,10 @@ static void send_close(struct socks_proxy_conn* c) {
else { c->close_pending = 0; c->close_sent = 1; }
}
static void send_fin(struct socks_proxy_conn* c) {
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "SOCKS send FIN sid=%08x", c->stream_id);
send_msg(c->inst, TOPO_GROUP_UTUN, c->via_node_id, TCP_PROXY_SUBCMD_FIN, c->stream_id, NULL, 0, 1);
// Удалить только разобранные байты, сохранив следующий запрос/данные того же recv().
static void consume_header(struct socks_proxy_conn* c, uint16_t len) {
c->buf_len -= len;
memmove(c->buf, c->buf + len, c->buf_len);
}
// ====================================================================
@ -145,7 +134,7 @@ static void process_socks_greeting(struct socks_proxy_conn* c) {
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: greeting ver=%d nmethods=%d", ver, nmethods);
uint8_t reply[] = { 0x05, 0x00 };
write_to_client(c, reply, 2);
c->buf_len = 0;
consume_header(c, 2 + nmethods);
c->state = SOCKS_STATE_REQUEST;
}
@ -170,7 +159,7 @@ static void process_socks_request(struct socks_proxy_conn* c) {
if (atyp == 1) {
memcpy(c->dest_ip, c->buf + 4, 4);
memcpy(&c->dest_port, c->buf + 8, 2);
c->buf_len = 0;
consume_header(c, need);
socks_finalize(c);
return;
} else if (atyp == 4) {
@ -183,7 +172,7 @@ static void process_socks_request(struct socks_proxy_conn* c) {
uint8_t dlen = c->buf[4];
char domain[256]; memcpy(domain, c->buf + 5, dlen); domain[dlen] = '\0';
memcpy(&c->dest_port, c->buf + 5 + dlen, 2);
c->buf_len = 0;
consume_header(c, need);
socks_issue_dns(c, domain, DNSK_SOCKS);
return;
@ -204,7 +193,11 @@ static void process_http_request(struct socks_proxy_conn* c) {
uint16_t line_len = (uint16_t)((uint8_t*)line_end - c->buf);
if (line_len < 8) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: http request too short"); goto error; }
char line[512]; if (line_len > sizeof(line) - 1) line_len = sizeof(line) - 1;
char line[512];
if (line_len >= sizeof(line)) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "socks_proxy: request line too long"); goto error; }
char* hdr_end = memmem(c->buf, c->buf_len, "\r\n\r\n", 4);
if (!hdr_end) return;
uint16_t headers_len = (uint16_t)((uint8_t*)hdr_end - c->buf) + 4;
memcpy(line, c->buf, line_len); line[line_len] = '\0';
// ===== CONNECT =====
@ -222,16 +215,12 @@ static void process_http_request(struct socks_proxy_conn* c) {
c->dest_port = htons((uint16_t)port);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: HTTP CONNECT %s:%d sid=%08x", host_port, port, c->stream_id);
c->buf_len = 0;
consume_header(c, headers_len);
socks_issue_dns(c, host_port, DNSK_HTTP_CONNECT);
return;
}
// ===== Не-CONNECT: HTTP-прокси (GET, POST, PUT, HEAD, OPTIONS, ...) =====
char* hdr_end = memmem(c->buf, c->buf_len, "\r\n\r\n", 4);
if (!hdr_end) return;
uint16_t headers_len = (uint16_t)((uint8_t*)hdr_end - c->buf) + 4;
char method[16] = {0}, url[512] = {0};
if (sscanf(line, "%15s %511s", method, url) < 2) {
@ -287,7 +276,7 @@ static void process_http_request(struct socks_proxy_conn* c) {
uint16_t rest_off = line_len + 2;
uint16_t rest_len = headers_len - rest_off;
uint8_t hdr_buf[2048];
uint8_t hdr_buf[sizeof(c->buf) + 512];
int hdr_n = snprintf((char*)hdr_buf, sizeof(hdr_buf), "%s %s%s\r\n", method, path_start, version_str);
if (hdr_n < 0 || (size_t)hdr_n + rest_len > sizeof(hdr_buf)) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: header reconstruction overflow hdr_n=%d rest=%u", hdr_n, rest_len);
@ -318,43 +307,40 @@ error: {
// ====================================================================
// DNS-резолвинг (неблокирующий) + финализация рукопожатия
// ====================================================================
// DNS готов: запрашиваем exit, успех клиенту сообщаем только после CONNECTED.
static void socks_finalize(struct socks_proxy_conn* c) {
switch (c->dns_kind) {
case DNSK_SOCKS: {
uint8_t reply[] = { 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
write_to_client(c, reply, 10);
break;
}
case DNSK_HTTP_CONNECT: {
uint8_t resp[] = "HTTP/1.1 200 Connection Established\r\n\r\n";
write_to_client(c, resp, (uint16_t)strlen((char*)resp));
break;
}
case DNSK_HTTP_PROXY:
default:
break; // reply нет — сразу CONNECT + данные
c->state = SOCKS_STATE_CONNECTING;
if (send_connect(c) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: send_connect failed sid=%08x", c->stream_id);
socks_dns_error_and_close(c);
}
}
if (send_connect(c) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: send_connect failed sid=%08x kind=%d", c->stream_id, c->dns_kind);
tcp_conn_push_close(c->tc);
// Ответ CONNECTED открывает релей и выпускает сохранённый хвост рукопожатия.
static void socks_connected(struct socks_proxy_conn* c) {
if (c->state != SOCKS_STATE_CONNECTING || c->flow.ready) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "socks_proxy: unexpected CONNECTED sid=%08x state=%u", c->stream_id, c->state);
return;
}
if (c->dns_kind == DNSK_HTTP_PROXY && c->dns_pending) {
uint8_t* pkt = c->dns_pending;
uint16_t len = c->dns_pending_len;
c->flow.ready = 1;
if (c->dns_kind == DNSK_SOCKS) {
const uint8_t reply[] = {5, 0, 0, 1, 0, 0, 0, 0, 0, 0};
if (write_to_client(c, reply, sizeof(reply)) < 0) { on_error_cb(c->tc, ENOMEM, c); return; }
} else if (c->dns_kind == DNSK_HTTP_CONNECT) {
const uint8_t reply[] = "HTTP/1.1 200 Connection Established\r\n\r\n";
if (write_to_client(c, reply, sizeof(reply) - 1) < 0) { on_error_cb(c->tc, ENOMEM, c); return; }
}
c->state = SOCKS_STATE_RELAY;
if (c->dns_pending) {
c->tx_buf = c->dns_pending; c->tx_len = c->dns_pending_len;
c->dns_pending = NULL; c->dns_pending_len = 0;
int ret = send_data(c, pkt, len, 0);
if (ret == 0) {
u_free(pkt);
} else {
c->tx_buf = pkt; c->tx_len = len;
socks_bp_register(c);
}
} else if (c->buf_len) {
c->tx_buf = u_malloc(c->buf_len);
if (!c->tx_buf) { on_error_cb(c->tc, ENOMEM, c); return; }
memcpy(c->tx_buf, c->buf, c->buf_len); c->tx_len = c->buf_len; c->buf_len = 0;
}
c->state = c->is_http ? HTTP_STATE_RELAY : SOCKS_STATE_RELAY;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: established sid=%08x buffered=%u", c->stream_id, c->tx_len);
socks_flow_wake(c);
}
static void socks_dns_error_and_close(struct socks_proxy_conn* c) {
@ -365,6 +351,7 @@ static void socks_dns_error_and_close(struct socks_proxy_conn* c) {
uint8_t err[] = { 0x05, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
write_to_client(c, err, 10);
}
c->rem_closed = 1;
tcp_conn_push_close(c->tc);
}
@ -402,6 +389,7 @@ static void socks_dns_done_cb(const struct adns_result* res, void* arg) {
// ====================================================================
static void on_read_cb(struct ll_queue* q, void* arg) {
struct socks_proxy_conn* c = (struct socks_proxy_conn*)arg;
if (c->state == SOCKS_STATE_CONNECTING || c->tx_buf || c->rem_closed) return;
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
@ -421,43 +409,16 @@ static void on_read_cb(struct ll_queue* q, void* arg) {
memcpy(c->buf + c->buf_len, e->dgram, e->len);
c->buf_len += e->len;
memory_pool_free(c->tc->data_pool, e->dgram); queue_entry_free(e);
queue_resume_callback(q);
if (c->is_http) {
process_http_request(c);
} else {
if (c->state == SOCKS_STATE_GREETING) process_socks_greeting(c);
if (c->state == SOCKS_STATE_REQUEST) process_socks_request(c);
if (!c->rem_closed && c->state == SOCKS_STATE_REQUEST) process_socks_request(c);
}
if (c->state != SOCKS_STATE_CONNECTING && !c->rem_closed) queue_resume_callback(q);
return;
}
if (c->state == SOCKS_STATE_CONNECTING || c->state == HTTP_STATE_CONNECTING) {
// DNS в процессе: копим входящий body (HTTP proxy) / дропаем (CONNECT)
if (c->dns_kind == DNSK_HTTP_PROXY) {
size_t nl = (size_t)c->dns_pending_len + e->len;
if (nl <= UINT16_MAX) {
uint8_t* nb = u_realloc(c->dns_pending, nl);
if (nb) { memcpy(nb + c->dns_pending_len, e->dgram, e->len); c->dns_pending = nb; c->dns_pending_len = (uint16_t)nl; }
else DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: dns_pending realloc failed sid=%08x", c->stream_id);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: dns_pending overflow sid=%08x", c->stream_id);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "socks_proxy: data during DNS (kind=%d) sid=%08x — drop len=%u", c->dns_kind, c->stream_id, e->len);
}
memory_pool_free(c->tc->data_pool, e->dgram); queue_entry_free(e);
queue_resume_callback(q);
return;
}
// RELAY = релей данных в ETCP
if (c->tx_buf) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: RELAY with pending tx_buf sid=%08x — drop new len=%u", c->stream_id, e->len);
memory_pool_free(c->tc->data_pool, e->dgram); queue_entry_free(e);
queue_resume_callback(q);
return;
}
int ret = send_data(c, e->dgram, e->len, 0);
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "SOCKS PROXY SEND sid=%08x len=%u ret=%d is_http=%d", c->stream_id, e->len, ret, c->is_http);
if (ret == 0) {
@ -466,90 +427,70 @@ static void on_read_cb(struct ll_queue* q, void* arg) {
} else {
c->tx_buf = u_malloc(e->len);
if (c->tx_buf) { memcpy(c->tx_buf, e->dgram, e->len); c->tx_len = e->len; }
else { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: tx_buf malloc=%u failed sid=%08x — drop", e->len, c->stream_id); }
else { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: tx_buf allocation failed sid=%08x", c->stream_id); }
memory_pool_free(c->tc->data_pool, e->dgram); queue_entry_free(e);
if (!c->tx_buf) { on_error_cb(c->tc, ENOMEM, c); return; }
socks_bp_register(c);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCKS PROXY BP: sid=%08x tx_buf=%u waiter_reg", c->stream_id, c->tx_len);
}
}
// Отправка буфера частями сохраняет HTTP headers/body, независимо от размера одного DATA.
static void socks_flow_wake(void* arg) {
struct socks_proxy_conn* c = arg;
if (c->rem_closed || c->freed || !c->flow.ready) return;
while (c->tx_buf) {
uint16_t len = c->tx_len > TCP_PROXY_CHUNK ? TCP_PROXY_CHUNK : c->tx_len;
int ret = send_data(c, c->tx_buf, len, 0);
if (ret < 0) { socks_bp_register(c); return; }
c->tx_len -= len;
if (c->tx_len) memmove(c->tx_buf, c->tx_buf + len, c->tx_len);
else { u_free(c->tx_buf); c->tx_buf = NULL; }
}
queue_resume_callback(c->tc->read_queue);
socks_maybe_relay_fin(c);
}
static void tx_waiter_cb(struct ll_queue* q, void* arg) {
(void)q;
struct socks_proxy_conn* c = (struct socks_proxy_conn*)arg;
if (c->rem_closed || c->close_sent) return;
if (!c->tx_buf) { if (c->tc && c->tc->read_queue) queue_resume_callback(c->tc->read_queue); return; }
int ret = send_data(c, c->tx_buf, c->tx_len, 0);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCKS PROXY WAKE: sid=%08x tx_buf=%u ret=%d is_http=%d", c->stream_id, c->tx_len, ret, c->is_http);
if (ret == 0) {
u_free(c->tx_buf); c->tx_buf = NULL; c->tx_len = 0;
if (c->tc && c->tc->read_queue) queue_resume_callback(c->tc->read_queue);
socks_maybe_relay_fin(c);
} else {
socks_bp_register(c);
}
socks_flow_wake(arg);
}
static void tx_retry_timer_cb(void* arg) {
struct socks_proxy_conn* c = (struct socks_proxy_conn*)arg;
struct socks_proxy_conn* c = arg;
c->tx_retry_timer = NULL;
if (c->rem_closed || c->close_sent) return;
if (!c->tx_buf) return;
int ret = send_data(c, c->tx_buf, c->tx_len, 1);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCKS PROXY RETRY: sid=%08x len=%u force=1 ret=%d", c->stream_id, c->tx_len, ret);
if (ret == 0) {
u_free(c->tx_buf); c->tx_buf = NULL; c->tx_len = 0;
if (c->tc && c->tc->read_queue) queue_resume_callback(c->tc->read_queue);
socks_maybe_relay_fin(c);
} else {
c->tx_retry_timer = uasync_set_timeout(c->ua, 5000, c, tx_retry_timer_cb, "socks_retry");
}
socks_flow_wake(c);
}
static void socks_bp_register(struct socks_proxy_conn* c) {
etcp_router_on_send_ready(c->inst, TOPO_GROUP_UTUN, c->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &c->tx_waiter, tx_waiter_cb, c);
if (!c->tx_retry_timer)
c->tx_retry_timer = uasync_set_timeout(c->ua, 5000, c, tx_retry_timer_cb, "socks_retry");
if (c->flow.ready && c->flow.tx_credit >= (c->tx_len > TCP_PROXY_CHUNK ? TCP_PROXY_CHUNK : c->tx_len))
etcp_router_on_send_ready(c->inst, TOPO_GROUP_UTUN, c->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &c->tx_waiter, tx_waiter_cb, c);
if (!c->tx_retry_timer) c->tx_retry_timer = uasync_set_timeout(c->ua, 5000, c, tx_retry_timer_cb, "socks_retry");
}
// Обе половины закрываются независимо: write_queue не задерживает FIN прочитанного направления.
static void socks_maybe_relay_fin(struct socks_proxy_conn* c) {
if (c->fin_deferred && !c->tx_buf) {
c->fin_deferred = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: relay deferred FIN sid=%08x", c->stream_id);
send_fin(c);
if (c->fin_remote && !c->close_sent && !c->close_pending) send_close(c);
if (c->rem_closed || c->close_queued || !c->flow.ready) return;
if (c->tc->fin_remote && !c->tx_buf && !c->tc->read_queue->head && !c->flow.fin_sent) {
c->flow.fin_pending = 1;
proxy_flow_flush(&c->flow);
}
if (c->flow.fin_sent && c->flow.fin_received && c->tc->fin_local) {
c->close_queued = 1;
tcp_conn_push_close(c->tc);
}
}
// ====================================================================
// tcp_io: FIN / Error / Closed
// ====================================================================
static void on_fin_cb(struct tcp_conn* tc, void* arg) {
struct socks_proxy_conn* c = (struct socks_proxy_conn*)arg;
if (c->rem_closed || c->close_sent) return;
if (c->tx_buf) {
// отложенный FIN: дождёмся сброса tx_buf (backpressure)
c->fin_deferred = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: local FIN deferred (tx_buf pending) sid=%08x", c->stream_id);
return;
}
if (tc->fin_local) {
// оба FIN обменяны: мы уже shutdown-нули свой write (fin_local), peer прислал FIN.
// Закрываем локальный сокет, симметрично exit-стороне (tcp_proxy_server.c on_fin_cb).
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: both FINs done → close local socket sid=%08x", c->stream_id);
send_close(c);
tcp_conn_push_close(tc);
return;
}
if (!tc->write_buf && !tc->write_queue->head) {
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: local FIN → relay FIN sid=%08x", c->stream_id);
send_fin(c);
if (c->fin_remote && !c->close_sent && !c->close_pending) send_close(c);
} else {
// данные ещё в write_queue, отложим FIN
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "socks_proxy: local FIN deferred (wq=%d wbuf=%s) sid=%08x",
tc->write_queue->count, tc->write_buf ? "y" : "n", c->stream_id);
tcp_conn_set_flushed(tc, NULL);
}
(void)tc;
socks_maybe_relay_fin(arg);
}
static void socks_flushed_cb(struct tcp_conn* tc, void* arg) {
struct socks_proxy_conn* c = arg;
if (!tc->write_buf && !tc->write_queue->head)
proxy_flow_consume(&c->flow, TCP_PROXY_WINDOW - c->flow.rx_credit - c->flow.consumed);
socks_maybe_relay_fin(c);
}
static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
@ -563,6 +504,7 @@ static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
struct UTUN_INSTANCE* inst = c->inst;
uint64_t via = c->via_node_id;
uint32_t sid = c->stream_id;
c->rem_closed = 1;
socks_proxy_conn_free_soon(c);
if (notify && inst) send_msg(inst, TOPO_GROUP_UTUN, via, TCP_PROXY_SUBCMD_ERROR, sid, NULL, 0, 1);
}
@ -571,7 +513,7 @@ static void on_closed_cb(struct tcp_conn* tc, void* arg) {
struct socks_proxy_conn* c = (struct socks_proxy_conn*)arg;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: tcp closed sid=%08x state=%d fm_r=%d fm_l=%d rem_cl=%d bytes_client=%u bytes_exit=%u",
c->stream_id, c->state, tc->fin_remote, tc->fin_local, c->rem_closed, c->bytes_to_client, c->bytes_from_exit);
if (!c->close_sent && !c->close_pending) send_close(c);
if (!c->flow.fin_sent && !c->rem_closed && !c->close_sent && !c->close_pending) send_close(c);
socks_proxy_conn_free_soon(c);
}
@ -592,10 +534,12 @@ static void on_accept_cb(socket_t sock, void* arg) {
c->ua = ctx->ua; c->inst = ctx->inst; c->via_node_id = ctx->via_node_id;
c->state = ctx->is_http ? HTTP_STATE_REQUEST : SOCKS_STATE_GREETING;
c->head = ctx->conns; c->count = ctx->conn_count;
proxy_flow_init(&c->flow, ctx->inst, ctx->ua, ctx->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, c->stream_id, socks_flow_wake, c);
c->tc = tcp_conn_create(ctx->ua, csock, 4096, 4096, 8, 0, 0, on_fin_cb, on_error_cb, c);
if (!c->tc) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: tcp_conn_create failed"); u_free(c); socket_close_wrapper(csock); return; }
c->tc->on_closed = on_closed_cb;
c->tc->on_fin_sent = on_fin_cb;
queue_set_callback(c->tc->read_queue, on_read_cb, c);
queue_set_waiter_defer(c->tc->read_queue, 1);
@ -620,9 +564,17 @@ int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count,
const uint8_t* data, size_t data_len) {
struct socks_proxy_conn* c = socks_proxy_find_conn(*head, stream_id);
if (!c) return 0;
if (c->rem_closed || c->freed) return 1;
if (subcmd == TCP_PROXY_SUBCMD_CONNECTED) { socks_connected(c); return 1; }
if (subcmd == TCP_PROXY_SUBCMD_WINDOW) {
if (proxy_flow_window(&c->flow, data, data_len) < 0) on_error_cb(c->tc, EPROTO, c);
else socks_flow_wake(c);
return 1;
}
if (subcmd == TCP_PROXY_SUBCMD_DATA) {
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "SOCKS DATA <- sid=%08x len=%zu", stream_id, data_len);
if (proxy_flow_receive(&c->flow, data_len) < 0) { on_error_cb(c->tc, EPROTO, c); return 1; }
c->bytes_from_exit += (uint32_t)data_len;
if (data_len > 0) {
if (data_len > c->tc->data_pool->object_size) {
@ -635,11 +587,13 @@ int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count,
if (e && buf) {
memcpy(buf, data, data_len); e->dgram = buf; e->len = (uint16_t)data_len;
c->bytes_to_client += (uint32_t)data_len;
tcp_conn_set_flushed(c->tc, socks_flushed_cb);
queue_data_put(c->tc->write_queue, e);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "socks_proxy: handle_data alloc failed sid=%08x wq=%d bytes_in=%u bytes_out=%u",
stream_id, c->tc->write_queue->count, c->bytes_from_exit, c->bytes_to_client);
if (e) queue_entry_free(e); if (buf) memory_pool_free(c->tc->data_pool, buf);
on_error_cb(c->tc, ENOMEM, c);
}
}
return 1;
@ -654,7 +608,8 @@ int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count,
}
if (subcmd == TCP_PROXY_SUBCMD_ERROR) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: ERROR from exit sid=%08x", stream_id);
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "socks_proxy: ERROR from exit sid=%08x", stream_id);
if (!c->flow.ready) { socks_dns_error_and_close(c); return 1; }
c->rem_closed = 1;
etcp_router_cancel_send_ready(c->inst, TOPO_GROUP_UTUN, c->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &c->tx_waiter);
tcp_conn_push_close(c->tc);
@ -663,9 +618,9 @@ int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count,
if (subcmd == TCP_PROXY_SUBCMD_FIN) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: FIN from exit sid=%08x", stream_id);
c->fin_remote = 1;
tcp_conn_push_fin(c->tc);
if (c->tc->fin_local && !c->close_sent && !c->close_pending) send_close(c);
if (c->flow.fin_received) return 1;
c->fin_remote = 1; c->flow.fin_received = 1;
if (tcp_conn_push_fin(c->tc) < 0) on_error_cb(c->tc, ENOMEM, c);
return 1;
}
@ -687,6 +642,7 @@ void socks_proxy_conn_free(struct socks_proxy_conn* c) {
if (!c) return;
if (c->freed) return;
c->freed = 1;
proxy_flow_destroy(&c->flow);
if (c->free_soon_id) { uasync_call_soon_cancel(c->ua, c->free_soon_id); c->free_soon_id = NULL; }
struct socks_proxy_conn** head = c->head; int* count = c->count;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "socks_proxy: FREE sid=%08x state=%d total=%d", c->stream_id, c->state, count ? *count : 0);

5
src/proxy/socks_proxy.h

@ -8,6 +8,7 @@ extern "C" {
#include <stdint.h>
#include "proxy_protocol.h"
#include "../lib/socket_compat.h"
#include "../lib/ll_queue.h"
@ -42,6 +43,7 @@ struct socks_proxy_conn {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
uint64_t via_node_id;
struct proxy_flow flow;
uint32_t stream_id;
uint8_t dest_ip[4];
uint16_t dest_port;
@ -52,8 +54,9 @@ struct socks_proxy_conn {
uint8_t is_http;
uint8_t state;
uint8_t freed;
uint8_t close_queued;
void* free_soon_id; // handle отложенного освобождения (uasync_call_soon)
uint8_t buf[1024];
uint8_t buf[16384];
uint16_t buf_len;
uint8_t* tx_buf; // буфер при backpressure (retry в tx_waiter_cb)
uint16_t tx_len;

221
src/proxy/tcp_proxy_client.c

@ -34,6 +34,8 @@
// ====================================================================
// Предварительные объявления
// ====================================================================
static void tcp_proxy_client_flow_wake(void* arg);
static int tcp_proxy_client_fin_flush(struct tcp_proxy_client_conn* pc);
static err_t tcp_proxy_client_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err);
static err_t tcp_proxy_client_sent_cb(void *arg, struct tcp_pcb *pcb, uint16_t len);
static void tcp_proxy_client_err_cb(void *arg, err_t err);
@ -70,17 +72,9 @@ static struct ll_entry* tcp_proxy_client_entry_from_data(struct memory_pool* poo
// Протокол: сборка и отправка сообщений прокси через ETCP
// ====================================================================
static int tcp_proxy_client_send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd,
uint32_t sid, const uint8_t* data, size_t len, int force) {
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "queue_entry_new failed subcmd=%02x sid=%08x", subcmd, sid); return -1; }
e->dgram = u_malloc(TCP_PROXY_HDR_SIZE + len);
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "malloc(%zu) failed subcmd=%02x sid=%08x", TCP_PROXY_HDR_SIZE + len, subcmd, sid); queue_entry_free(e); return -1; }
e->dgram[0] = ETCP_RT_ID_TCP_PROXY_SERVER;
e->dgram[1] = subcmd;
memcpy(e->dgram + 2, &sid, 4);
if (len > 0) memcpy(e->dgram + TCP_PROXY_HDR_SIZE, data, len);
e->len = TCP_PROXY_HDR_SIZE + len;
return etcp_route_send(inst, group_id, dst, e, force, 0);
uint32_t sid, const uint8_t* data, size_t len, int force) {
(void)group_id;
return proxy_send(inst, dst, ETCP_RT_ID_TCP_PROXY_SERVER, subcmd, sid, data, len, force);
}
static int tcp_proxy_client_send_connect(struct tcp_proxy_client_conn* pc) {
@ -94,8 +88,7 @@ static int tcp_proxy_client_send_connect(struct tcp_proxy_client_conn* pc) {
}
static int tcp_proxy_client_send_data(struct tcp_proxy_client_conn* pc, const uint8_t* data, uint16_t len, int force) {
int ret = tcp_proxy_client_send_msg(pc->proxy->inst, TOPO_GROUP_UTUN, pc->proxy->via_node_id,
TCP_PROXY_SUBCMD_DATA, pc->stream_id, data, len, force);
int ret = proxy_flow_send(&pc->flow, data, len, force);
if (ret == 0) pc->bytes_to_exit += len;
else {
pc->bp_count++;
@ -125,12 +118,6 @@ static int tcp_proxy_client_send_error(struct tcp_proxy_client_conn* pc) {
return 0;
}
static int tcp_proxy_client_send_fin(struct tcp_proxy_client_conn* pc) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY FIN RELAY sid=%08x", pc->stream_id);
return tcp_proxy_client_send_msg(pc->proxy->inst, TOPO_GROUP_UTUN, pc->proxy->via_node_id,
TCP_PROXY_SUBCMD_FIN, pc->stream_id, NULL, 0, 1);
}
// ====================================================================
// Вывод: lwIP TCP отправляет IP пакеты через этот callback
// ====================================================================
@ -189,53 +176,33 @@ static int tcp_proxy_client_handle_non_tcp(struct tcp_proxy_client* p, uint8_t*
// Помощник: передача данных из очереди to_lwip в lwIP TCP
// ====================================================================
static void tcp_proxy_client_feed_from_transport(struct tcp_proxy_client_conn *pc) {
if (!pc->to_lwip || !pc->pcb) return;
if (pc->rem_closed) return;
int sent_any = 0;
uint32_t q_pre = queue_entry_count(pc->to_lwip);
while (1) {
if (!pc->to_lwip || !pc->pcb || pc->rem_closed) return;
int sent = 0;
while (pc->to_lwip->head) {
uint16_t space = tcp_sndbuf(pc->pcb);
if (space < TCP_MSS / 2) {
if (queue_entry_count(pc->to_lwip) > 0)
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"PROXY FEED stalled sid=%08x snd_buf=%u to_lwip=%d snd_wnd=%u cwnd=%u",
pc->stream_id, space, queue_entry_count(pc->to_lwip),
pc->pcb->snd_wnd, pc->pcb->cwnd);
break;
if (!space) break;
struct ll_entry* e = queue_data_get(pc->to_lwip);
uint16_t len = e->len < space ? e->len : space;
err_t ret = tcp_write(pc->pcb, e->dgram, len, TCP_WRITE_FLAG_COPY);
if (ret != LERR_OK) {
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy: lwIP write blocked sid=%08x ret=%d space=%u", pc->stream_id, ret, space);
queue_data_put_first(pc->to_lwip, e); break;
}
struct ll_entry *e = queue_data_get(pc->to_lwip);
if (!e) break;
if (e->len <= space) {
err_t ret = tcp_write(pc->pcb, e->dgram, e->len, TCP_WRITE_FLAG_COPY);
if (ret == LERR_OK) { sent_any = 1; queue_dgram_free(e); queue_entry_free(e); }
else {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"PROXY FEED write fail sid=%08x len=%u ret=%d snd_buf=%u q=%d",
pc->stream_id, e->len, ret, space, queue_entry_count(pc->to_lwip) + 1);
queue_data_put_first(pc->to_lwip, e); break;
}
} else {
queue_data_put_first(pc->to_lwip, e);
break;
}
queue_resume_callback(pc->to_lwip);
sent = 1;
e->len -= len;
if (e->len) { memmove(e->dgram, e->dgram + len, e->len); queue_data_put_first(pc->to_lwip, e); }
else { queue_dgram_free(e); queue_entry_free(e); }
// Буфер lwIP тоже ограничен snd_buf; to_lwip ограничен окном proxy.
proxy_flow_consume(&pc->flow, len);
}
queue_resume_callback(pc->to_lwip);
if (sent_any) {
uint32_t unsent = 0; struct tcp_seg* s;
for (s = pc->pcb->unsent; s; s = s->next) unsent++;
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "PROXY FEED exit->client sid=%08x fed=%u q=%u->%u snd_wnd=%u cwnd=%u unsent=%u",
pc->stream_id, sent_any, q_pre, queue_entry_count(pc->to_lwip), pc->pcb->snd_wnd, pc->pcb->cwnd, unsent);
tcp_output(pc->pcb);
}
// FIN от exit был отложен до дренажа to_lwip — теперь очередь пуста, шлём FIN локальной стороне
if (pc->fin_deferred && pc->pcb && queue_entry_count(pc->to_lwip) == 0 &&
pc->pcb->state != TIME_WAIT && pc->pcb->state != CLOSED) {
pc->fin_deferred = 0;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY FIN RELAY (deferred) sid=%08x — to_lwip drained, shutdown write", pc->stream_id);
tcp_shutdown(pc->pcb, 0, 1);
tcp_sent(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
if (sent) tcp_output(pc->pcb);
if (pc->fin_deferred && !pc->to_lwip->head) {
err_t ret = tcp_shutdown(pc->pcb, 0, 1);
if (ret == LERR_OK) {
pc->fin_deferred = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy: lwIP FIN queued sid=%08x", pc->stream_id);
} else DEBUG_WARN(DEBUG_CATEGORY_PROXY, "proxy: lwIP FIN retry sid=%08x ret=%d", pc->stream_id, ret);
}
}
@ -249,6 +216,7 @@ static err_t tcp_proxy_client_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t
struct tcp_proxy_client_conn *pc = u_calloc(1, sizeof(struct tcp_proxy_client_conn));
if (!pc) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: accept alloc failed"); return LERR_MEM; }
pc->proxy = p; pc->pcb = newpcb; pc->stream_id = ++p->next_stream_id;
proxy_flow_init(&pc->flow, p->inst, p->ua, p->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, pc->stream_id, tcp_proxy_client_flow_wake, pc);
int i;
for (i = 0; i < p->mapping_count; i++) {
@ -273,14 +241,14 @@ static err_t tcp_proxy_client_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t
pc->to_lwip = queue_new(p->ua, 0, 0, 0, "to_lwip");
pc->tx_queue = queue_new(p->ua, 0, 0, 0, "tx_queue");
if (!pc->tx_queue) {
if (!pc->tx_queue || !pc->to_lwip) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: tx_queue alloc failed sid=%08x", pc->stream_id);
queue_free(pc->to_lwip); pc->to_lwip = NULL;
tcp_arg(newpcb, NULL); tcp_abort(newpcb); u_free(pc); return LERR_MEM;
queue_free(pc->to_lwip); queue_free(pc->tx_queue); pc->to_lwip = NULL;
tcp_arg(newpcb, NULL); tcp_abort(newpcb); u_free(pc); return LERR_ABRT;
}
queue_set_callback(pc->tx_queue, tcp_proxy_client_tx_queue_drain_cb, pc);
if (tcp_proxy_client_send_connect(pc) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: send_connect failed sid=%08x to %d.%d.%d.%d:%d", pc->stream_id, pc->dest_ip[0], pc->dest_ip[1], pc->dest_ip[2], pc->dest_ip[3], ntohs(pc->dest_port)); tcp_arg(newpcb, NULL); tcp_abort(newpcb); queue_free(pc->to_lwip); u_free(pc); return LERR_MEM; }
if (tcp_proxy_client_send_connect(pc) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: send_connect failed sid=%08x to %d.%d.%d.%d:%d", pc->stream_id, pc->dest_ip[0], pc->dest_ip[1], pc->dest_ip[2], pc->dest_ip[3], ntohs(pc->dest_port)); tcp_arg(newpcb, NULL); tcp_abort(newpcb); queue_free(pc->to_lwip); queue_free(pc->tx_queue); u_free(pc); return LERR_ABRT; }
pc->next = p->conns; p->conns = pc; p->conn_count++;
pc->diag_timer = uasync_set_timeout(p->ua, 5000, pc, tcp_proxy_client_diag_timer_cb, "tpc_diag");
@ -297,19 +265,25 @@ static err_t tcp_proxy_client_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t
// После отправки последних данных (очередь пуста): релеить FIN/CLOSE в exit.
// Возвращает 1, если соединение завершено и pc освобождён (дальше pc/q использовать нельзя).
static int tcp_proxy_client_fin_flush(struct tcp_proxy_client_conn* pc) {
if (pc->fin_local && !pc->close_sent && !pc->close_pending) {
if (pc->fin_remote || pc->rem_closed)
tcp_proxy_client_send_close(pc);
else
tcp_proxy_client_send_fin(pc);
if (pc->fin_local && !pc->tx_queue->head && !pc->flow.fin_sent) {
pc->flow.fin_pending = 1;
proxy_flow_flush(&pc->flow);
}
if (pc->fin_local && pc->fin_remote) {
if (pc->flow.fin_sent && pc->fin_remote && !pc->fin_deferred && !pc->tx_queue->head && !pc->to_lwip->head) {
tcp_proxy_client_conn_finish(pc);
return 1;
}
return 0;
}
static void tcp_proxy_client_flow_wake(void* arg) {
struct tcp_proxy_client_conn* pc = arg;
if (pc->rem_closed || pc->error) return;
queue_resume_callback(pc->tx_queue);
tcp_proxy_client_feed_from_transport(pc);
tcp_proxy_client_fin_flush(pc);
}
// Дрейн tx_queue → ETCP. tcp_recved вызывается только после успешной отправки:
// окно закрывается плавно (штатная backpressure), а при возобновлении открывается
// пачкой (за 2 пакета wnd_inflation ≥ TCP_WND_UPDATE_THRESHOLD → немедленный ACK).
@ -330,7 +304,7 @@ static void tcp_proxy_client_tx_queue_drain_cb(struct ll_queue* q, void* arg) {
queue_data_put_first(q, e);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "PROXY BP stall sid=%08x q=%d rcv_wnd=%u",
pc->stream_id, queue_entry_count(q), pc->pcb ? pc->pcb->rcv_wnd : 0);
etcp_router_on_send_ready(pc->proxy->inst, TOPO_GROUP_UTUN, pc->proxy->via_node_id,
if (ret != -2) etcp_router_on_send_ready(pc->proxy->inst, TOPO_GROUP_UTUN, pc->proxy->via_node_id,
ETCP_RT_ID_TCP_PROXY_SERVER, &pc->tx_waiter,
tcp_proxy_client_pause_resume_cb, pc);
if (!pc->tx_retry_timer)
@ -421,19 +395,22 @@ static err_t tcp_proxy_client_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbu
tcp_recved(pcb, len); pbuf_free(p); return LERR_OK;
}
uint8_t *data = u_malloc(len);
if (!data) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY recv malloc(%u) failed sid=%08x", len, pc->stream_id);
pbuf_free(p); return LERR_OK;
}
pbuf_copy_partial(p, data, len, 0);
struct ll_entry* e = queue_entry_new_from_pool(pc->proxy->entry_pool);
if (!e) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY recv entry pool exhausted sid=%08x", pc->stream_id);
u_free(data); pbuf_free(p); return LERR_OK;
struct ll_entry* head = NULL;
struct ll_entry** tail = &head;
for (uint32_t off = 0; off < len;) {
uint16_t chunk = len - off > TCP_PROXY_CHUNK ? TCP_PROXY_CHUNK : len - off;
struct ll_entry* e = queue_entry_new_from_pool(pc->proxy->entry_pool);
if (e) { e->dgram = u_malloc(chunk); e->len = chunk; }
if (!e || !e->dgram) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: recv allocation failed sid=%08x len=%u", pc->stream_id, len);
if (e) queue_entry_free(e);
while (head) { struct ll_entry* next = head->next; queue_dgram_free(head); queue_entry_free(head); head = next; }
return LERR_MEM;
}
pbuf_copy_partial(p, e->dgram, chunk, off);
e->next = NULL; *tail = e; tail = &e->next; off += chunk;
}
e->dgram = data; e->len = len;
queue_data_put(pc->tx_queue, e);
while (head) { struct ll_entry* next = head->next; head->next = NULL; queue_data_put(pc->tx_queue, head); head = next; }
pbuf_free(p);
return LERR_OK;
}
@ -443,6 +420,7 @@ static err_t tcp_proxy_client_sent_cb(void *arg, struct tcp_pcb *pcb, uint16_t l
struct tcp_proxy_client_conn *pc = (struct tcp_proxy_client_conn *)arg;
if (!pc || !pc->proxy || pc->rem_closed) return LERR_OK;
tcp_proxy_client_feed_from_transport(pc);
tcp_proxy_client_fin_flush(pc);
return LERR_OK;
}
@ -452,12 +430,11 @@ static void tcp_proxy_client_err_cb(void *arg, err_t err) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: error %d sid=%08x fin_local=%d rem_closed=%d",
err, pc->stream_id, pc->fin_local, pc->rem_closed);
pc->pcb = NULL; // pcb уже освобождён (или вот-вот) стеком lwip — не разыменовывать
if (err == LERR_CLSD) { // graceful close активной стороной — ERROR слать не нужно
tcp_proxy_client_conn_free(pc);
return;
if (err != LERR_CLSD) {
pc->error = 1;
if (tcp_proxy_client_send_error(pc) < 0) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY send_error failed sid=%08x", pc->stream_id);
}
pc->error = 1;
if (tcp_proxy_client_send_error(pc) < 0) DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY send_error failed sid=%08x", pc->stream_id);
tcp_proxy_client_conn_free(pc);
}
static err_t tcp_proxy_client_poll_cb(void *arg, struct tcp_pcb *pcb) {
@ -476,9 +453,11 @@ static err_t tcp_proxy_client_poll_cb(void *arg, struct tcp_pcb *pcb) {
pc->pcb = NULL;
}
tcp_proxy_client_conn_free(pc);
return LERR_OK;
return LERR_ABRT;
}
tcp_proxy_client_feed_from_transport(pc);
if (tcp_proxy_client_fin_flush(pc)) return LERR_OK;
if (pc->close_pending) {
if (pc->error) tcp_proxy_client_send_error(pc);
else tcp_proxy_client_send_close(pc);
@ -546,6 +525,7 @@ static void tcp_proxy_client_tun_input(struct ll_queue* q, void* arg) {
static void tcp_proxy_client_conn_free(struct tcp_proxy_client_conn *pc) {
if (!pc) return;
struct tcp_proxy_client *p = pc->proxy;
proxy_flow_destroy(&pc->flow);
uint32_t pending = pc->to_lwip ? queue_entry_count(pc->to_lwip) : 0;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY FREE sid=%08x total_conns=%d to_lwip_q=%u",
pc->stream_id, p->conn_count, pending);
@ -608,8 +588,16 @@ static void tcp_proxy_client_handle_data(struct tcp_proxy_client* p, struct ETCP
if (data_len > 0) {
pc->bytes_from_exit += (uint32_t)data_len;
struct ll_entry* e = tcp_proxy_client_entry_from_data(pc->proxy->entry_pool, entry->dgram + TCP_PROXY_RECV_HDR_SIZE, (uint16_t)data_len);
if (e) queue_data_put(pc->to_lwip, e);
tcp_proxy_client_feed_from_transport(pc);
if (!e || proxy_flow_receive(&pc->flow, data_len) < 0) {
if (e) { queue_dgram_free(e); queue_entry_free(e); }
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: receive failed sid=%08x", stream_id);
tcp_proxy_client_send_error(pc);
if (pc->pcb) { tcp_arg(pc->pcb, NULL); tcp_err(pc->pcb, NULL); tcp_abort(pc->pcb); pc->pcb = NULL; }
tcp_proxy_client_conn_free(pc);
} else {
queue_data_put(pc->to_lwip, e);
tcp_proxy_client_feed_from_transport(pc);
}
}
queue_dgram_free(entry); queue_entry_free(entry);
}
@ -641,7 +629,7 @@ static void tcp_proxy_client_handle_error(struct tcp_proxy_client* p, uint32_t s
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
tcp_close(pc->pcb);
tcp_abort(pc->pcb);
pc->pcb = NULL;
}
pc->rem_closed = 1;
@ -653,22 +641,10 @@ static void tcp_proxy_client_handle_fin(struct tcp_proxy_client* p, uint32_t str
if (!pc || !pc->pcb) return;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY FIN FROM exit sid=%08x pcb_state=%u to_lwip=%d — shutdown write (send FIN to local)",
stream_id, pc->pcb->state, pc->to_lwip ? queue_entry_count(pc->to_lwip) : -1);
pc->fin_remote = 1;
// Если в to_lwip ещё есть данные от exit — откладываем FIN до их дренажа,
// иначе остаток потеряется (после tcp_shutdown писать в lwIP нельзя).
if (pc->to_lwip && queue_entry_count(pc->to_lwip) > 0 && pc->pcb->state != TIME_WAIT && pc->pcb->state != CLOSED) {
pc->fin_deferred = 1;
tcp_proxy_client_feed_from_transport(pc);
return;
}
if (pc->pcb->state != TIME_WAIT && pc->pcb->state != CLOSED) {
tcp_shutdown(pc->pcb, 0, 1);
tcp_sent(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
}
if (pc->fin_local && !pc->close_sent && !pc->close_pending)
tcp_proxy_client_send_close(pc);
if (pc->fin_local) tcp_proxy_client_conn_finish(pc);
if (pc->flow.fin_received) return;
pc->fin_remote = 1; pc->flow.fin_received = 1; pc->fin_deferred = 1;
tcp_proxy_client_feed_from_transport(pc);
tcp_proxy_client_fin_flush(pc);
}
// ====================================================================
@ -682,6 +658,15 @@ void tcp_proxy_client_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* en
if (entry) { 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; }
uint64_t peer, group;
memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
memcpy(&group, entry->dgram + ROUTER_SVC_GROUP_OFF, 8);
if (!proxy || peer != proxy->via_node_id || group != TOPO_GROUP_UTUN) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "proxy client: unexpected peer=%016llx group=%016llx",
(unsigned long long)peer, (unsigned long long)group);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
// CLOSE_ALL / restart-уведомление: [svc_id][src][dst] без payload
if (entry->len == ROUTER_SVC_HDR_SIZE && entry->dgram[0] == ETCP_RT_ID_TCP_PROXY_CLIENT) {
uint64_t peer_id;
@ -730,6 +715,18 @@ void tcp_proxy_client_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* en
stream_id, subcmd, entry->dgram + TCP_PROXY_RECV_HDR_SIZE, data_len)) {
queue_dgram_free(entry); queue_entry_free(entry); return;
}
struct tcp_proxy_client_conn* pc = tcp_proxy_client_find_conn(proxy, stream_id);
if (pc && subcmd == TCP_PROXY_SUBCMD_CONNECTED) {
pc->flow.ready = 1;
tcp_proxy_client_flow_wake(pc);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
if (pc && subcmd == TCP_PROXY_SUBCMD_WINDOW) {
if (proxy_flow_window(&pc->flow, entry->dgram + TCP_PROXY_RECV_HDR_SIZE, data_len) < 0)
tcp_proxy_client_handle_error(proxy, stream_id);
else tcp_proxy_client_flow_wake(pc);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
// Существующие lwIP conns
if (subcmd == TCP_PROXY_SUBCMD_DATA) { tcp_proxy_client_handle_data(proxy, conn, stream_id, entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_CLOSE) { tcp_proxy_client_handle_close(proxy, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
@ -825,8 +822,7 @@ void tcp_proxy_client_destroy(struct tcp_proxy_client* p) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy client destroying: lwip=%d socks=%d http=%d",
p->conn_count, p->socks_conn_count, p->http_conn_count);
if (p->inst) etcp_router_unbind(p->inst, ETCP_RT_ID_TCP_PROXY_CLIENT);
udp_proxy_destroy(p->inst);
icmp_proxy_destroy(p->inst);
if (p->inst && !p->inst->tcp_proxy_server.enabled) { udp_proxy_destroy(p->inst); icmp_proxy_destroy(p->inst); }
if (p->socks_listen) socks_proxy_close_listen(p->ua, p->socks_listen, NULL);
socks_proxy_conn_free_all(&p->socks_conns, &p->socks_conn_count);
@ -837,6 +833,7 @@ void tcp_proxy_client_destroy(struct tcp_proxy_client* p) {
struct tcp_proxy_client_conn* pc = p->conns;
while (pc) {
struct tcp_proxy_client_conn* next = pc->next;
proxy_flow_destroy(&pc->flow);
if (pc->pcb && !memory_pool_is_freed(p->lwip->pcb_pool, pc->pcb) && pc->pcb->state != CLOSED) {
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);

2
src/proxy/tcp_proxy_client.h

@ -8,6 +8,7 @@ extern "C" {
#include <stdint.h>
#include "proxy_protocol.h"
#include <stddef.h>
#include "../lib/socket_compat.h"
#include "../lib/ll_queue.h"
@ -32,6 +33,7 @@ struct tcp_proxy_client_conn {
struct tcp_proxy_client* proxy;
struct tcp_pcb* pcb;
struct proxy_flow flow;
uint32_t stream_id;
struct ll_queue* to_lwip; // DATA от exit → lwIP (управление потоком)

296
src/proxy/tcp_proxy_server.c

@ -26,20 +26,19 @@
#include <fcntl.h>
#endif
static struct tcp_proxy_server* g_tcp_proxy_server_ctx = NULL;
static void server_wake(void* arg);
static void on_connected_cb(struct tcp_conn* tc, void* arg);
static void on_fin_cb(struct tcp_conn* tc, void* arg);
static void on_error_cb(struct tcp_conn* tc, int err, void* arg);
static void on_flushed_cb(struct tcp_conn* tc, void* arg);
static void on_closed_cb(struct tcp_conn* tc, void* arg);
static void read_queue_drain_cb(struct ll_queue* q, void* arg);
static void pause_resume_cb(struct ll_queue* q, void* arg);
static void close_retry_cb(void* arg);
static void diag_timer_cb(void* arg);
static void retry_timer_cb(void* arg);
static int send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd,
uint32_t sid, const uint8_t* data, size_t len, int force);
static void send_close(struct tcp_proxy_server_conn* rc);
void tcp_proxy_server_conn_free(struct tcp_proxy_server_conn* rc);
static inline int write_pending(struct tcp_conn* tc) {
@ -52,94 +51,50 @@ static inline int write_pending(struct tcp_conn* tc) {
static int send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd,
uint32_t sid, const uint8_t* data, size_t len, int force) {
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_server: queue_entry_new failed subcmd=%02x sid=%08x", subcmd, sid); return -1; }
e->dgram = u_malloc(TCP_PROXY_HDR_SIZE + len);
if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_server: malloc(%zu) failed", TCP_PROXY_HDR_SIZE + len); queue_entry_free(e); return -1; }
e->dgram[0] = ETCP_RT_ID_TCP_PROXY_CLIENT;
e->dgram[1] = subcmd;
memcpy(e->dgram + 2, &sid, 4);
if (len > 0) memcpy(e->dgram + TCP_PROXY_HDR_SIZE, data, len);
if (TCP_PROXY_HDR_SIZE + len > UINT16_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_server: msg too large len=%zu subcmd=%02x", len, subcmd);
queue_dgram_free(e); queue_entry_free(e); return -1;
}
e->len = TCP_PROXY_HDR_SIZE + len;
return etcp_route_send(inst, group_id, dst, e, force, 0);
(void)group_id;
return proxy_send(inst, dst, ETCP_RT_ID_TCP_PROXY_CLIENT, subcmd, sid, data, len, force);
}
static void close_retry_cb(void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
if (!rc) return;
rc->close_timer = NULL;
if (!rc->close_pending) return;
struct UTUN_INSTANCE* inst = rc->ctx ? rc->ctx->inst : NULL;
if (!inst) return;
uint8_t subcmd = (rc->tc && rc->tc->error) ? TCP_PROXY_SUBCMD_ERROR : TCP_PROXY_SUBCMD_CLOSE;
if (send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, subcmd, rc->stream_id, NULL, 0, 1) < 0) {
rc->close_backoff = rc->close_backoff < 5000 ? rc->close_backoff * 2 : 5000;
rc->close_timer = uasync_set_timeout(rc->ua, rc->close_backoff, rc, close_retry_cb, "tps_close_retry");
return;
// FIN относится только к прочитанному направлению: сначала передаём весь read_queue.
static void server_maybe_finish(struct tcp_proxy_server_conn* rc) {
struct tcp_conn* tc = rc->tc;
if (!tc || rc->cli_closed || rc->close_queued) return;
if (tc->fin_remote && !tc->read_queue->head && !rc->flow.fin_sent) {
rc->flow.fin_pending = 1;
proxy_flow_flush(&rc->flow);
}
rc->close_pending = 0;
}
static void send_close(struct tcp_proxy_server_conn* rc) {
struct UTUN_INSTANCE* inst = rc->ctx ? rc->ctx->inst : NULL;
if (!inst) return;
if (send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, TCP_PROXY_SUBCMD_CLOSE, rc->stream_id, NULL, 0, 1) < 0) {
rc->close_pending = 1;
rc->close_backoff = 50;
if (!rc->close_timer)
rc->close_timer = uasync_set_timeout(rc->ua, rc->close_backoff, rc, close_retry_cb, "tps_close_retry");
if (rc->flow.fin_sent && rc->flow.fin_received && tc->fin_local) {
if (tcp_conn_push_close(tc) == 0) rc->close_queued = 1;
}
}
static void send_fin(struct tcp_proxy_server_conn* rc) {
struct UTUN_INSTANCE* inst = rc->ctx ? rc->ctx->inst : NULL;
if (!inst) return;
send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, TCP_PROXY_SUBCMD_FIN, rc->stream_id, NULL, 0, 1);
}
// ====================================================================
// Коллбэки tcp_io
// ====================================================================
static void on_fin_cb(struct tcp_conn* tc, void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
if (rc->cli_closed) return;
int pend_w = write_pending(tc);
int pend_r = (rc->tx_buf != NULL) || (tc->read_queue && tc->read_queue->head);
DEBUG_INFO(DEBUG_CATEGORY_PROXY,
"SOCK:DST_FIN fd=%d sid=%08x fin_l=%d pend_w=%d pend_r=%d "
"sent=%u recv=%u relayed=%u drain#=%u data#=%u "
"rq=%d(%zub) wq=%d(%zub) wbuf=%s err=%d tx_buf=%s",
(int)tc->sock, rc->stream_id, tc->fin_local, pend_w, pend_r,
rc->bytes_sent, rc->bytes_recv, rc->bytes_relayed,
rc->drain_count, rc->data_count,
tc->read_queue->count, queue_total_bytes(tc->read_queue),
tc->write_queue->count, queue_total_bytes(tc->write_queue),
tc->write_buf ? "y" : "n", tc->error, rc->tx_buf ? "y" : "n");
if (tc->fin_local) {
send_close(rc); tcp_conn_push_close(tc);
} else if (pend_w) {
tcp_conn_set_flushed(tc, on_flushed_cb);
} else if (pend_r) {
rc->dst_fin_deferred = 1;
} else {
send_fin(rc);
}
(void)tc;
server_maybe_finish(arg);
}
// Кредит возвращается после записи всей принятой порции в OS TCP, включая write_buf.
static void on_flushed_cb(struct tcp_conn* tc, void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
struct tcp_proxy_server_conn* rc = arg;
if (rc->cli_closed) return;
if (tc->fin_local) {
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:FLUSHED fd=%d sid=%08x fin_local=%d — both FINs, closing",
(int)tc->sock, rc->stream_id, tc->fin_local);
send_close(rc); tcp_conn_push_close(tc); return;
}
send_fin(rc);
if (!write_pending(tc)) proxy_flow_consume(&rc->flow, TCP_PROXY_WINDOW - rc->flow.rx_credit - rc->flow.consumed);
server_maybe_finish(rc);
}
static void on_connected_cb(struct tcp_conn* tc, void* arg) {
struct tcp_proxy_server_conn* rc = arg;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "proxy exit connected peer=%016llx sid=%08x fd=%d",
(unsigned long long)rc->peer_node_id, rc->stream_id, (int)tc->sock);
rc->flow.connected_pending = 1;
proxy_flow_flush(&rc->flow);
server_wake(rc);
}
static void server_wake(void* arg) {
struct tcp_proxy_server_conn* rc = arg;
if (!rc->tc || rc->cli_closed) return;
queue_resume_callback(rc->tc->read_queue);
server_maybe_finish(rc);
}
static void on_closed_cb(struct tcp_conn* tc, void* arg) {
@ -155,11 +110,11 @@ static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
DEBUG_ERROR(DEBUG_CATEGORY_PROXY,
"SOCK:ERR fd=%d sid=%08x err=%d fin_r=%d fin_l=%d "
"sent=%u recv=%u relayed=%u drain#=%u data#=%u "
"sent=%u relayed=%u drain#=%u data#=%u "
"rq=%d wq=%d wbuf=%s conn=%d",
tc ? (int)tc->sock : -1, rc->stream_id, err,
tc ? tc->fin_remote : 0, tc ? tc->fin_local : 0,
rc->bytes_sent, rc->bytes_recv, rc->bytes_relayed,
rc->bytes_sent, rc->bytes_relayed,
rc->drain_count, rc->data_count,
tc ? tc->read_queue->count : 0, tc ? tc->write_queue->count : 0,
tc && tc->write_buf ? "y" : "n", tc ? tc->connected : 0);
@ -203,99 +158,36 @@ static void diag_timer_cb(void* arg) {
// ====================================================================
static void read_queue_drain_cb(struct ll_queue* q, void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
struct UTUN_INSTANCE* inst = rc->ctx ? rc->ctx->inst : NULL;
struct tcp_conn* tc = rc->tc;
struct ll_entry* e;
if (rc->tx_buf) {
int ret = send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, TCP_PROXY_SUBCMD_DATA, rc->stream_id, rc->tx_buf, rc->tx_len, 0);
if (!tc->read_queue) { u_free(rc->tx_buf); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "TPS TXBUF RETRY: sid=%08x len=%u ret=%d", rc->stream_id, rc->tx_len, ret);
if (ret == 0) {
u_free(rc->tx_buf); rc->tx_buf = NULL; rc->tx_len = 0;
if (rc->retry_timer) { uasync_cancel_timeout(rc->ua, rc->retry_timer); rc->retry_timer = NULL; }
if (rc->dst_fin_deferred && !q->head) {
rc->dst_fin_deferred = 0;
send_fin(rc);
return;
}
} else {
etcp_router_on_send_ready(inst, TOPO_GROUP_UTUN, rc->peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT,
&rc->pause_waiter, pause_resume_cb, rc);
if (!rc->retry_timer) rc->retry_timer = uasync_set_timeout(rc->ua, 5000, rc, retry_timer_cb, "tps_retry");
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "TPS TXBUF BP AGAIN: sid=%08x waiter_reg", rc->stream_id);
return;
}
}
e = queue_data_get(q);
if (!e) {
if (rc->dst_fin_deferred) {
rc->dst_fin_deferred = 0;
send_fin(rc);
}
queue_resume_callback(q); return;
}
if (rc->cli_closed) {
do {
memory_pool_free(tc->data_pool, e->dgram);
queue_entry_free(e);
e = queue_data_get(q);
} while (e);
queue_resume_callback(q);
return;
}
int ret = send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, TCP_PROXY_SUBCMD_DATA, rc->stream_id, e->dgram, e->len, 0);
if (!tc->read_queue) { memory_pool_free(tc->data_pool, e->dgram); queue_entry_free(e); return; }
rc->bytes_relayed += e->len; rc->drain_count++;
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "TPS DRAIN #%u sid=%08x len=%u → ret=%d rq=%d",
rc->drain_count, rc->stream_id, e->len, ret, q->count);
struct tcp_proxy_server_conn* rc = arg;
if (!rc->tc || rc->cli_closed) return;
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); server_maybe_finish(rc); return; }
int ret = proxy_flow_send(&rc->flow, e->dgram, e->len, 0);
if (ret == 0) {
memory_pool_free(tc->data_pool, e->dgram); queue_entry_free(e);
rc->bytes_relayed += e->len; rc->drain_count++;
memory_pool_free(rc->tc->data_pool, e->dgram); queue_entry_free(e);
queue_resume_callback(q);
} else {
rc->tx_buf = u_malloc(e->len);
if (rc->tx_buf) { memcpy(rc->tx_buf, e->dgram, e->len); rc->tx_len = e->len; }
else { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TPS tx_buf malloc=%u failed sid=%08x — drop", e->len, rc->stream_id); }
memory_pool_free(tc->data_pool, e->dgram); queue_entry_free(e);
if (!tc->read_queue) return;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:BACKPRESSURE fd=%d sid=%08x — buffered %u for retry",
(int)tc->sock, rc->stream_id, rc->tx_len);
etcp_router_on_send_ready(inst, TOPO_GROUP_UTUN, rc->peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT,
&rc->pause_waiter, pause_resume_cb, rc);
if (!rc->retry_timer) rc->retry_timer = uasync_set_timeout(rc->ua, 5000, rc, retry_timer_cb, "tps_retry");
server_maybe_finish(rc);
return;
}
queue_data_put_first(q, e);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy exit: paused sid=%08x ret=%d credit=%u queued=%zu",
rc->stream_id, ret, rc->flow.tx_credit, queue_total_bytes(q));
if (ret != -2)
etcp_router_on_send_ready(rc->ctx->inst, TOPO_GROUP_UTUN, rc->peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT,
&rc->pause_waiter, pause_resume_cb, rc);
if (!rc->retry_timer) rc->retry_timer = uasync_set_timeout(rc->ua, 5000, rc, retry_timer_cb, "tps_retry");
}
static void pause_resume_cb(struct ll_queue* q, void* arg) {
(void)q;
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
if (!rc->tc || rc->tc->sock == SOCKET_INVALID || rc->cli_closed) return;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "TPS RESUME sid=%08x tx_buf=%s rq=%d",
rc->stream_id, rc->tx_buf ? "y" : "n",
rc->tc->read_queue ? rc->tc->read_queue->count : 0);
queue_resume_callback(rc->tc->read_queue);
server_wake(arg);
}
static void retry_timer_cb(void* arg) {
struct tcp_proxy_server_conn* rc = (struct tcp_proxy_server_conn*)arg;
struct tcp_proxy_server_conn* rc = arg;
rc->retry_timer = NULL;
if (!rc->tx_buf || !rc->tc || rc->tc->sock == SOCKET_INVALID || rc->cli_closed) return;
struct UTUN_INSTANCE* inst = rc->ctx ? rc->ctx->inst : NULL;
int ret = send_msg(inst, TOPO_GROUP_UTUN, rc->peer_node_id, TCP_PROXY_SUBCMD_DATA, rc->stream_id, rc->tx_buf, rc->tx_len, 1);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "TPS RETRY TIMER: sid=%08x len=%u force=1 ret=%d", rc->stream_id, rc->tx_len, ret);
if (ret == 0) {
u_free(rc->tx_buf); rc->tx_buf = NULL; rc->tx_len = 0;
if (rc->dst_fin_deferred && rc->tc->read_queue && !rc->tc->read_queue->head) {
rc->dst_fin_deferred = 0;
send_fin(rc);
return;
}
queue_resume_callback(rc->tc->read_queue);
} else {
rc->retry_timer = uasync_set_timeout(rc->ua, 5000, rc, retry_timer_cb, "tps_retry");
}
server_wake(rc);
}
// ====================================================================
@ -320,28 +212,22 @@ void tcp_proxy_server_conn_free(struct tcp_proxy_server_conn* rc) {
int total = conn_total(rc);
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:FREE fd=%d sid=%08x total=%d cli_closed=%d fin=%d error=%d",
rc->tc ? (int)rc->tc->sock : -1, rc->stream_id, total, rc->cli_closed, rc->tc ? rc->tc->fin_remote : 0, rc->tc ? rc->tc->error : 0);
if (rc->close_pending && rc->ctx && rc->ctx->inst) {
rc->close_pending = 0;
uint8_t subcmd = (rc->tc && rc->tc->error) ? TCP_PROXY_SUBCMD_ERROR : TCP_PROXY_SUBCMD_CLOSE;
send_msg(rc->ctx->inst, TOPO_GROUP_UTUN, rc->peer_node_id, subcmd, rc->stream_id, NULL, 0, 1);
}
if (rc->ctx) {
struct tcp_proxy_server_conn** prev = &rc->ctx->conns;
while (*prev) { if (*prev == rc) { *prev = rc->next; rc->ctx->conn_count--; break; } prev = &(*prev)->next; }
}
if (rc->ctx && rc->ctx->inst) etcp_router_cancel_send_ready(rc->ctx->inst, TOPO_GROUP_UTUN, rc->peer_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT, &rc->pause_waiter);
if (rc->tx_buf) { u_free(rc->tx_buf); rc->tx_buf = NULL; rc->tx_len = 0; }
proxy_flow_destroy(&rc->flow);
if (rc->retry_timer) { uasync_cancel_timeout(rc->ua, rc->retry_timer); rc->retry_timer = NULL; }
if (rc->close_timer) { uasync_cancel_timeout(rc->ua, rc->close_timer); rc->close_timer = NULL; }
if (rc->diag_timer) { uasync_cancel_timeout(rc->ua, rc->diag_timer); rc->diag_timer = NULL; }
if (rc->tc) { tcp_conn_destroy(rc->tc); rc->tc = NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:FREE u_free rc=%p", (void*)rc);
u_free(rc);
}
struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server* ctx, uint32_t stream_id) {
struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server* ctx, uint64_t peer, uint32_t stream_id) {
struct tcp_proxy_server_conn* c;
for (c = ctx->conns; c; c = c->next) if (c->stream_id == stream_id) return c;
for (c = ctx->conns; c; c = c->next) if (c->peer_node_id == peer && c->stream_id == stream_id) return c;
return NULL;
}
@ -352,6 +238,11 @@ struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server
int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry* entry, uint32_t stream_id, uint64_t src_node_id) {
if (!inst || !inst->tcp_proxy_server.enabled) { if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return -1; }
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
if (tcp_proxy_server_find_conn(ctx, src_node_id, stream_id)) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "proxy exit: duplicate CONNECT peer=%016llx sid=%08x", (unsigned long long)src_node_id, stream_id);
send_msg(inst, TOPO_GROUP_UTUN, src_node_id, TCP_PROXY_SUBCMD_ERROR, stream_id, NULL, 0, 1);
queue_dgram_free(entry); queue_entry_free(entry); return -1;
}
if (entry->len < TCP_PROXY_RECV_HDR_SIZE + 6) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: CONNECT too short len=%u", entry->len); queue_dgram_free(entry); queue_entry_free(entry); return -1; }
uint8_t* dest_ip = entry->dgram + TCP_PROXY_RECV_HDR_SIZE;
uint16_t dest_port = 0; memcpy(&dest_port, dest_ip + 4, 2);
@ -361,6 +252,7 @@ int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry*
rc->ctx = ctx; rc->stream_id = stream_id; rc->peer_node_id = src_node_id;
memcpy(rc->dest_ip, dest_ip, 4); rc->dest_port = dest_port;
rc->ua = inst->ua;
proxy_flow_init(&rc->flow, inst, inst->ua, src_node_id, ETCP_RT_ID_TCP_PROXY_CLIENT, stream_id, server_wake, rc);
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: socket() failed"); u_free(rc); queue_dgram_free(entry); queue_entry_free(entry); return -1; }
@ -399,6 +291,8 @@ int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry*
rc->tc = tcp_conn_create(inst->ua, sock, 1500, 8192, 4, 0, inst->config->global.tcp_recv_buf, on_fin_cb, on_error_cb, rc);
if (!rc->tc) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy server: tcp_conn_create failed"); socket_close_wrapper(sock); ctx->conn_count--; u_free(rc); queue_dgram_free(entry); queue_entry_free(entry); return -1; }
rc->tc->on_closed = on_closed_cb;
rc->tc->on_connected = on_connected_cb;
rc->tc->on_fin_sent = on_fin_cb;
queue_set_callback(rc->tc->read_queue, read_queue_drain_cb, rc);
queue_set_waiter_defer(rc->tc->read_queue, 1);
@ -414,6 +308,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); }
tcp_conn_set_connect_timeout(rc->tc, g->tcp_proxy_server_connect_timeout_ms);
rc->diag_timer = uasync_set_timeout(rc->ua, 10000, rc, diag_timer_cb, "tps_diag");
@ -423,9 +318,10 @@ int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry*
int tcp_proxy_server_handle_data(struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn, struct ll_entry* entry, uint32_t stream_id) {
(void)conn;
uint64_t peer; memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return -1; }
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, stream_id);
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, peer, stream_id);
if (!rc || !rc->tc || rc->tc->sock == SOCKET_INVALID || rc->tc->error || rc->cli_closed) {
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TPS handle_data: no/closed conn sid=%08x, dropping", stream_id);
queue_dgram_free(entry); queue_entry_free(entry); return -1;
@ -434,6 +330,9 @@ int tcp_proxy_server_handle_data(struct UTUN_INSTANCE* inst, struct ETCP_CONN* c
rc->bytes_sent += (uint32_t)data_len; rc->data_count++;
DEBUG_TRACE(DEBUG_CATEGORY_PROXY, "TPS DATA #%u sid=%08x len=%zu",
rc->data_count, stream_id, data_len);
if (proxy_flow_receive(&rc->flow, data_len) < 0) {
on_error_cb(rc->tc, EPROTO, rc); queue_dgram_free(entry); queue_entry_free(entry); return -1;
}
if (data_len > 0) {
if (data_len > rc->tc->data_pool->object_size) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_server: data_len=%zu > pool_sz=%zu, dropping sid=%08x",
@ -445,21 +344,23 @@ int tcp_proxy_server_handle_data(struct UTUN_INSTANCE* inst, struct ETCP_CONN* c
if (e && buf) {
memcpy(buf, entry->dgram + TCP_PROXY_RECV_HDR_SIZE, data_len);
e->dgram = buf; e->len = (uint16_t)data_len;
tcp_conn_set_flushed(rc->tc, on_flushed_cb);
queue_data_put(rc->tc->write_queue, e);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "tcp_proxy_server: handle_data alloc failed sid=%08x", stream_id);
if (e) queue_entry_free(e);
if (buf) memory_pool_free(rc->tc->data_pool, buf);
on_error_cb(rc->tc, ENOMEM, rc);
}
}
queue_dgram_free(entry); queue_entry_free(entry);
return 0;
}
void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint32_t stream_id) {
void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id) {
if (!inst) return;
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, stream_id);
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, peer, stream_id);
if (!rc) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "TCP proxy server: CLOSE sid=%08x — no conn", stream_id); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:CLOSE_RECV fd=%d sid=%08x total=%d fin=%d write_pend=%d",
rc->tc ? (int)rc->tc->sock : -1, stream_id, conn_total(rc),
@ -475,28 +376,31 @@ void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint32_t stream_i
queue_entry_free(e);
}
}
if (rc->tc->connected) tcp_conn_push_close(rc->tc); else tcp_proxy_server_conn_free(rc);
if (rc->tc->connected) {
if (!rc->close_queued && tcp_conn_push_close(rc->tc) == 0) rc->close_queued = 1;
} else tcp_proxy_server_conn_free(rc);
}
void tcp_proxy_server_handle_error(struct UTUN_INSTANCE* inst, uint32_t stream_id) {
void tcp_proxy_server_handle_error(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id) {
if (!inst) return;
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, stream_id);
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, peer, stream_id);
if (!rc) { DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server: ERROR sid=%08x — no conn", stream_id); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:ERROR_RECV fd=%d sid=%08x total=%d fin=%d write_pend=%d",
rc->tc ? (int)rc->tc->sock : -1, stream_id, conn_total(rc),
rc->tc ? rc->tc->fin_remote : 0, rc->tc ? write_pending(rc->tc) : 0);
tcp_proxy_server_handle_close(inst, stream_id);
tcp_proxy_server_handle_close(inst, peer, stream_id);
}
void tcp_proxy_server_handle_fin(struct UTUN_INSTANCE* inst, uint32_t stream_id) {
void tcp_proxy_server_handle_fin(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id) {
if (!inst) return;
struct tcp_proxy_server* ctx = &inst->tcp_proxy_server;
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, stream_id);
if (!rc || !rc->tc || !rc->tc->connected) return;
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(ctx, peer, stream_id);
if (!rc || !rc->tc || rc->flow.fin_received) return;
rc->flow.fin_received = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "SOCK:FIN_RECV fd=%d sid=%08x — pushing FIN to wq",
(int)rc->tc->sock, stream_id);
tcp_conn_push_fin(rc->tc);
if (tcp_conn_push_fin(rc->tc) < 0) on_error_cb(rc->tc, ENOMEM, rc);
}
// ====================================================================
@ -530,6 +434,7 @@ void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
queue_dgram_free(entry); queue_entry_free(entry);
return;
}
uint64_t peer; memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
uint8_t subcmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF];
uint32_t stream_id; memcpy(&stream_id, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, 4);
@ -542,12 +447,18 @@ void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
return;
}
if (inst && inst->tcp_proxy_server.enabled) {
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(&inst->tcp_proxy_server, stream_id);
struct tcp_proxy_server_conn* rc = tcp_proxy_server_find_conn(&inst->tcp_proxy_server, peer, stream_id);
if (rc) {
if (subcmd == TCP_PROXY_SUBCMD_WINDOW) {
if (proxy_flow_window(&rc->flow, entry->dgram + TCP_PROXY_RECV_HDR_SIZE, entry->len - TCP_PROXY_RECV_HDR_SIZE) < 0)
on_error_cb(rc->tc, EPROTO, rc);
else server_wake(rc);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
if (subcmd == TCP_PROXY_SUBCMD_DATA) { tcp_proxy_server_handle_data(inst, conn, entry, stream_id); return; }
if (subcmd == TCP_PROXY_SUBCMD_CLOSE) { tcp_proxy_server_handle_close(inst, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_ERROR) { tcp_proxy_server_handle_error(inst, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_FIN) { tcp_proxy_server_handle_fin(inst, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_CLOSE) { tcp_proxy_server_handle_close(inst, peer, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_ERROR) { tcp_proxy_server_handle_error(inst, peer, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
if (subcmd == TCP_PROXY_SUBCMD_FIN) { tcp_proxy_server_handle_fin(inst, peer, stream_id); queue_dgram_free(entry); queue_entry_free(entry); return; }
}
}
if (subcmd == TCP_PROXY_SUBCMD_DATA || subcmd == TCP_PROXY_SUBCMD_CLOSE || subcmd == TCP_PROXY_SUBCMD_FIN || subcmd == TCP_PROXY_SUBCMD_ERROR) {
@ -570,12 +481,9 @@ int tcp_proxy_server_init(struct UTUN_INSTANCE* inst) {
ctx->enabled = inst->config->global.tcp_proxy_server_enabled;
ctx->inst = inst;
if (!ctx->enabled) return 0;
g_tcp_proxy_server_ctx = ctx;
etcp_router_bind(inst, ETCP_RT_ID_TCP_PROXY_SERVER, tcp_proxy_server_recv_cb);
if (!inst->config->global.tcp_proxy_client_enabled) {
udp_proxy_init(inst, inst->ua);
icmp_proxy_init(inst, inst->ua);
}
udp_proxy_init(inst, inst->ua);
icmp_proxy_init(inst, inst->ua);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server initialized node=%016llx", (unsigned long long)inst->node_id);
return 0;
}
@ -587,7 +495,7 @@ void tcp_proxy_server_destroy(struct UTUN_INSTANCE* inst) {
struct tcp_proxy_server_conn* rc = ctx->conns;
while (rc) { struct tcp_proxy_server_conn* next = rc->next; tcp_proxy_server_conn_free(rc); rc = next; }
ctx->conns = NULL; ctx->enabled = 0;
if (g_tcp_proxy_server_ctx == ctx) g_tcp_proxy_server_ctx = NULL;
etcp_router_unbind(inst, ETCP_RT_ID_TCP_PROXY_SERVER);
udp_proxy_destroy(inst);
icmp_proxy_destroy(inst);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy server destroyed");

29
src/proxy/tcp_proxy_server.h

@ -18,42 +18,27 @@ struct UASYNC;
struct ll_entry;
struct ETCP_CONN;
#define TCP_PROXY_SUBCMD_CONNECT 0x01
#define TCP_PROXY_SUBCMD_DATA 0x03
#define TCP_PROXY_SUBCMD_CLOSE 0x04
#define TCP_PROXY_SUBCMD_ERROR 0x05
#define TCP_PROXY_SUBCMD_FIN 0x06
#define TCP_PROXY_HDR_SIZE 6 // svc_id(1)+subcmd(1)+stream_id(4)
#define TCP_PROXY_CONNECT_HDR_SIZE 12 // HDR_SIZE + dest_ip(4)+dest_port(2)
// recv-формат (после router_deliver): [svc_id][src][dst][subcmd][stream_id][data]
#define TCP_PROXY_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 5) // 22: до data (subcmd+stream_id)
#include "proxy_protocol.h"
struct tcp_proxy_server_conn {
struct tcp_proxy_server_conn* next;
struct tcp_proxy_server* ctx;
struct tcp_conn* tc;
struct proxy_flow flow;
uint32_t stream_id;
uint64_t peer_node_id;
uint8_t dest_ip[4];
uint16_t dest_port;
uint8_t close_queued;
uint8_t cli_closed; // client sent CLOSE
uint8_t close_pending; // CLOSE/ERROR не доставлен, ретрай
void* close_timer; // таймер повтора CLOSE/ERROR
int close_backoff; // backoff: 50..5000 tb (5ms..500ms)
void* diag_timer; // 1-секундный таймер диагностики
uint8_t dst_fin_deferred; // FIN от destination отложен (ждём drain read_queue + tx_buf)
uint8_t freed; // 1 = уже освобождён, защита от double-free
struct queue_waiter_handle pause_waiter;
void* retry_timer; // fallback timer (500ms) for force=1 retry on stall
uint8_t* tx_buf; // буфер при backpressure (retry в pause_resume_cb)
uint16_t tx_len;
uint32_t bytes_sent; // байт записано в destination (реальный TCP)
uint32_t bytes_recv; // байт прочитано от destination
uint32_t bytes_relayed; // байт отправлено клиенту через ETCP
uint32_t drain_count; // счётчик вызовов read_queue_drain_cb
uint32_t data_count; // счётчик входящих DATA от клиента
@ -70,7 +55,7 @@ struct tcp_proxy_server {
int tcp_proxy_server_init(struct UTUN_INSTANCE* inst);
void tcp_proxy_server_destroy(struct UTUN_INSTANCE* inst);
struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server* ctx, uint32_t stream_id);
struct tcp_proxy_server_conn* tcp_proxy_server_find_conn(struct tcp_proxy_server* ctx, uint64_t peer, uint32_t stream_id);
void tcp_proxy_server_conn_free(struct tcp_proxy_server_conn* rc);
void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
@ -78,9 +63,9 @@ void tcp_proxy_server_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
int tcp_proxy_server_handle_connect(struct UTUN_INSTANCE* inst, struct ll_entry* entry,
uint32_t stream_id, uint64_t src_node_id);
int tcp_proxy_server_handle_data(struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn, struct ll_entry* entry, uint32_t stream_id);
void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint32_t stream_id);
void tcp_proxy_server_handle_error(struct UTUN_INSTANCE* inst, uint32_t stream_id);
void tcp_proxy_server_handle_fin(struct UTUN_INSTANCE* inst, uint32_t stream_id);
void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
void tcp_proxy_server_handle_error(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
void tcp_proxy_server_handle_fin(struct UTUN_INSTANCE* inst, uint64_t peer, uint32_t stream_id);
#ifdef __cplusplus

69
src/proxy/udp_proxy.c

@ -23,7 +23,6 @@
#define UDP_FLOW_TIMEOUT_TB 600000 // 60с
struct udp_proxy_ctx* g_udp_ctx = NULL;
static void flow_expire_timer_cb(void* arg);
static void flow_expire(struct udp_proxy_ctx* ctx);
@ -40,11 +39,15 @@ static struct udp_flow* flow_find(struct udp_flow* head, uint64_t client_node_id
static void flow_read_cb(socket_t sock, void* arg) {
(void)sock; struct udp_flow* f = (struct udp_flow*)arg;
if (!f || f->sock == SOCKET_INVALID || !g_udp_ctx) return;
uint8_t buf[1600];
struct udp_proxy_ctx* ctx = f ? f->ctx : NULL;
if (!f || f->sock == SOCKET_INVALID || !ctx) return;
uint8_t buf[65507];
struct sockaddr_in from; socklen_t flen = sizeof(from);
ssize_t n = recvfrom(f->sock, buf, sizeof(buf), 0, (struct sockaddr*)&from, &flen);
if (n <= 0) return;
if (n < 0) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: recv failed errno=%d", errno); return; }
if (from.sin_addr.s_addr != f->dst_ip || from.sin_port != f->dst_port) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: unexpected reply source"); return;
}
f->last_activity_tb = get_time_tb();
@ -54,7 +57,7 @@ static void flow_read_cb(socket_t sock, void* arg) {
e->dgram = u_malloc(UDP_PROXY_HDR_SIZE + n);
if (!e->dgram) { queue_entry_free(e); return; }
e->dgram[0] = ETCP_RT_ID_UDP_PROXY;
e->dgram[1] = UDP_PROXY_SUBCMD_DATA;
e->dgram[1] = UDP_PROXY_SUBCMD_REPLY;
// Ответ src = оригинальный dst_ip:dest_port
memcpy(e->dgram + 2, &f->dst_ip, 4);
memcpy(e->dgram + 6, &f->dst_port, 2);
@ -64,15 +67,16 @@ static void flow_read_cb(socket_t sock, void* arg) {
memcpy(e->dgram + UDP_PROXY_HDR_SIZE, buf, n);
e->len = UDP_PROXY_HDR_SIZE + n;
etcp_route_send(g_udp_ctx->inst, TOPO_GROUP_UTUN, f->client_node_id, e, 0, 0);
etcp_route_send(ctx->inst, TOPO_GROUP_UTUN, f->client_node_id, e, 0, 0);
}
// ====================================================================
// Exit узел: принять REQUEST, создать сокет, переслать
// ====================================================================
static void exit_handle_data(struct ETCP_CONN* conn, struct ll_entry* entry) {
struct UTUN_INSTANCE* inst = conn ? conn->instance : (g_udp_ctx ? g_udp_ctx->inst : NULL);
if (!inst || !g_udp_ctx || entry->len < UDP_PROXY_RECV_HDR_SIZE + 1) goto drop;
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
struct udp_proxy_ctx* ctx = inst ? inst->udp_proxy : NULL;
if (!inst || !ctx || !inst->tcp_proxy_server.enabled || entry->len < UDP_PROXY_RECV_HDR_SIZE) goto drop;
uint64_t client_node_id; memcpy(&client_node_id, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
uint32_t src_ip; memcpy(&src_ip, entry->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, 4);
@ -82,24 +86,25 @@ static void exit_handle_data(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint8_t* payload = entry->dgram + UDP_PROXY_RECV_HDR_SIZE;
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
struct udp_flow* f = flow_find(g_udp_ctx->flows, client_node_id, src_ip, src_port, dst_ip, dst_port);
struct udp_flow* f = flow_find(ctx->flows, client_node_id, src_ip, src_port, dst_ip, dst_port);
if (!f) {
f = u_calloc(1, sizeof(struct udp_flow));
if (!f) goto drop;
f->ctx = ctx;
f->client_node_id = client_node_id; f->src_ip = src_ip; f->src_port = src_port;
f->dst_ip = dst_ip; f->dst_port = dst_port;
f->ua = g_udp_ctx->ua; f->created_tb = get_time_tb(); f->last_activity_tb = f->created_tb;
f->ua = ctx->ua; f->created_tb = get_time_tb(); f->last_activity_tb = f->created_tb;
f->sock = socket(AF_INET, SOCK_DGRAM, 0);
if (f->sock == SOCKET_INVALID) { u_free(f); DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "udp_proxy: socket failed"); goto drop; }
socket_set_nonblocking(f->sock);
struct sockaddr_in bind_addr = {.sin_family = AF_INET, .sin_addr = {.s_addr = INADDR_ANY}, .sin_port = 0};
bind(f->sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr));
f->read_id = uasync_add_socket_t(g_udp_ctx->ua, f->sock, flow_read_cb, NULL, NULL, "udp_flow", f);
f->read_id = uasync_add_socket_t(ctx->ua, f->sock, flow_read_cb, NULL, NULL, "udp_flow", f);
if (!f->read_id) { socket_close_wrapper(f->sock); u_free(f); goto drop; }
f->next = g_udp_ctx->flows; g_udp_ctx->flows = f; g_udp_ctx->flow_count++;
if (!g_udp_ctx->expire_timer)
g_udp_ctx->expire_timer = uasync_set_timeout(g_udp_ctx->ua, g_udp_ctx->flow_timeout_tb, g_udp_ctx, flow_expire_timer_cb, "udp_expire");
f->next = ctx->flows; ctx->flows = f; ctx->flow_count++;
if (!ctx->expire_timer)
ctx->expire_timer = uasync_set_timeout(ctx->ua, ctx->flow_timeout_tb, ctx, flow_expire_timer_cb, "udp_expire");
}
f->last_activity_tb = get_time_tb();
@ -114,7 +119,7 @@ drop:
// Сторона клиента: принять REPLY, доставить в TUN
// ====================================================================
static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry->len < UDP_PROXY_RECV_HDR_SIZE + 1) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (entry->len < UDP_PROXY_RECV_HDR_SIZE) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint32_t src_ip, dst_ip;
uint16_t src_port, dst_port;
@ -126,7 +131,7 @@ static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry)
uint8_t* payload = d + UDP_PROXY_RECV_HDR_SIZE;
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
struct UTUN_INSTANCE* inst = conn ? conn->instance : (g_udp_ctx ? g_udp_ctx->inst : NULL);
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
udp_proxy_deliver_reply(inst, src_ip, src_port, dst_ip, dst_port, payload, payload_len);
queue_dgram_free(entry); queue_entry_free(entry);
}
@ -135,16 +140,20 @@ static void client_handle_reply(struct ETCP_CONN* conn, struct ll_entry* entry)
// Единый etcp_router коллбэк
// ====================================================================
void udp_proxy_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_HDR_SIZE) {
if (!entry || !entry->dgram || entry->len <= ROUTER_SVC_HDR_SIZE) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL;
uint64_t peer; memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
uint8_t subcmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF];
if (subcmd == UDP_PROXY_SUBCMD_DATA) {
if (g_udp_ctx && g_udp_ctx->is_exit) exit_handle_data(conn, entry);
else client_handle_reply(conn, entry);
return;
if (subcmd == UDP_PROXY_SUBCMD_REQUEST && inst && inst->tcp_proxy_server.enabled) {
exit_handle_data(conn, entry); return;
}
if (subcmd == UDP_PROXY_SUBCMD_REPLY && inst && inst->tcp_proxy_client && peer == inst->tcp_proxy_client->via_node_id) {
client_handle_reply(conn, entry); return;
}
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: rejected subcmd=%u peer=%016llx", subcmd, (unsigned long long)peer);
queue_dgram_free(entry); queue_entry_free(entry);
}
@ -155,13 +164,13 @@ int udp_proxy_send_to_exit(struct UTUN_INSTANCE* inst, uint64_t exit_node_id,
uint32_t src_ip, uint16_t src_port,
uint32_t dst_ip, uint16_t dst_port,
const uint8_t* payload, size_t payload_len) {
if (!inst || !g_udp_ctx) return -1;
if (!inst || !inst->udp_proxy || payload_len > 65507) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "udp_proxy: invalid send"); return -1; }
struct ll_entry* e = queue_entry_new(0);
if (!e) return -1;
e->dgram = u_malloc(UDP_PROXY_HDR_SIZE + payload_len);
if (!e->dgram) { queue_entry_free(e); return -1; }
e->dgram[0] = ETCP_RT_ID_UDP_PROXY;
e->dgram[1] = UDP_PROXY_SUBCMD_DATA;
e->dgram[1] = UDP_PROXY_SUBCMD_REQUEST;
memcpy(e->dgram + 2, &src_ip, 4);
memcpy(e->dgram + 6, &src_port, 2);
memcpy(e->dgram + 8, &dst_ip, 4);
@ -237,13 +246,14 @@ static void flow_expire(struct udp_proxy_ctx* ctx) {
// ====================================================================
int udp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
if (!inst) return -1;
if (inst->udp_proxy) return 0;
struct udp_proxy_ctx* ctx = u_calloc(1, sizeof(struct udp_proxy_ctx));
if (!ctx) return -1;
ctx->inst = inst; ctx->ua = ua;
ctx->flow_timeout_tb = UDP_FLOW_TIMEOUT_TB;
ctx->expire_timer = NULL;
ctx->is_exit = inst->tcp_proxy_server.enabled;
g_udp_ctx = ctx;
inst->udp_proxy = ctx;
etcp_router_bind(inst, ETCP_RT_ID_UDP_PROXY, udp_proxy_recv_cb);
ctx->initialized = 1;
@ -252,13 +262,14 @@ int udp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
}
void udp_proxy_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || !g_udp_ctx) return;
struct udp_proxy_ctx* ctx = inst ? inst->udp_proxy : NULL;
if (!ctx) return;
etcp_router_unbind(inst, ETCP_RT_ID_UDP_PROXY);
if (g_udp_ctx->expire_timer) { uasync_cancel_timeout(g_udp_ctx->ua, g_udp_ctx->expire_timer); g_udp_ctx->expire_timer = NULL; }
struct udp_flow* f = g_udp_ctx->flows;
if (ctx->expire_timer) { uasync_cancel_timeout(ctx->ua, ctx->expire_timer); ctx->expire_timer = NULL; }
struct udp_flow* f = ctx->flows;
while (f) { struct udp_flow* n = f->next;
if (f->read_id) { uasync_remove_socket_t(g_udp_ctx->ua, f->sock); f->read_id = NULL; }
if (f->read_id) { uasync_remove_socket_t(ctx->ua, f->sock); f->read_id = NULL; }
socket_close_wrapper(f->sock); u_free(f); f = n; }
u_free(g_udp_ctx); g_udp_ctx = NULL;
u_free(ctx); inst->udp_proxy = NULL;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "udp_proxy destroyed");
}

5
src/proxy/udp_proxy.h

@ -17,7 +17,8 @@ struct ll_entry;
struct ETCP_CONN;
// Подкоманды
#define UDP_PROXY_SUBCMD_DATA 0x01 // client→exit: пробрось датаграмму | exit→client: ответ
#define UDP_PROXY_SUBCMD_REQUEST 0x01
#define UDP_PROXY_SUBCMD_REPLY 0x02
// Заголовок сообщения на отправке (включая байт svc_id; src/dst node_id добавляет роутер):
// svc_id(1) + subcmd(1) + src_ip(4) + src_port(2) + dst_ip(4) + dst_port(2) + payload
@ -29,6 +30,7 @@ struct ETCP_CONN;
// Один UDP поток (сторона exit узла: сопоставляет client_node_id + кортеж адресов с OS сокетом)
struct udp_flow {
struct udp_flow* next;
struct udp_proxy_ctx* ctx;
uint64_t client_node_id;
uint32_t src_ip;
uint16_t src_port;
@ -52,7 +54,6 @@ struct udp_proxy_ctx {
void* expire_timer; // периодический таймер очистки истёкших потоков
};
extern struct udp_proxy_ctx* g_udp_ctx;
int udp_proxy_init(struct UTUN_INSTANCE* inst, struct UASYNC* ua);
void udp_proxy_destroy(struct UTUN_INSTANCE* inst);

2
src/utun_instance.h

@ -248,6 +248,8 @@ struct UTUN_INSTANCE {
// TCP proxy server (exit node)
struct tcp_proxy_server tcp_proxy_server;
struct udp_proxy_ctx* udp_proxy;
struct icmp_proxy_ctx* icmp_proxy;
// Networks (queue of NETWORK_ENTRY, indexed by 56-bit id)
struct ll_queue* networks;

9
tests/Makefile.am

@ -50,6 +50,8 @@ check_PROGRAMS = \
test_tcp_proxy_server \
test_udp_proxy \
test_icmp_proxy \
test_proxy_packets \
test_proxy_regressions \
test_tcp_proxy_client \
test_socks_http_proxy \
test_socks_client \
@ -633,10 +635,15 @@ test_services_SOURCES = test_services.c
test_services_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_services_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
check_PROGRAMS += test_proxy_packets
# Граничные длины и checksum UDP/ICMP после восстановления пакета.
test_proxy_packets_SOURCES = test_proxy_packets.c
test_proxy_packets_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
# Управляемый транспорт для граничных состояний proxy.
test_proxy_regressions_SOURCES = test_proxy_regressions.c
test_proxy_regressions_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_proxy_regressions_LDFLAGS = -Wl,--wrap=etcp_route_send
check_PROGRAMS += test_tcp_io_flush
test_tcp_io_flush_SOURCES = test_tcp_io_flush.c
test_tcp_io_flush_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)

292
tests/test_proxy_regressions.c

@ -0,0 +1,292 @@
// Реальные локальные TCP-сокеты и управляемый транспорт: проверяем границы
// handshake, подтверждение connect, изоляцию потоков и порядок DATA/FIN.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "utun_instance.h"
#include "etcp_connections.h"
#include "etcp.h"
#include "proxy/socks_proxy.h"
#include "proxy/udp_proxy.h"
#include "proxy/icmp_proxy.h"
#include "lwip_tcp/lwip_tcp.h"
#include "tun_if.h"
#include "debug_config.h"
#include "mem.h"
#ifndef _WIN32
#include <unistd.h>
#else
#define SHUT_WR SD_SEND
#endif
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); exit(1); } } while (0)
struct message { struct UTUN_INSTANCE* inst; uint64_t peer; uint8_t svc, cmd; uint32_t sid; size_t len; uint8_t data[4096]; };
static struct message messages[512];
static unsigned message_count;
static struct UASYNC* ua;
// Перехватывается только транспорт. Парсеры, tcp_io, очереди и lifecycle — настоящие.
int __wrap_etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer,
struct ll_entry* entry, int force, int mode) {
(void)group; (void)force; (void)mode;
CHECK(message_count < 512 && entry->len >= 6 && entry->len <= 4102);
struct message* m = &messages[message_count++];
m->inst = inst; m->peer = peer; m->svc = entry->dgram[0]; m->cmd = entry->dgram[1];
memcpy(&m->sid, entry->dgram + 2, 4); m->len = entry->len - 6;
memcpy(m->data, entry->dgram + 6, m->len);
queue_dgram_free(entry); queue_entry_free(entry);
return 0;
}
static void pump(void) { for (int i = 0; i < 12; i++) uasync_poll(ua, 1); }
static socket_t listen_local(uint16_t* port) {
socket_t fd = socket(AF_INET, SOCK_STREAM, 0);
CHECK(fd != SOCKET_INVALID);
struct sockaddr_in a = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
CHECK(bind(fd, (struct sockaddr*)&a, sizeof(a)) == 0 && listen(fd, 8) == 0);
socklen_t len = sizeof(a); CHECK(getsockname(fd, (struct sockaddr*)&a, &len) == 0);
*port = ntohs(a.sin_port); socket_set_nonblocking(fd);
return fd;
}
static socket_t connect_local(uint16_t port) {
socket_t fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in a = {.sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
CHECK(connect(fd, (struct sockaddr*)&a, sizeof(a)) == 0);
socket_set_nonblocking(fd); pump(); return fd;
}
static void send_bytes(socket_t fd, const void* bytes, size_t len) {
CHECK(send(fd, bytes, len, 0) == (ssize_t)len); pump();
}
static unsigned count_cmd(uint8_t cmd) {
unsigned count = 0;
for (unsigned i = 0; i < message_count; i++) if (messages[i].cmd == cmd) count++;
return count;
}
static void deliver(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t svc, uint8_t cmd,
uint32_t sid, const void* data, size_t len) {
struct ll_entry* e = queue_entry_new(0);
CHECK(e);
e->len = cmd ? TCP_PROXY_RECV_HDR_SIZE + len : ROUTER_SVC_HDR_SIZE;
e->dgram = u_calloc(1, e->len); CHECK(e->dgram);
e->dgram[0] = svc;
uint64_t group = TOPO_GROUP_UTUN;
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &peer, 8);
memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8);
if (cmd) {
e->dgram[ROUTER_SVC_PAYLOAD_OFF] = cmd;
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, &sid, 4);
if (len) memcpy(e->dgram + TCP_PROXY_RECV_HDR_SIZE, data, len);
}
struct ETCP_CONN conn = {0}; conn.instance = inst;
if (svc == ETCP_RT_ID_TCP_PROXY_SERVER) tcp_proxy_server_recv_cb(&conn, e);
else tcp_proxy_client_router_recv_cb(&conn, e);
pump();
}
static void parser_tests(struct UTUN_INSTANCE* inst) {
uint16_t sp, hp;
socket_t reserved = listen_local(&sp); socket_close_wrapper(reserved);
reserved = listen_local(&hp); socket_close_wrapper(reserved);
char socks[64], http[64];
snprintf(socks, sizeof(socks), "127.0.0.1:%u", sp); snprintf(http, sizeof(http), "127.0.0.1:%u", hp);
struct tcp_proxy_client* p = tcp_proxy_client_create(inst, ua, NULL, NULL, 1500, 1, NULL, 0, 42, 1, socks, 1, http);
CHECK(p); inst->tcp_proxy_client = p;
socket_t fd = connect_local(sp);
// Greeting, CONNECT и ранние данные намеренно одним write.
const uint8_t request[] = {5,1,0, 5,1,0,1,127,0,0,1,0,80, 'a','b','c'};
message_count = 0; send_bytes(fd, request, sizeof(request));
CHECK(count_cmd(TCP_PROXY_SUBCMD_CONNECT) == 1 && count_cmd(TCP_PROXY_SUBCMD_DATA) == 0);
uint8_t reply[256]; CHECK(recv(fd, reply, sizeof(reply), 0) == 2 && reply[1] == 0);
uint32_t sid = p->socks_conns->stream_id;
deliver(inst, 99, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_CONNECTED, sid, NULL, 0);
deliver(inst, 99, ETCP_RT_ID_TCP_PROXY_CLIENT, 0, 0, NULL, 0);
CHECK(p->socks_conn_count == 1 && !p->socks_conns->flow.ready);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_CONNECTED, sid, NULL, 0);
CHECK(recv(fd, reply, sizeof(reply), 0) == 10 && reply[1] == 0);
CHECK(count_cmd(TCP_PROXY_SUBCMD_DATA) == 1 && messages[message_count-1].len == 3);
CHECK(memcmp(messages[message_count-1].data, "abc", 3) == 0);
// Закрытое окно: FIN обоих направлений не должен уничтожать ожидающую передачу.
p->socks_conns->flow.tx_credit = 0;
message_count = 0; send_bytes(fd, "tail", 4);
CHECK(shutdown(fd, SHUT_WR) == 0); pump();
CHECK(count_cmd(TCP_PROXY_SUBCMD_FIN) == 0);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_FIN, sid, NULL, 0);
CHECK(p->socks_conn_count == 1);
uint32_t credit = 4;
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_WINDOW, sid, &credit, 4);
CHECK(count_cmd(TCP_PROXY_SUBCMD_DATA) == 1 && count_cmd(TCP_PROXY_SUBCMD_FIN) == 1);
CHECK(messages[0].cmd == TCP_PROXY_SUBCMD_DATA && memcmp(messages[0].data, "tail", 4) == 0);
CHECK(p->socks_conn_count == 0); socket_close_wrapper(fd);
fd = connect_local(hp); message_count = 0;
const char first[] = "CONNECT 127.0.0.1:443 HTTP/1.1\r\n";
send_bytes(fd, first, sizeof(first)-1); CHECK(message_count == 0);
CHECK(recv(fd, reply, sizeof(reply), 0) < 0);
const char second[] = "Host: 127.0.0.1\r\n\r\nTLS";
send_bytes(fd, second, sizeof(second)-1); CHECK(count_cmd(TCP_PROXY_SUBCMD_CONNECT) == 1);
sid = p->http_conns->stream_id;
CHECK(recv(fd, reply, sizeof(reply), 0) < 0);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_CONNECTED, sid, NULL, 0);
ssize_t n = recv(fd, reply, sizeof(reply), 0); CHECK(n > 12 && memcmp(reply, "HTTP/1.1 200", 12) == 0);
CHECK(messages[message_count-1].len == 3 && memcmp(messages[message_count-1].data, "TLS", 3) == 0);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_ERROR, sid, NULL, 0);
socket_close_wrapper(fd);
fd = connect_local(hp); message_count = 0;
send_bytes(fd, first, sizeof(first)-1); send_bytes(fd, second, sizeof(second)-1);
sid = p->http_conns->stream_id;
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_ERROR, sid, NULL, 0);
n = recv(fd, reply, sizeof(reply), 0); CHECK(n > 12 && memcmp(reply, "HTTP/1.1 502", 12) == 0);
socket_close_wrapper(fd);
fd = connect_local(sp); message_count = 0;
const uint8_t auth[] = {5,1,2}; send_bytes(fd, auth, sizeof(auth));
CHECK(recv(fd, reply, sizeof(reply), 0) == 2 && reply[1] == 255 && message_count == 0);
socket_close_wrapper(fd);
fd = connect_local(sp); message_count = 0;
uint8_t v6[25] = {5,1,0,5,1,0,4}; v6[24] = 80;
send_bytes(fd, v6, sizeof(v6));
n = recv(fd, reply, sizeof(reply), 0); CHECK(n == 12 && reply[3] == 8 && message_count == 0);
socket_close_wrapper(fd);
// Большие заголовки и body, поступающий до CONNECTED, должны сохраниться и разбиться на DATA.
fd = connect_local(hp); message_count = 0;
char upload[32000], expected[32000];
int header = snprintf(upload, sizeof(upload), "POST http://127.0.0.1/upload HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Pad: ");
memset(upload + header, 'x', 9000); header += 9000;
header += snprintf(upload + header, sizeof(upload) - header, "\r\nContent-Length: 20000\r\n\r\n");
memset(upload + header, 'B', 20000);
for (int off = 0; off < header + 20000;) {
int chunk = header + 20000 - off; if (chunk > 1024) chunk = 1024;
send_bytes(fd, upload + off, chunk); off += chunk;
}
CHECK(count_cmd(TCP_PROXY_SUBCMD_CONNECT) == 1 && count_cmd(TCP_PROXY_SUBCMD_DATA) == 0);
sid = p->http_conns->stream_id;
CHECK(p->http_conns->tc->read_queue->count <= 8);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_CONNECTED, sid, NULL, 0);
for (int i = 0; i < 10; i++) pump();
size_t total = 0;
for (unsigned i = 0; i < message_count; i++) if (messages[i].cmd == TCP_PROXY_SUBCMD_DATA) {
CHECK(messages[i].len <= TCP_PROXY_CHUNK && total + messages[i].len <= sizeof(expected));
memcpy(expected + total, messages[i].data, messages[i].len); total += messages[i].len;
}
const size_t prefix = strlen("http://127.0.0.1");
CHECK(total == header + 20000 - prefix);
CHECK(memcmp(expected, "POST ", 5) == 0 && memcmp(expected + 5, upload + 5 + prefix, total - 5) == 0);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_ERROR, sid, NULL, 0);
socket_close_wrapper(fd);
tcp_proxy_client_destroy(p); inst->tcp_proxy_client = NULL; pump();
puts("[PASS] SOCKS/HTTP split/coalesced headers, CONNECTED/error, source identity and half-close");
}
static void server_tests(struct UTUN_INSTANCE* inst) {
uint16_t port; socket_t listener = listen_local(&port);
inst->tcp_proxy_server.enabled = 1; inst->tcp_proxy_server.inst = inst;
uint8_t connect_data[6] = {127,0,0,1}; uint16_t wire_port = htons(port); memcpy(connect_data+4, &wire_port, 2);
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CONNECT, 2, connect_data, 6);
socket_t a = accept(listener, NULL, NULL); CHECK(a != SOCKET_INVALID); socket_set_nonblocking(a);
deliver(inst, 22, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CONNECT, 2, connect_data, 6);
socket_t b = accept(listener, NULL, NULL); CHECK(b != SOCKET_INVALID); socket_set_nonblocking(b);
CHECK(inst->tcp_proxy_server.conn_count == 2);
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_DATA, 2, "AAA", 3);
deliver(inst, 22, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_DATA, 2, "BBB", 3);
uint8_t buf[16]; CHECK(recv(a, buf, sizeof(buf), 0) == 3 && memcmp(buf, "AAA", 3) == 0);
CHECK(recv(b, buf, sizeof(buf), 0) == 3 && memcmp(buf, "BBB", 3) == 0);
deliver(inst, 33, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CLOSE, 2, NULL, 0);
CHECK(inst->tcp_proxy_server.conn_count == 2);
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CONNECT, 2, connect_data, 6);
CHECK(inst->tcp_proxy_server.conn_count == 2);
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CLOSE, 2, NULL, 0);
CHECK(inst->tcp_proxy_server.conn_count == 1);
deliver(inst, 22, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CLOSE, 2, NULL, 0);
CHECK(inst->tcp_proxy_server.conn_count == 0);
socket_close_wrapper(a); socket_close_wrapper(b); socket_close_wrapper(listener);
inst->tcp_proxy_server.enabled = 0; pump();
puts("[PASS] exit streams isolated by node + stream ID, duplicate CONNECT rejected");
}
static void instance_tests(struct UTUN_INSTANCE* a) {
struct UTUN_INSTANCE* b = u_calloc(1, sizeof(*b)); CHECK(b); b->ua = ua;
CHECK(udp_proxy_init(a, ua) == 0 && udp_proxy_init(b, ua) == 0);
CHECK(icmp_proxy_init(a, ua) == 0 && icmp_proxy_init(b, ua) == 0);
CHECK(a->udp_proxy != b->udp_proxy && a->icmp_proxy != b->icmp_proxy);
struct udp_proxy_ctx* udp = b->udp_proxy; struct icmp_proxy_ctx* icmp = b->icmp_proxy;
udp_proxy_destroy(a); icmp_proxy_destroy(a);
CHECK(b->udp_proxy == udp && b->icmp_proxy == icmp);
CHECK(udp_proxy_init(b, ua) == 0 && b->udp_proxy == udp);
udp_proxy_destroy(b); icmp_proxy_destroy(b); u_free(b);
puts("[PASS] UDP/ICMP contexts and destruction are instance-local");
}
// Принимающий callback lwIP запускаем с PCB установленного соединения; дальнейшие
// recv/abort/shutdown проходят через настоящие callbacks proxy и стек lwIP.
static struct tcp_pcb* accept_tun(struct tcp_proxy_client* p) {
struct tcp_pcb* pcb = tcp_new(p->lwip); CHECK(pcb);
pcb->state = ESTABLISHED; pcb->local_port = 80; pcb->remote_port = 12345;
pcb->local_ip = htonl(0xc0000201); pcb->remote_ip = htonl(0x0a000002);
pcb->next = p->lwip->active_pcbs; p->lwip->active_pcbs = pcb;
struct tcp_pcb_listen* listener = (struct tcp_pcb_listen*)p->lwip->listen_pcbs; CHECK(listener && listener->accept);
CHECK(listener->accept(p, pcb, LERR_OK) == LERR_OK);
return pcb;
}
static void tun_tests(struct UTUN_INSTANCE* inst) {
#ifndef _WIN32
struct tcp_proxy_client_mapping_config map = {.local_port=80, .remote_port=80, .remote_ip="192.0.2.1"};
struct tcp_proxy_client* p = tcp_proxy_client_create(inst, ua, "testproxy", "10.0.0.1", 1500, 1, &map, 1, 42, 0, NULL, 0, NULL);
CHECK(p); inst->tcp_proxy_client = p;
message_count = 0;
struct tcp_pcb* pcb = accept_tun(p); CHECK(p->conn_count == 1);
uint32_t sid = p->conns->stream_id;
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_CONNECTED, sid, NULL, 0);
p->conns->flow.tx_credit = 0;
struct pbuf* pb = pbuf_alloc(PBUF_RAW, 4); CHECK(pb); pbuf_take(pb, "tail", 4);
CHECK(pcb->recv(pcb->callback_arg, pcb, pb, LERR_OK) == LERR_OK);
pcb->state = CLOSE_WAIT;
CHECK(pcb->recv(pcb->callback_arg, pcb, NULL, LERR_OK) == LERR_OK);
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_FIN, sid, NULL, 0);
CHECK(p->conn_count == 1 && p->conns->tx_queue->count == 1);
message_count = 0; uint32_t credit = 4;
deliver(inst, 42, ETCP_RT_ID_TCP_PROXY_CLIENT, TCP_PROXY_SUBCMD_WINDOW, sid, &credit, 4);
CHECK(p->conn_count == 0 && count_cmd(TCP_PROXY_SUBCMD_DATA) == 1 && count_cmd(TCP_PROXY_SUBCMD_FIN) == 1);
CHECK(messages[0].cmd == TCP_PROXY_SUBCMD_DATA && memcmp(messages[0].data, "tail", 4) == 0);
pcb = accept_tun(p); CHECK(p->conn_count == 1);
tcp_abort(pcb); CHECK(p->conn_count == 0);
// IPv4 options смещают UDP-заголовок; последующий фрагмент не является UDP-запросом.
uint8_t ip[36] = {0x46, 0, 0, 36, 0, 0, 0, 0, 64, 17};
uint32_t src = htonl(0x0a000002), dst = htonl(0xc0000201);
memcpy(ip+12, &src, 4); memcpy(ip+16, &dst, 4);
uint16_t sport=htons(12345), dport=htons(53), ulen=htons(12);
memcpy(ip+24, &sport, 2); memcpy(ip+26, &dport, 2); memcpy(ip+28, &ulen, 2); memcpy(ip+32, "data", 4);
message_count = 0;
for (int fragment = 0; fragment < 2; fragment++) {
struct ll_entry* e = queue_entry_new(0); CHECK(e);
e->dgram = u_calloc(1, sizeof(ip)+1); CHECK(e->dgram); e->len = sizeof(ip)+1;
ip[7] = fragment; memcpy(e->dgram+1, ip, sizeof(ip));
queue_data_put(p->tun->output_queue, e); pump();
}
CHECK(message_count == 1 && messages[0].svc == ETCP_RT_ID_UDP_PROXY && messages[0].len == 12);
CHECK(memcmp(messages[0].data+6, &dport, 2) == 0 && memcmp(messages[0].data+8, "data", 4) == 0);
tcp_proxy_client_destroy(p); inst->tcp_proxy_client = NULL; pump();
puts("[PASS] TUN pending DATA survives FIN; aborted PCB freed; IPv4 options/fragment validation");
#endif
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_PROXY, DEBUG_LEVEL_DEBUG);
CHECK(socket_platform_init() == 0);
ua = uasync_create(); CHECK(ua);
struct UTUN_INSTANCE* inst = u_calloc(1, sizeof(*inst)); CHECK(inst);
struct utun_config config = {0}; inst->config = &config; inst->ua = ua;
instance_tests(inst); parser_tests(inst); server_tests(inst); tun_tests(inst);
u_free(inst); pump(); uasync_destroy(ua, 0);
CHECK(u_get_allocated_count() == 0);
socket_platform_cleanup();
return 0;
}

2
tests/test_udp_proxy.c

@ -96,7 +96,7 @@ static void cli_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return;
}
size_t payload_len = entry->len - UDP_PROXY_RECV_HDR_SIZE;
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == UDP_PROXY_SUBCMD_DATA) {
if (entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == UDP_PROXY_SUBCMD_REPLY) {
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;

7
tools/lightsout-android/.gitignore vendored

@ -0,0 +1,7 @@
build/
.gradle/
*.apk
*.iml
.idea/
local.properties
.cxx/

49
tools/lightsout-android/app/build.gradle.kts

@ -0,0 +1,49 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.utun.lightsout"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
applicationId = "com.utun.lightsout"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.5"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
}
dependencies {
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.activity:activity-compose:1.8.2")
}

20
tools/lightsout-android/app/src/main/AndroidManifest.xml

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.LightsOut">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

77
tools/lightsout-android/app/src/main/java/com/utun/lightsout/LightsOutGame.kt

@ -0,0 +1,77 @@
package com.utun.lightsout
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue
import kotlin.random.Random
// Логика игры «Выключить свет»: хранит истинные состояния клеток (real, 0/1),
// отображаемое значение каждой клетки — XOR её самой с четырьмя соседями.
// Поле хранится как Compose-наблюдаемый список, чтобы клики перерисовывали доску.
class LightsOutGame(initialSize: Int = 5) {
var size: Int = initialSize
private set
val real = mutableStateListOf<Int>()
var moves by mutableIntStateOf(0)
private set
init {
newGame(initialSize)
}
// Новая партия: сброс поля и случайная решаемая позиция.
fun newGame(size: Int) {
this.size = size
real.clear()
repeat(size * size) { real.add(0) }
moves = 0
scramble()
}
// Переключает клетку, инкрементирует счётчик, возвращает true, если решено.
fun toggle(row: Int, col: Int): Boolean {
val idx = row * size + col
real[idx] = real[idx] xor 1
moves++
return isSolved()
}
// Отображаемое значение: XOR клетки с четырьмя соседями (с учётом границ).
fun display(row: Int, col: Int): Int {
var v = real[row * size + col]
if (row > 0) v = v xor real[(row - 1) * size + col]
if (row < size - 1) v = v xor real[(row + 1) * size + col]
if (col > 0) v = v xor real[row * size + (col - 1)]
if (col < size - 1) v = v xor real[row * size + (col + 1)]
return v
}
// Истинное состояние клетки (для подсказки).
fun isReallyOff(row: Int, col: Int): Boolean = real[row * size + col] == 0
// Победа — все отображаемые клетки погашены.
fun isSolved(): Boolean {
for (r in 0 until size) {
for (c in 0 until size) {
if (display(r, c) != 0) return false
}
}
return true
}
// Позиция строится случайными одиночными переключениями из нулевого состояния —
// обратная последовательность ходов гарантированно решает её.
private fun scramble() {
do {
for (i in real.indices) real[i] = 0
val scrambles = size * size * 3
repeat(scrambles) {
val idx = Random.nextInt(size * size)
real[idx] = real[idx] xor 1
}
} while (isSolved())
}
}

206
tools/lightsout-android/app/src/main/java/com/utun/lightsout/MainActivity.kt

@ -0,0 +1,206 @@
package com.utun.lightsout
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
private val OnColor = Color(0xFFFFD65C) // «включена» — тёплый жёлтый
private val OffColor = Color(0xFF2C3038) // «выключена» — тёмно-серый
private val HintColor = Color(0xFF78DC78) // подсказка — зелёная точка
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
LightsOutApp()
}
}
}
}
@Composable
fun LightsOutApp() {
var game by remember { mutableStateOf<LightsOutGame?>(null) }
var hintEnabled by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(true) }
var solved by remember { mutableStateOf(false) }
// Диалог выбора размера: при старте и по кнопке «Новая игра».
if (showDialog || game == null) {
val current = game?.size ?: 5
var selected by remember(current) { mutableIntStateOf(current) }
val isInitial = game == null
AlertDialog(
onDismissRequest = { if (!isInitial) showDialog = false },
title = { Text(if (isInitial) "Выключить свет" else "Новая игра") },
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"$selected × $selected",
style = MaterialTheme.typography.headlineMedium
)
Slider(
value = selected.toFloat(),
onValueChange = { selected = it.roundToInt() },
valueRange = 3f..8f,
steps = 4
)
Text(
"Размер поля",
style = MaterialTheme.typography.bodyMedium
)
}
},
confirmButton = {
TextButton(onClick = {
game = LightsOutGame(selected)
hintEnabled = false
solved = false
showDialog = false
}) {
Text(if (isInitial) "Играть" else "Начать")
}
},
dismissButton = {
if (!isInitial) {
TextButton(onClick = { showDialog = false }) { Text("Отмена") }
}
}
)
}
game?.let { g ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Ходы: ${g.moves}", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(16.dp))
Board(
game = g,
hintEnabled = hintEnabled,
onCellTap = { r, c ->
if (g.toggle(r, c)) {
solved = true
}
}
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(onClick = { hintEnabled = !hintEnabled }) {
Text(if (hintEnabled) "Скрыть подсказку" else "Подсказка")
}
Button(onClick = { showDialog = true }) {
Text("Новая игра")
}
}
}
}
if (solved) {
val g = game ?: return@LightsOutApp
AlertDialog(
onDismissRequest = { solved = false },
title = { Text("Победа!") },
text = { Text("Свет выключен за ${g.moves} ходов.") },
confirmButton = {
Button(onClick = { showDialog = true }) { Text("Новая игра") }
},
dismissButton = {
TextButton(onClick = { solved = false }) { Text("Ок") }
}
)
}
}
@Composable
private fun Board(
game: LightsOutGame,
hintEnabled: Boolean,
onCellTap: (Int, Int) -> Unit
) {
val n = game.size
Column(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
for (r in 0 until n) {
Row(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
for (c in 0 until n) {
val on = game.display(r, c) != 0
val hint = hintEnabled && game.isReallyOff(r, c)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(6.dp))
.background(if (on) OnColor else OffColor)
.border(1.dp, Color(0xFF14161A), RoundedCornerShape(6.dp))
.clickable { onCellTap(r, c) },
contentAlignment = Alignment.Center
) {
if (hint) {
Box(
modifier = Modifier
.size(18.dp)
.clip(CircleShape)
.background(HintColor)
)
}
}
}
}
}
}
}

4
tools/lightsout-android/app/src/main/res/values/strings.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Выключить свет</string>
</resources>

4
tools/lightsout-android/app/src/main/res/values/themes.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.LightsOut" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

4
tools/lightsout-android/build.gradle.kts

@ -0,0 +1,4 @@
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}

49
tools/lightsout-android/build.sh

@ -0,0 +1,49 @@
#!/bin/bash
# Сборка и прошивка игры «Выключить свет» (Android).
# Использование:
# ./build.sh # clean + сборка + установка на все подключённые устройства
# ./build.sh noinstall # только сборка, без прошивки
# ./build.sh <SN> # clean + сборка + прошивка на конкретный девайс
set -eo pipefail
cd "$(dirname "$0")"
export ANDROID_HOME="${ANDROID_HOME:-/home/user/android}"
APK=app/build/outputs/apk/debug/app-debug.apk
echo "Building..."
TMP=$(mktemp)
./gradlew clean > /dev/null 2>&1
if ! ./gradlew assembleDebug > "$TMP" 2>&1; then
cat "$TMP"
rm -f "$TMP"
exit 1
fi
rm -f "$TMP"
echo "Build OK"
[ "$1" = "noinstall" ] && exit 0
if [ -n "$1" ]; then
DEVICES=("$1")
else
mapfile -t DEVICES < <(adb devices | awk 'NR>1 && $2=="device"{print $1}')
fi
if [ ${#DEVICES[@]} -eq 0 ]; then
echo "No devices connected" >&2
exit 1
fi
echo "Devices (${#DEVICES[@]}):"
i=1
for d in "${DEVICES[@]}"; do
name=$(adb -s "$d" shell getprop ro.product.model 2>/dev/null | tr -d '\r')
[ -z "$name" ] && name=$(adb -s "$d" shell getprop ro.product.marketname 2>/dev/null | tr -d '\r')
echo " $i) $d $name"
i=$((i+1))
done
for d in "${DEVICES[@]}"; do
echo "Installing on $d..."
adb -s "$d" install -r "$APK"
done
echo "Install OK"

3
tools/lightsout-android/gradle.properties

@ -0,0 +1,3 @@
android.useAndroidX=true
kotlin.code.style=official
org.gradle.jvmargs=-Xmx2g

BIN
tools/lightsout-android/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

7
tools/lightsout-android/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
tools/lightsout-android/gradlew vendored

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

18
tools/lightsout-android/settings.gradle.kts

@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "lightsout-android"
include(":app")

22
tools/lightsout/CMakeLists.txt

@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.16)
project(lightsout VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 COMPONENTS Widgets QUIET)
if(NOT Qt6_FOUND)
find_package(Qt5 COMPONENTS Widgets REQUIRED)
set(QT_LIBS Qt5::Widgets)
else()
set(QT_LIBS Qt6::Widgets)
endif()
add_executable(lightsout
src/main.cpp
src/mainwindow.cpp
src/boardwidget.cpp
)
target_link_libraries(lightsout PRIVATE ${QT_LIBS})

8
tools/lightsout/build.sh

@ -0,0 +1,8 @@
#!/bin/sh
# Сборка игры «Выключить свет» (Qt).
set -e
cd "$(dirname "$0")"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc 2>/dev/null || echo 4)"
echo "Готово: ./build/lightsout"

141
tools/lightsout/src/boardwidget.cpp

@ -0,0 +1,141 @@
#include "boardwidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QRandomGenerator>
namespace {
constexpr int kMargin = 8; // внешний отступ поля
constexpr int kGap = 4; // зазор между клетками
constexpr int kMinCell = 36; // минимальный размер клетки (пикс.)
constexpr int kMaxCell = 72; // максимальный размер клетки (пикс.)
}
BoardWidget::BoardWidget(QWidget *parent)
: QWidget(parent) {
setMinimumSize(220, 220);
}
// Создаёт новую партию: сбрасывает поле, затем случайными одиночными
// переключениями формирует решаемую стартовую позицию.
void BoardWidget::newGame(int size) {
m_size = size;
m_moves = 0;
m_real.fill(0, m_size * m_size);
// Позиция строится переключением случайных клеток из нулевого состояния —
// обратная последовательность ходов гарантированно решает её.
const int scrambles = m_size * m_size * 3;
for (int i = 0; i < scrambles; ++i) {
const int idx = QRandomGenerator::global()->bounded(m_size * m_size);
m_real[idx] ^= 1;
}
update();
emit stateChanged(isSolved());
}
// Включает/выключает показ подсказки.
void BoardWidget::setHintEnabled(bool enabled) {
if (m_hintEnabled == enabled) {
return;
}
m_hintEnabled = enabled;
update();
}
// Отображаемое значение клетки — XOR её самой с четырьмя соседями.
int BoardWidget::displayValue(int row, int col) const {
int v = m_real[row * m_size + col];
if (row > 0) v ^= m_real[(row - 1) * m_size + col];
if (row < m_size - 1) v ^= m_real[(row + 1) * m_size + col];
if (col > 0) v ^= m_real[row * m_size + (col - 1)];
if (col < m_size - 1) v ^= m_real[row * m_size + (col + 1)];
return v;
}
// Переключает клетку по клику и проверяет победу.
void BoardWidget::toggleCell(int row, int col) {
m_real[row * m_size + col] ^= 1;
++m_moves;
update();
emit stateChanged(isSolved());
}
// Победа — все отображаемые клетки погашены.
bool BoardWidget::isSolved() const {
for (int r = 0; r < m_size; ++r) {
for (int c = 0; c < m_size; ++c) {
if (displayValue(r, c) != 0) {
return false;
}
}
}
return true;
}
void BoardWidget::mousePressEvent(QMouseEvent *event) {
if (m_size <= 0 || event->button() != Qt::LeftButton) {
QWidget::mousePressEvent(event);
return;
}
const QRectF board = boardRect();
if (!board.contains(event->pos())) {
return;
}
const double cell = (board.width() - (m_size - 1) * kGap) / m_size;
const int col = static_cast<int>((event->pos().x() - board.left()) / (cell + kGap));
const int row = static_cast<int>((event->pos().y() - board.top()) / (cell + kGap));
if (row >= 0 && row < m_size && col >= 0 && col < m_size) {
toggleCell(row, col);
}
}
void BoardWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
if (m_size <= 0) {
return;
}
const QRectF board = boardRect();
const double cell = (board.width() - (m_size - 1) * kGap) / m_size;
const QColor onColor(255, 214, 92); // «включена» — тёплый жёлтый
const QColor offColor(44, 48, 56); // «выключена» — тёмно-серый
const QColor hintColor(120, 220, 120); // подсказка — зелёная точка
for (int r = 0; r < m_size; ++r) {
for (int c = 0; c < m_size; ++c) {
const QRectF rect(
board.left() + c * (cell + kGap),
board.top() + r * (cell + kGap),
cell, cell);
painter.setPen(QPen(QColor(20, 22, 26), 1.5));
painter.setBrush(displayValue(r, c) ? onColor : offColor);
painter.drawRoundedRect(rect, 6, 6);
// Подсказка: точка на клетках, чьё истинное значение равно 0.
if (m_hintEnabled && m_real[r * m_size + c] == 0) {
painter.setPen(Qt::NoPen);
painter.setBrush(hintColor);
const double d = cell * 0.22;
painter.drawEllipse(rect.center(), d / 2, d / 2);
}
}
}
}
// Прямоугольник, занимаемый сеткой клеток (центрированный квадрат).
QRectF BoardWidget::boardRect() const {
const int cell = kMinCell;
const double side = cell * m_size + kGap * (m_size - 1);
const double x = (width() - side) / 2.0;
const double y = (height() - side) / 2.0;
return QRectF(x, y, side, side);
}

54
tools/lightsout/src/boardwidget.h

@ -0,0 +1,54 @@
#ifndef LIGHTSOUT_BOARDWIDGET_H
#define LIGHTSOUT_BOARDWIDGET_H
#include <QWidget>
#include <QVector>
// Виджет игрового поля: хранит истинные состояния клеток (real, 0/1),
// отображает XOR каждой клетки с четырьмя соседями, опционально показывает
// подсказку (точку) на клетках, чьё истинное значение равно 0.
class BoardWidget : public QWidget {
Q_OBJECT
public:
explicit BoardWidget(QWidget *parent = nullptr);
// Создаёт новую игру: поле size x size, случайная решаемая позиция.
void newGame(int size);
// Включает/выключает показ подсказки (точка на клетках real == 0).
void setHintEnabled(bool enabled);
// Показывается ли сейчас подсказка.
bool isHintEnabled() const { return m_hintEnabled; }
// Число ходов с начала партии.
int moves() const { return m_moves; }
signals:
// Испускается после каждого хода; solved == true, если все клетки погашены.
void stateChanged(bool solved);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
// Прямоугольник, занимаемый сеткой клеток (центрированный квадрат).
QRectF boardRect() const;
// Отображаемое значение клетки: XOR её и четырёх соседей.
int displayValue(int row, int col) const;
// Переключить клетку (обрабатывает клик), пересчитать и проверить победу.
void toggleCell(int row, int col);
// Проверяет, все ли отображаемые клетки погашены.
bool isSolved() const;
int m_size = 0; // размер поля (3..8)
QVector<quint8> m_real; // истинное состояние клеток
bool m_hintEnabled = false; // показывать подсказку
int m_moves = 0; // число ходов
};
#endif // LIGHTSOUT_BOARDWIDGET_H

21
tools/lightsout/src/main.cpp

@ -0,0 +1,21 @@
#include "mainwindow.h"
#include <QApplication>
#include <QInputDialog>
// Точка входа: запрашивает размер поля (3–8) и запускает игру.
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
bool ok = false;
const int size = QInputDialog::getInt(
nullptr, QObject::tr("Выключить свет"),
QObject::tr("Размер поля (3–8):"), 5, 3, 8, 1, &ok);
if (!ok) {
return 0;
}
MainWindow window(size);
window.show();
return app.exec();
}

73
tools/lightsout/src/mainwindow.cpp

@ -0,0 +1,73 @@
#include "mainwindow.h"
#include "boardwidget.h"
#include <QInputDialog>
#include <QLabel>
#include <QMenuBar>
#include <QMessageBox>
#include <QPushButton>
#include <QToolBar>
#include <QVBoxLayout>
#include <QWidget>
MainWindow::MainWindow(int size, QWidget *parent)
: QMainWindow(parent), m_size(size) {
setWindowTitle(QStringLiteral("Выключить свет"));
m_board = new BoardWidget(this);
m_board->newGame(m_size);
m_movesLabel = new QLabel(this);
m_movesLabel->setText(tr("Ходы: 0"));
auto *hintButton = new QPushButton(tr("Подсказка"), this);
hintButton->setCheckable(true);
connect(hintButton, &QPushButton::toggled, this, &MainWindow::toggleHint);
auto *newButton = new QPushButton(tr("Новая игра"), this);
connect(newButton, &QPushButton::clicked, this, &MainWindow::newGame);
auto *controls = new QWidget(this);
auto *layout = new QVBoxLayout(controls);
layout->addWidget(m_board, 1);
layout->addWidget(m_movesLabel);
layout->addWidget(hintButton);
layout->addWidget(newButton);
setCentralWidget(controls);
connect(m_board, &BoardWidget::stateChanged, this, &MainWindow::onBoardStateChanged);
}
// Начинает новую партию, запрашивая размер поля у пользователя.
void MainWindow::newGame() {
bool ok = false;
const int size = QInputDialog::getInt(
this, tr("Новая игра"),
tr("Размер поля (3–8):"), m_size, 3, 8, 1, &ok);
if (!ok) {
return;
}
m_size = size;
m_board->newGame(m_size);
m_movesLabel->setText(tr("Ходы: 0"));
}
// Переключает показ подсказок.
void MainWindow::toggleHint() {
m_board->setHintEnabled(!m_board->isHintEnabled());
}
// Обновляет счётчик ходов и сообщает о победе.
void MainWindow::onBoardStateChanged(bool solved) {
m_movesLabel->setText(tr("Ходы: %1").arg(m_board->moves()));
if (solved) {
const auto ret = QMessageBox::information(
this, tr("Победа!"),
tr("Свет выключен за %1 ходов.").arg(m_board->moves()),
QMessageBox::Ok | QMessageBox::Reset, QMessageBox::Ok);
if (ret == QMessageBox::Reset) {
newGame();
}
}
}

26
tools/lightsout/src/mainwindow.h

@ -0,0 +1,26 @@
#ifndef LIGHTSOUT_MAINWINDOW_H
#define LIGHTSOUT_MAINWINDOW_H
#include <QMainWindow>
class BoardWidget;
class QLabel;
// Главное окно игры: доска, кнопки «Новая игра»/«Подсказка», счётчик ходов.
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(int size, QWidget *parent = nullptr);
private slots:
void newGame();
void toggleHint();
void onBoardStateChanged(bool solved);
private:
int m_size;
BoardWidget *m_board;
QLabel *m_movesLabel;
};
#endif // LIGHTSOUT_MAINWINDOW_H
Loading…
Cancel
Save