Clarify TCP lifecycle and platform address selection contracts

This commit is contained in:
evgeny committed 2026-10-21 16:46:45 +02:00
1 parent e5fafacf99
commit 0e15234ce1
3 files changed
+43 -19

No files matched your search

+8 -6
View File
@@ -187,19 +187,21 @@ int random_bytes(uint8_t *buffer, size_t len);
uint32_t get_interface_ip_by_index(uint32_t netif_index);
// Get IPv6 address of interface by its index (ifindex)
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
// POSIX: temporary=1 выбирает temporary (privacy), temporary=0 — постоянный адрес.
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
// prefer_stable=0: prefer temporary over stable
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
// Determine the interface index for the default route (outgoing traffic)
// Creates a temp UDP socket, connects to a well-known address to force
// kernel routing, then matches the chosen source IP to getifaddrs()
// POSIX: UDP connect к фиксированному внешнему адресу, затем сопоставление локального IP с getifaddrs().
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
// который может отличаться от default route при более специфичных маршрутах.
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
uint32_t get_default_route_netif_index(int family);
+34 -12
View File
@@ -2,7 +2,7 @@
## 1. Назначение
Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`.
Асинхронный TCP-коннектор, построенный на event loop `uasync` и очередях `ll_queue` одного потока.
Одна структура `struct tcp_conn` = одно TCP-соединение.
Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах.
@@ -12,7 +12,7 @@
- Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы.
- Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди.
- Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке.
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены.
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — сентинелы в write_queue; сначала предшествующие данные передаются ОС.
## 2. Как пользоваться
@@ -29,10 +29,11 @@ struct tcp_conn* tc = tcp_conn_create(
on_fin_cb, on_error_cb, arg);
```
После создания нужно установить callback на read_queue и включить отложенную обработку:
После создания нужно установить callback на read_queue. Отложенный первый вызов и уведомления backpressure настраиваются отдельно:
```c
queue_set_callback(tc->read_queue, on_read_cb, my_conn);
queue_set_callback_defer(tc->read_queue, 1); // необязательно: первый consumer callback через call_soon
queue_set_waiter_defer(tc->read_queue, 1);
```
@@ -44,6 +45,7 @@ static void on_read_cb(struct ll_queue* q, void* arg) {
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// обработать e->dgram (e->len байт)
memory_pool_free(c->tc->data_pool, e->dgram);
queue_entry_free(e);
queue_resume_callback(q); // обязательно!
}
@@ -51,7 +53,8 @@ static void on_read_cb(struct ll_queue* q, void* arg) {
### Запись данных
Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов):
Пользователь напрямую кладёт entry в `write_queue`. Первый callback может выполниться синхронно из `queue_data_put`;
продолжение после `queue_resume_callback` откладывается через `uasync_call_soon`:
```c
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
@@ -71,7 +74,10 @@ tcp_conn_push_close(tc); // close сокета после отправки вс
```
Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.).
Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1).
`push_fin` возвращает -1 как при уже поставленном FIN (`fin_pending=1`), так и после успешного shutdown (`fin_local=1`).
`push_close` возвращает -1 после фактического закрытия (`closed=1`); до обработки CLOSE повторная постановка не блокируется.
При ошибке shutdown вызывается `on_error`, а `on_fin_sent` не вызывается. Успешный push означает принятие сентинела в очередь;
связанный callback может выполниться ещё до возврата из push.
### Уничтожение
@@ -79,7 +85,10 @@ tcp_conn_push_close(tc); // close сокета после отправки вс
void tcp_conn_destroy(struct tcp_conn* tc);
```
Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`.
Сразу отменяет connect-таймер и callbacks очередей/сокета, дренирует очереди и закрывает сокет.
Пулы и сам `tc` освобождаются через `uasync_call_soon` в том же потоке. После destroy владелец не использует указатель.
Повторный вызов безопасен только до выполнения отложенного free; NULL безопасен всегда.
Перед остановкой/уничтожением uasync нужно выполнить отложенную очистку (`uasync_drain_immediate` вне текущих callbacks).
### Обработка ошибок
@@ -99,7 +108,7 @@ static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
```c
tcp_conn_set_flushed(tc, my_flushed_cb);
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют; сбрасывается перед вызовом
```
### Приостановка чтения
@@ -108,12 +117,24 @@ tcp_conn_set_flushed(tc, my_flushed_cb);
tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter
```
### Connect-таймаут
`tcp_conn_set_connect_timeout(tc, timeout_ms)` заменяет интервал ожидания, начиная с текущего вызова.
Значение <=0 отменяет прежний таймер; положительное значение ограничено `INT_MAX/10` мс из-за единиц uasync (0.1 мс).
Для уже установленного соединения настройка сохраняется без создания таймера.
Возврат: 0 — применено, `-EINVAL` — аргументы/состояние, `-ENOMEM` — создание таймера, `-EIO` — нарушение состояния отмены.
При ошибке прежние настройка и handle сохраняются. Новый таймер резервируется перед отменой старого;
операция выполняется целиком в uasync-потоке без вызова пользовательских callbacks. Вызывающий код проверяет результат.
Истечение вызывает `on_error(ETIMEDOUT)`; завершение connect, фатальная ошибка и destroy отменяют таймер.
### Ограничения и многопоточность
- **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока.
- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены.
- **FIN-сентинел в очереди.** После успешного `push_fin` новые данные в write_queue не помещают;
предшествующие данные передаются ОС до shutdown.
- **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди.
- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги).
- **Можно вызывать push_fin/push_close из callback-ов.** FIN защищён от повторной постановки;
CLOSE проверяет только уже выполненное закрытие. Владелец учитывает возможный синхронный callback.
## 3. API
@@ -128,7 +149,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai
| Функция | Описание |
|---------|----------|
| `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке |
| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен |
| `tcp_conn_destroy(tc)` | Сразу отменяет таймер/callbacks, дренирует очереди и закрывает сокет; пулы и tc освобождает через `uasync_call_soon` |
### Коллбэки (поля struct tcp_conn)
@@ -137,7 +158,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai
| `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа |
| `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом |
| `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен |
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — перед вызовом сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
| `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных |
### Graceful shutdown
@@ -145,7 +166,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai
| Функция | Описание |
|---------|----------|
| `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 |
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 |
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. После закрытия → -1 |
### Прочее
@@ -153,3 +174,4 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai
|---------|----------|
| `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue |
| `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога |
| `tcp_conn_set_connect_timeout(tc, ms)` | Заменить/отключить connect-таймер; возвращает 0 или отрицательный код ошибки |
+1 -1
View File
@@ -16,7 +16,7 @@ extern "C" {
#include <stdint.h> // For uint64_t
#include <stddef.h> // For size_t
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync использует timebase: 0.1 мс).
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync хранит здесь миллисекунды).
typedef struct {
TimeoutTime expiration; // Sort key (smaller = earlier)