diff --git a/src/proxy/http_proxy.h b/src/proxy/http_proxy.h index cc16a199..ac5fa649 100644 --- a/src/proxy/http_proxy.h +++ b/src/proxy/http_proxy.h @@ -1,7 +1,11 @@ /* Потоковый HTTP/1.x codec локального proxy. Не владеет сокетами/DNS/ETCP. * Заголовки проверяются целиком до пересылки; тело передаётся ограниченными порциями. - * emit получает исходное число байт для возврата ETCP credit после доставки результата. - * Возврат feed: 0=нужно продолжение, 1=сообщение завершено, <0=HTTP error status. */ + * Для нового сообщения обнулить message/request, затем headers_feed → request/response_parse → body_feed. + * parse меняет headers и сохраняет в fields указатели внутрь него: message нельзя копировать после parse. + * emit вызывается синхронно, буфер заимствован только на время вызова; source_len нужен для возврата + * ETCP credit после доставки результата. Ошибку feed/parse нельзя продолжать как успешное сообщение. + * Возврат feed: 0=нужно продолжение, 1=заголовки/тело завершены, <0=отрицательный HTTP status. + * HTTP_BODY_EOF завершает владелец по FIN, отдельной функции EOF у codec нет. */ #ifndef HTTP_PROXY_H #define HTTP_PROXY_H @@ -12,54 +16,64 @@ #define HTTP_PROXY_HEADER_LIMIT 32768 #define HTTP_PROXY_FIELDS_LIMIT 512 +// Результат: >=0 = принято целиком, <0 = ошибка вывода (codec вернёт -500); частичный приём не поддержан. +// source_len может отличаться от len; len=0/data=NULL допустимы при удалении исходных байтов. typedef int (*http_proxy_emit)(void* arg, const uint8_t* data, size_t len, size_t source_len); -struct http_proxy_field { char* name; char* value; }; +struct http_proxy_field { char* name; char* value; }; // Заимствованные NUL-строки внутри message.headers. +// NONE — исходное нулевое состояние; EOF — тело до внешнего FIN; DONE — точная граница сообщения. enum http_proxy_body_state { HTTP_BODY_NONE, HTTP_BODY_LENGTH, HTTP_BODY_EOF, HTTP_BODY_CHUNK_SIZE, HTTP_BODY_CHUNK_DATA, HTTP_BODY_CHUNK_CRLF, HTTP_BODY_TRAILERS, HTTP_BODY_DONE }; struct http_proxy_message { - char headers[HTTP_PROXY_HEADER_LIMIT + 1]; - size_t header_len; - size_t first_line_len; - struct http_proxy_field fields[HTTP_PROXY_FIELDS_LIMIT]; - unsigned field_count; - uint8_t headers_done; - uint8_t minor; - uint8_t close; - uint8_t chunked; - uint8_t has_length; - uint8_t upgrade; - uint8_t decode_chunks; - unsigned status; - uint64_t length; - enum http_proxy_body_state body; - uint64_t remaining; - char line[HTTP_PROXY_LINE_LIMIT + 1]; - size_t line_len; - size_t trailer_len; - unsigned crlf_pos; + char headers[HTTP_PROXY_HEADER_LIMIT + 1]; // До parse — CRLF-блок, после — строки с NUL-разделителями. + size_t header_len; // Число исходных байтов, включая CRLFCRLF. + size_t first_line_len; // Длина стартовой строки с CRLF. + struct http_proxy_field fields[HTTP_PROXY_FIELDS_LIMIT]; // Разобранные поля собственного headers. + unsigned field_count; // Число заполненных fields. + uint8_t headers_done; // headers_feed получил полный CRLFCRLF. + uint8_t minor; // Версия исходного HTTP: 0 или 1. + uint8_t close; // Запрос: Connection close; ответ: решение закрыть frontend. + uint8_t chunked; // Единственный поддержанный Transfer-Encoding — chunked. + uint8_t has_length; // Присутствует проверенный Content-Length (включая нулевой). + uint8_t upgrade; // В Connection найден token upgrade. + uint8_t decode_chunks; // Убрать framing chunked для ответа HTTP/1.0. + unsigned status; // HTTP status ответа; у запроса 0. + uint64_t length; // Content-Length; отсутствие различает has_length. + enum http_proxy_body_state body; // Состояние потокового разбора тела. + uint64_t remaining; // Остаток Content-Length или текущего chunk. + char line[HTTP_PROXY_LINE_LIMIT + 1]; // Неполная строка размера chunk или trailer. + size_t line_len; // Сколько байт накоплено в line. + size_t trailer_len; // Суммарный размер trailers, лимит HEADER_LIMIT. + unsigned crlf_pos; // 0/1: сколько байт CRLF после chunk уже принято. }; struct http_proxy_request { - char host[256]; - char authority[264]; - char upgrade[HTTP_PROXY_LINE_LIMIT + 1]; - uint16_t port; - uint8_t connect; - uint8_t head; - uint8_t close; - uint8_t minor; + char host[256]; // IPv4 literal или DNS-имя; IPv6 отклоняется с -502. + char authority[264]; // Нормализованный host:port для upstream Host. + char upgrade[HTTP_PROXY_LINE_LIMIT + 1]; // Предложенные protocol[/version], пусто без Upgrade. + uint16_t port; // Host byte order; HTTP default=80, CONNECT требует порт. + uint8_t connect; // Метод CONNECT: после успеха используется сырой туннель. + uint8_t head; // Метод HEAD: у ответа нет тела. + uint8_t close; // Закрыть frontend: HTTP/1.0 или Connection close. + uint8_t minor; // Версия клиента, определяет сериализацию ответа. }; -// Накапливает только заголовки; хвост входа остаётся у вызывающей стороны. +// Накапливает заголовки из заимствованного data; *used = число принятых байтов, хвост остаётся вызывающему. +// m первоначально обнулён; 1 = CRLFCRLF получен, 0 = ждать продолжения, <0 = -400/-414/-431. int http_proxy_headers_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used); -// Проверяет запрос и отправляет реконструированные заголовки через emit (кроме CONNECT). +// Однократно после headers_feed==1: проверяет запрос, заполняет обнулённый r и body в m. +// Обычный запрос требует absolute http:// URL; emit получает origin-form HTTP/1.1 headers. +// CONNECT проверяет authority и не вызывает emit. 0 = успех, <0 = отрицательный HTTP status. int http_proxy_request_parse(struct http_proxy_message* m, struct http_proxy_request* r, http_proxy_emit emit, void* arg); -// Проверяет ответ, учитывает HEAD/Upgrade и формирует собственные connection headers. +// Однократно после headers_feed==1; r — исходный запрос, close_client — решение закрыть frontend. +// Проверяет HEAD/Upgrade, задаёт body, выводит headers в версии клиента. 0 = успех, <0 = -502/-500. +// Для следующего промежуточного ответа владелец обнуляет m; r сохраняется до final response. int http_proxy_response_parse(struct http_proxy_message* m, const struct http_proxy_request* r, int close_client, http_proxy_emit emit, void* arg); -// Определяет точную границу тела, фильтрует trailers; поддерживает произвольные TCP-разбиения. +// После parse: передаёт тело из data через emit, *used = принятые байты до точной границы тела. +// Фильтрует trailers; 1 = DONE, 0 = продолжить, <0 = отрицательный HTTP status. +// Для HTTP_BODY_EOF возвращает 0, завершение по FIN проверяет владелец. Вывод не откатывается при ошибке. int http_proxy_body_feed(struct http_proxy_message* m, const uint8_t* data, size_t len, size_t* used, http_proxy_emit emit, void* arg); diff --git a/src/proxy/proxy_protocol.h b/src/proxy/proxy_protocol.h index ee12d5d9..47cd0bef 100644 --- a/src/proxy/proxy_protocol.h +++ b/src/proxy/proxy_protocol.h @@ -1,6 +1,11 @@ -// Общий TCP proxy протокол. DATA ограничен окном каждого потока; WINDOW возвращает -// кредит после потребления данных локальным TCP. FIN следует за всеми DATA своего -// направления. CONNECTED подтверждает реальный connect exit → destination. +/* Общий TCP proxy протокол и inline-машина окна; все вызовы выполняются в uasync владельца. + * init задаёт окно, но ready устанавливает владелец после CONNECTED (на exit — flush). + * Перед destroy владельца обязателен proxy_flow_destroy: retry хранит указатель на flow. + * Отправка: svc(1), cmd(1), sid(4), payload; приём: router header(26), cmd(1), sid(4), payload. + * sid и WINDOW uint32_t копируются в host byte order; IPv4/порт CONNECT — network byte order. + * CONNECTED подтверждает реальный connect exit → destination. DATA ограничен окном потока. + * FIN завершает одно направление; владелец ставит fin_pending только после всех своих DATA. + * CLOSE/ERROR и CONNECT отправляет владелец напрямую: общий retry их не сохраняет. */ #ifndef PROXY_PROTOCOL_H #define PROXY_PROTOCOL_H @@ -22,30 +27,32 @@ #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_RECV_HDR_SIZE (ROUTER_SVC_PAYLOAD_OFF + 5) // 31 байт с метаданными router, не wire header #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; + struct UTUN_INSTANCE* inst; // Заимствованный instance для отправки в TOPO_GROUP_UTUN, mode=0. + struct UASYNC* ua; // Заимствованный event loop; живёт дольше flow и retry. + uint64_t peer; // Конечный узел противоположной стороны потока. + uint32_t sid; // ID потока; на exit уникален только вместе с peer. + uint8_t svc; // Сервис получателя: TCP_PROXY_SERVER или TCP_PROXY_CLIENT. + uint8_t ready; // Разрешены DATA и FIN; начальное значение 0. + uint8_t connected_pending; // Exit должен поставить CONNECTED в router перед остальными control. + uint8_t fin_pending; // Владелец уже передал все DATA и просит отправить FIN. + uint8_t fin_sent; // FIN принят router; это не ACK конечного TCP. + uint8_t fin_received; // Владелец принял FIN; последующие DATA отклоняются. + uint32_t tx_credit; // Сколько DATA ещё разрешено отправить, изначально TCP_PROXY_WINDOW. + uint32_t rx_credit; // Сколько DATA разрешено принять до следующего WINDOW. + uint32_t consumed; // Потреблённые байты, кредит которых ещё не передан router. + void* retry; // Отменяемый таймер control, 5000 tb = 500 мс. + void (*wake)(void* arg); // Возобновление очередей владельца после timer flush; может освободить владельца. + void* arg; // Заимствованный аргумент wake. }; -// Сообщение целиком передаётся маршрутизатору, который освобождает entry при любом результате. +// Копирует заимствованный data (len<=4096; при len>0 не NULL), отправляет в UTUN с mode=0. +// 0 = принято router, <0 = ошибка; force обходит лимит send_q, не гарантирует доставку. +// Созданный entry целиком передаётся router, который освобождает его при любом результате. 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) { @@ -66,6 +73,8 @@ static inline int proxy_send(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t return etcp_route_send(inst, TOPO_GROUP_UTUN, peer, e, force, 0); } +// Отправляет pending CONNECTED → WINDOW → FIN с force=1; неудачу повторяет через 500 мс. +// Владелец сам дренирует DATA перед fin_pending; flush не проверяет его очереди. static inline void proxy_flow_flush(struct proxy_flow* f); // Повторяем только недоставленные control-сообщения; DATA остаётся у владельца потока. @@ -76,6 +85,8 @@ static inline void proxy_flow_retry(void* arg) { if (f->wake) f->wake(f->arg); } +// Обнуляет неактивный f, задаёт peer/svc/sid и оба окна по 64 KiB; указатели заимствуются. +// Перед повторным init нужно отменить прежний retry через destroy; ready остаётся 0. 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)); @@ -84,6 +95,7 @@ static inline void proxy_flow_init(struct proxy_flow* f, struct UTUN_INSTANCE* i f->wake = wake; f->arg = arg; } +// Отменяет только control retry; память flow, очереди и router waiter освобождает владелец. static inline void proxy_flow_destroy(struct proxy_flow* f) { if (f->retry) { uasync_cancel_timeout(f->ua, f->retry); f->retry = NULL; } } @@ -114,7 +126,8 @@ retry: if (!f->retry) f->retry = uasync_set_timeout(f->ua, 5000, f, proxy_flow_retry, "proxy_control"); } -// 0 = отправлено, -2 = ждём окно/CONNECTED, -1 = router backpressure/ошибка выделения. +// Копирует DATA; 0 = принято router, -2 = ждём окно/CONNECTED, -1 = неверный DATA/ошибка router или выделения. +// При ошибке восстанавливает tx_credit; исходный буфер остаётся у владельца для повтора. 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) { @@ -127,6 +140,8 @@ static inline int proxy_flow_send(struct proxy_flow* f, const uint8_t* data, siz return ret; } +// Проверяет DATA len=1..4096, доступное rx_credit и отсутствие FIN; 0 списывает кредит, -1 = ошибка. +// Проверку ready и доставку байтов локальному TCP выполняет вызывающая сторона. 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", @@ -137,6 +152,8 @@ static inline int proxy_flow_receive(struct proxy_flow* f, size_t len) { return 0; } +// WINDOW: заимствованные 4 байта host-order credit; 0 добавляет кредит, -1 = неверный размер/кредит. +// Не вызывает wake: владелец после успеха сам возобновляет отправку. 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); @@ -149,7 +166,8 @@ static inline int proxy_flow_window(struct proxy_flow* f, const uint8_t* data, s return 0; } -// Вызывать только после передачи байтов локальному TCP, не после получения ETCP DATA. +// Возвращает кредит через flush после передачи len байт локальному TCP (OS send/lwIP tcp_write). +// Владелец гарантирует, что байты ранее приняты и не учтены повторно; проверки здесь нет. static inline void proxy_flow_consume(struct proxy_flow* f, uint32_t len) { f->consumed += len; proxy_flow_flush(f); diff --git a/src/proxy/socks_proxy.h b/src/proxy/socks_proxy.h index 0f08c539..dfac84de 100644 --- a/src/proxy/socks_proxy.h +++ b/src/proxy/socks_proxy.h @@ -1,6 +1,9 @@ /* Локальные SOCKS5 CONNECT и HTTP proxy/CONNECT: DNS, handshake и передача TCP через удалённый exit. * tcp_proxy_client владеет listener и списками соединений; их callbacks выполняются в uasync. - * init_listen заимствует указатели на списки/счётчики до close_listen; закрытие listener не освобождает клиентов. */ + * init_listen заимствует inst/ua/списки/счётчики; они живут до закрытия listener И освобождения клиентов. + * Сначала закрыть listener, затем conn_free_all; закрытие listener само по себе не освобождает клиентов. + * DNS выполняется локально (первая A-запись); SOCKS поддерживает только CONNECT без аутентификации. + * HTTP keep-alive использует новый stream для каждой транзакции; CONNECT/101 переходят в сырой туннель. */ #ifndef SOCKS_PROXY_H #define SOCKS_PROXY_H @@ -25,80 +28,83 @@ struct socks_http_state; enum { SOCKS_STATE_GREETING = 0, // ждём SOCKS5 greeting (ver + methods) SOCKS_STATE_REQUEST, // ждём SOCKS5 CONNECT request (== HTTP_STATE_REQUEST) - SOCKS_STATE_CONNECTING, // DNS в процессе (== HTTP_STATE_CONNECTING) + SOCKS_STATE_CONNECTING, // ждём DNS или CONNECTED от exit (== HTTP_STATE_CONNECTING) SOCKS_STATE_RELAY // релей данных (== HTTP_STATE_RELAY) }; // DNS/connect и сырой туннель используют общие состояния; HTTP framing хранится отдельно в http. enum { HTTP_STATE_REQUEST = SOCKS_STATE_REQUEST, // ждём заголовки очередного HTTP-запроса - HTTP_STATE_CONNECTING = SOCKS_STATE_CONNECTING, // DNS в процессе + HTTP_STATE_CONNECTING = SOCKS_STATE_CONNECTING, // DNS или реальный connect на exit HTTP_STATE_RELAY = SOCKS_STATE_RELAY // релей данных }; struct socks_proxy_conn { - struct socks_proxy_conn* next; + struct socks_proxy_conn* next; // следующий клиент в списке владельца struct socks_proxy_conn** head; // указатель на голову списка (для удаления) int* count; // указатель на счётчик списка - struct tcp_conn* tc; - struct UASYNC* ua; - struct UTUN_INSTANCE* inst; - uint64_t via_node_id; - struct proxy_flow flow; - uint32_t stream_id; + struct tcp_conn* tc; // собственный OS TCP к локальному клиенту + struct UASYNC* ua; // заимствованный event loop + struct UTUN_INSTANCE* inst; // заимствованный instance + uint64_t via_node_id; // настроенный exit + struct proxy_flow flow; // окно и control retry текущего upstream + uint32_t stream_id; // текущий upstream; HTTP между транзакциями может быть 0 uint32_t* next_stream_id; // HTTP: новый поток для следующей транзакции - struct socks_http_state* http; - uint8_t dest_ip[4]; - uint16_t dest_port; - uint8_t fin_remote; - uint8_t rem_closed; - uint8_t close_sent; - uint8_t close_pending; - uint8_t state; - uint8_t freed; - uint8_t close_queued; + struct socks_http_state* http; // собственное HTTP-состояние; NULL для SOCKS + uint8_t dest_ip[4]; // IPv4 назначения, network byte order + uint16_t dest_port; // порт назначения, network byte order + uint8_t fin_remote; // FIN от exit; FIN локального клиента хранится в tc->fin_remote + uint8_t rem_closed; // прекращена обработка потока/клиента + uint8_t close_sent; // CLOSE принят router + uint8_t close_pending; // неудавшийся CLOSE; последняя попытка при free + uint8_t state; // SOCKS_STATE_* / HTTP_STATE_* + uint8_t freed; // освобождение уже началось + uint8_t close_queued; // close-маркер уже поставлен в write_queue void* free_soon_id; // handle отложенного освобождения (uasync_call_soon) - uint8_t buf[16384]; - uint16_t buf_len; + uint8_t buf[16384]; // handshake или неразобранный хвост read block + uint16_t buf_len; // число сохранённых байтов в buf uint8_t* tx_buf; // буфер при backpressure (retry в tx_waiter_cb) - uint16_t tx_len; + uint16_t tx_len; // остаток tx_buf; отправляется блоками до 4096 байт uint32_t bytes_to_client; // байт записано клиенту (curl) через write_queue uint32_t bytes_from_exit; // байт получено от exit node через ETCP DATA - struct queue_waiter_handle tx_waiter; - void* tx_retry_timer; // safety-net таймер backpressure (force=1) - uint8_t fin_deferred; // FIN отложен до сброса tx_buf + struct queue_waiter_handle tx_waiter; // ожидание router send_q, отменить до free + void* tx_retry_timer; // safety-net 500 мс, повтор с обычным force=0 + uint8_t fin_deferred; // зарезервировано, текущая реализация не использует struct adns_query* dns_q; // активный DNS-запрос (NULL когда нет) uint8_t dns_kind; // DNSK_SOCKS / DNSK_HTTP_CONNECT / DNSK_HTTP_PROXY }; struct listen_ctx; -// Создать слушающий сокет для SOCKS/HTTP proxy. addr_str = "IP:PORT". -// listen_ctx должен быть освобождён через socks_proxy_close_listen. -// Возвращает listen_ctx при успехе, NULL при ошибке. +// Создать IPv4 listener: addr_str = "IPv4:PORT", 0 разрешает выбор порта ОС. +// conns/count/next_stream_id и inst/ua заимствуются; is_http=0 — SOCKS5, иначе HTTP. +// Общий генератор sid нужен для всех списков одного tcp_proxy_client. Возврат: ctx или NULL при ошибке. +// Не регистрирует router callback: входящие ответы должен передавать tcp_proxy_client. struct listen_ctx* socks_proxy_init_listen(struct UASYNC* ua, const char* addr_str, struct socks_proxy_conn** conns, int* count, uint32_t* next_stream_id, struct UTUN_INSTANCE* inst, uint64_t via_node_id, int is_http); -// Закрыть слушающий сокет и освободить listen_ctx. +// Закрыть listener и освободить ctx в том же ua. NULL ctx допустим; активные клиенты сохраняются. // Если sock_out задан, в него записывается SOCKET_INVALID: сокет уже закрыт. void socks_proxy_close_listen(struct UASYNC* ua, struct listen_ctx* ctx, socket_t* sock_out); // Фактический порт активного listener (IP:0 выбирает ОС); 0 при ошибке. uint16_t socks_proxy_get_port(const struct listen_ctx* ctx); -// Найти соединение по stream_id +// Найти текущий stream_id в head; заимствованный результат или NULL, список не меняется. struct socks_proxy_conn* socks_proxy_find_conn(struct socks_proxy_conn* head, uint32_t stream_id); -// Обработать ответ exit: CONNECTED/DATA/WINDOW_UPDATE/CLOSE/ERROR/FIN. -// Возвращает 1 если обработано, 0 если stream_id не найден +// Передать ответ exit: CONNECTED/DATA/WINDOW/CLOSE/ERROR/FIN; data заимствован на время вызова. +// Проверку peer/group выполняет внешний dispatcher. Может закрыть/удалить соединение. +// Возвращает 1 при найденном sid (в том числе неизвестная команда), 0 если sid не найден. int socks_proxy_handle_etcp(struct socks_proxy_conn** head, int* count, uint32_t stream_id, uint8_t subcmd, const uint8_t* data, size_t data_len); -// Освободить одно соединение (удаляет из списка) +// Освободить c и его TCP/DNS/HTTP/таймеры/waiter, удалить из списка; NULL допустим. +// Активный HTTP upstream отменяется; в остальных случаях free не гарантирует уведомление exit. void socks_proxy_conn_free(struct socks_proxy_conn* c); -// Освободить все соединения в списке +// Освободить все соединения; head/count — те же живые адреса, что переданы init_listen. void socks_proxy_conn_free_all(struct socks_proxy_conn** head, int* count);