7 changed files with 516 additions and 3 deletions
@ -0,0 +1,236 @@
|
||||
/* outgoing_iface.c
|
||||
* |
||||
* Определяет IPv4-адрес и индекс/имя исходящего интерфейса, |
||||
* который ОС выберет при отправке на заданный destination |
||||
* после bind на 0.0.0.0 (INADDR_ANY). |
||||
* |
||||
* Сборка: |
||||
* Linux / BSD: gcc -o out out.c |
||||
* Windows (MSVC): cl out.c /link ws2_32.lib iphlpapi.lib |
||||
* Windows (MinGW): gcc -o out.exe out.c -lws2_32 -liphlpapi |
||||
*/ |
||||
|
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <stdint.h> |
||||
|
||||
/* ===================== Платформенные заголовки ===================== */ |
||||
#ifdef _WIN32 |
||||
# ifndef WIN32_LEAN_AND_MEAN |
||||
# define WIN32_LEAN_AND_MEAN |
||||
# endif |
||||
# include <winsock2.h> |
||||
# include <ws2tcpip.h> |
||||
# include <iphlpapi.h> |
||||
# pragma comment(lib, "ws2_32.lib") |
||||
# pragma comment(lib, "iphlpapi.lib") |
||||
|
||||
typedef SOCKET socket_t; |
||||
# define SOCKET_INVALID INVALID_SOCKET |
||||
# define SOCKET_ERRNO WSAGetLastError() |
||||
|
||||
/* На Windows нет IFNAMSIZ; берём с запасом */ |
||||
# ifndef IF_NAMESIZE |
||||
# define IF_NAMESIZE 256 |
||||
# endif |
||||
|
||||
#else /* Linux, FreeBSD, OpenBSD, NetBSD, macOS, … */ |
||||
# include <unistd.h> |
||||
# include <sys/types.h> |
||||
# include <sys/socket.h> |
||||
# include <netinet/in.h> |
||||
# include <arpa/inet.h> |
||||
# include <ifaddrs.h> |
||||
# include <net/if.h> |
||||
# include <errno.h> |
||||
|
||||
typedef int socket_t; |
||||
# define SOCKET_INVALID (-1) |
||||
# define SOCKET_ERRNO errno |
||||
|
||||
# ifndef IF_NAMESIZE |
||||
# define IF_NAMESIZE IFNAMSIZ /* обычно 16 */ |
||||
# endif |
||||
#endif |
||||
|
||||
/* ===================== Структура результата ===================== */ |
||||
typedef struct { |
||||
char ip[INET_ADDRSTRLEN]; /* IP строкой, например "192.168.1.5" */ |
||||
char name[IF_NAMESIZE]; /* имя интерфейса: "eth0", "em0", "Wi-Fi" … */ |
||||
uint32_t ifindex; /* числовой ifindex (if_nametoindex) */ |
||||
} iface_info_t; |
||||
|
||||
/* ===================== Утилиты ===================== */ |
||||
static int net_init(void) |
||||
{ |
||||
#ifdef _WIN32 |
||||
static int done = 0; |
||||
if (!done) { |
||||
WSADATA wsa; |
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) |
||||
return -1; |
||||
done = 1; |
||||
} |
||||
#endif |
||||
return 0; |
||||
} |
||||
|
||||
static void socket_close(socket_t s) |
||||
{ |
||||
#ifdef _WIN32 |
||||
if (s != SOCKET_INVALID) |
||||
closesocket(s); |
||||
#else |
||||
if (s >= 0) |
||||
close(s); |
||||
#endif |
||||
} |
||||
|
||||
/* ===================== Основная функция ===================== */ |
||||
/*
|
||||
* dst_ip — адрес назначения (например, "8.8.8.8" или IP вашего VPN-сервера) |
||||
* dst_port — любой открытый порт (например, 53 или 123), реальных пакетов не уйдёт |
||||
* info — заполняемая структура |
||||
* |
||||
* Возвращает 0 при успехе, -1 при ошибке. |
||||
*/ |
||||
int get_outgoing_iface(const char *dst_ip, int dst_port, iface_info_t *info) |
||||
{ |
||||
socket_t sock = SOCKET_INVALID; |
||||
int ret = -1; |
||||
struct sockaddr_in dst, src; |
||||
socklen_t src_len = sizeof(src); |
||||
|
||||
memset(info, 0, sizeof(*info)); |
||||
if (net_init() != 0) |
||||
return -1; |
||||
|
||||
/* 1. UDP-сокет */ |
||||
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
||||
if (sock == SOCKET_INVALID) |
||||
return -1; |
||||
|
||||
/* 2. Bind на 0.0.0.0 — именно так, как в вопросе */ |
||||
memset(&dst, 0, sizeof(dst)); |
||||
dst.sin_family = AF_INET; |
||||
dst.sin_addr.s_addr = INADDR_ANY; /* 0.0.0.0 */ |
||||
dst.sin_port = 0; /* система выберет порт */ |
||||
|
||||
if (bind(sock, (struct sockaddr *)&dst, sizeof(dst)) != 0) |
||||
goto end; |
||||
|
||||
/* 3. "Соединяемся" с destination.
|
||||
* Для UDP это НЕ шлёт реальных пакетов, но ядро просчитывает |
||||
* маршрут и фиксирует выбранный source IP в сокете. */ |
||||
memset(&dst, 0, sizeof(dst)); |
||||
dst.sin_family = AF_INET; |
||||
dst.sin_port = htons((uint16_t)dst_port); |
||||
if (inet_pton(AF_INET, dst_ip, &dst.sin_addr) != 1) |
||||
goto end; |
||||
if (connect(sock, (struct sockaddr *)&dst, sizeof(dst)) != 0) |
||||
goto end; |
||||
|
||||
/* 4. Забираем выбранный локальный адрес */ |
||||
if (getsockname(sock, (struct sockaddr *)&src, &src_len) != 0) |
||||
goto end; |
||||
if (!inet_ntop(AF_INET, &src.sin_addr, info->ip, sizeof(info->ip))) |
||||
goto end; |
||||
|
||||
/* 5. По source IP ищем интерфейс в системном списке */ |
||||
#ifdef _WIN32 |
||||
{ |
||||
ULONG size = 0; |
||||
DWORD rv; |
||||
PIP_ADAPTER_ADDRESSES adapters = NULL, a; |
||||
PIP_ADAPTER_UNICAST_ADDRESS u; |
||||
|
||||
/* первый вызов — узнать размер буфера */ |
||||
rv = GetAdaptersAddresses(AF_INET, GAA_FLAG_INCLUDE_PREFIX, |
||||
NULL, NULL, &size); |
||||
if (rv != ERROR_BUFFER_OVERFLOW) |
||||
goto end; |
||||
|
||||
adapters = (PIP_ADAPTER_ADDRESSES)malloc(size); |
||||
if (!adapters) |
||||
goto end; |
||||
|
||||
rv = GetAdaptersAddresses(AF_INET, GAA_FLAG_INCLUDE_PREFIX, |
||||
NULL, adapters, &size); |
||||
if (rv != NO_ERROR) { |
||||
free(adapters); |
||||
goto end; |
||||
} |
||||
|
||||
for (a = adapters; a != NULL; a = a->Next) { |
||||
for (u = a->FirstUnicastAddress; u; u = u->Next) { |
||||
struct sockaddr_in *ua = |
||||
(struct sockaddr_in *)u->Address.lpSockaddr; |
||||
if (ua->sin_addr.s_addr == src.sin_addr.s_addr) { |
||||
info->ifindex = (uint32_t)a->IfIndex; |
||||
/* FriendlyName — WCHAR, приводим к UTF-8 */ |
||||
WideCharToMultiByte(CP_UTF8, 0, a->FriendlyName, -1, |
||||
info->name, IF_NAMESIZE, |
||||
NULL, NULL); |
||||
ret = 0; |
||||
goto win_done; |
||||
} |
||||
} |
||||
} |
||||
win_done: |
||||
free(adapters); |
||||
} |
||||
|
||||
#else /* Linux / BSD / macOS */ |
||||
{ |
||||
struct ifaddrs *ifap, *ifa; |
||||
|
||||
if (getifaddrs(&ifap) != 0) |
||||
goto end; |
||||
|
||||
for (ifa = ifap; ifa; ifa = ifa->ifa_next) { |
||||
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) |
||||
continue; |
||||
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr; |
||||
if (addr->sin_addr.s_addr == src.sin_addr.s_addr) { |
||||
strncpy(info->name, ifa->ifa_name, IF_NAMESIZE - 1); |
||||
info->ifindex = if_nametoindex(ifa->ifa_name); |
||||
ret = 0; |
||||
break; |
||||
} |
||||
} |
||||
freeifaddrs(ifap); |
||||
} |
||||
#endif |
||||
|
||||
end: |
||||
socket_close(sock); |
||||
return ret; |
||||
} |
||||
|
||||
/* ===================== Пример использования =====================
|
||||
int main(int argc, char **argv) |
||||
{ |
||||
iface_info_t info; |
||||
const char *dst = (argc > 1) ? argv[1] : "8.8.8.8"; |
||||
int port = (argc > 2) ? atoi(argv[2]) : 53; |
||||
|
||||
if (get_outgoing_iface(dst, port, &info) == 0) { |
||||
printf("Destination : %s:%d\n", dst, port); |
||||
printf("Source IP : %s\n", info.ip); |
||||
printf("Iface name : %s\n", info.name); |
||||
printf("Iface index : %u\n", info.ifindex); |
||||
return 0; |
||||
} |
||||
|
||||
fprintf(stderr, "Failed to determine outgoing interface (errno=%d)\n", |
||||
#ifdef _WIN32 |
||||
(int)WSAGetLastError() |
||||
#else |
||||
errno |
||||
#endif |
||||
); |
||||
return 1; |
||||
} |
||||
*/ |
||||
@ -0,0 +1,11 @@
|
||||
Установка прямого подключения между узлами |
||||
|
||||
1. узел собирает адреса - кандидаты для подключения: |
||||
список пар адрес-порт: |
||||
- адреса и порты из конфига сокетов (если не 0.0.0.0) |
||||
- адреса и порты из сокетов (nat) где nat = EIM |
||||
список ретрансляторов узлов-кандидатов: |
||||
- rtt, nodeid, адрес-порт |
||||
|
||||
|
||||
пары ip:port передаются на peer в команде "direct connection request" ROUTE_SUBCMD_CONN_REQ 0x0B |
||||
@ -0,0 +1,218 @@
|
||||
# uTun — защищённый VPN-туннель поверх UDP |
||||
|
||||
uTun создаёт виртуальный TUN-интерфейс и маршрутизирует IP-трафик через зашифрованные UDP-каналы по протоколу **ETCP** (Extended TCP) — комбинация TCP-надёжности + QUIC-мультиплексирования + multi-link балансировки. |
||||
|
||||
## Возможности |
||||
|
||||
| Возможность | Описание | |
||||
|---|---| |
||||
| **Шифрование** | ECC (secp256r1) обмен ключами + AES-128-CCM с аутентификацией | |
||||
| **Целостность** | CRC32 внутри зашифрованного payload | |
||||
| **Multi-link** | Одно подключение через несколько UDP-каналов одновременно | |
||||
| **Балансировка** | Выбор канала с минимальным inflight + round-robin при равенстве | |
||||
| **Bandwidth shaping** | Ограничение полосы на каждом канале с автооценкой | |
||||
| **Inflight control** | Динамическое ограничение байт в полёте для предотвращения перегрузки | |
||||
| **Retransmission** | Адаптивный таймаут: RTT_avg × k₁ + jitter × k₂ | |
||||
| **Keepalive** | Двусторонний, с настраиваемым интервалом и таймаутом | |
||||
| **Фрагментация/сборка** | Normalizer: упаковка мелких пакетов вместе, разбиение крупных | |
||||
| **Маршрутизация** | Локальная (через TUN + системные маршруты) и распределённая (BGP-обмен) | |
||||
| **NAT-детекция** | Определение типа NAT (open/restricted) через ping-пробы | |
||||
| **Файрвол** | Белый список IP:port для трафика через туннель | |
||||
| **Control server** | TCP-сервер для мониторинга метрик в реальном времени (GUI-клиент) | |
||||
| **Горячая перезагрузка** | SIGHUP — выборочное обновление конфига (сравнение сокетов/клиентов/линков) | |
||||
| **Авто-ключи** | При первом запуске генерирует ECC-пару и node_id, записывает в конфиг | |
||||
| **Демонизация** | fork/setsid на Linux; foreground-режим `-f` для отладки | |
||||
| **Кроссплатформенность** | Linux (epoll), Windows (wintun), FreeBSD | |
||||
| **Эмуляция потерь** | `loss_rate=N` в конфиге сокета — для тестирования | |
||||
|
||||
## Архитектура (кратко) |
||||
|
||||
``` |
||||
Приложение/ОС |
||||
↓ IP-пакеты |
||||
TUN interface (tun_if) |
||||
↓ queue |
||||
Routing (routing.c) — поиск маршрута |
||||
↓ (если удалённый) |
||||
Normalizer (pkt_normalizer.c) — фрагментация/упаковка |
||||
↓ |
||||
ETCP (etcp.c) — seq/ack, retransmit, reassembly |
||||
↓ |
||||
LoadBalancer (etcp_loadbalancer.c) — выбор канала |
||||
↓ |
||||
Encrypt + UDP send (etcp_connections.c) |
||||
↓ |
||||
Сеть (UDP) |
||||
``` |
||||
|
||||
## Конфигурация (INI-формат) |
||||
|
||||
### CLI-запуск |
||||
|
||||
``` |
||||
utun -c utun.conf [-p /var/run/utun.pid] [-l utun.log] [-f] [-d "etcp:debug"] |
||||
``` |
||||
|
||||
- `-c` — путь к конфигу (по умолчанию `utun.conf`) |
||||
- `-p` — PID-файл |
||||
- `-l` — лог-файл (по умолчанию `utun.log`) |
||||
- `-f` — foreground (не демонизироваться) |
||||
- `-d` — отладочный конфиг на лету, формат: `"категория:уровень,..."` |
||||
|
||||
### Секции конфига |
||||
|
||||
#### `[global]` — основные параметры |
||||
|
||||
| Ключ | Значение | По умолчанию | |
||||
|---|---|---| |
||||
| `my_node_name` | Имя узла (до 15 символов) | — | |
||||
| `my_node_id` | 64-битный ID узла (hex, 16 символов) | авто | |
||||
| `my_private_key` | Приватный ключ ECC (64 hex символа) | авто | |
||||
| `my_public_key` | Публичный ключ ECC (128 hex символов) | авто | |
||||
| `tun_ifname` | Имя TUN-интерфейса | `tun0` | |
||||
| `tun_ip` | IP-адрес TUN-интерфейса | — | |
||||
| `mtu` | MTU для всех подключений | `1500` | |
||||
| `keepalive_timeout` | Таймаут keepalive (мс) | `2000` | |
||||
| `keepalive_interval` | Интервал keepalive (мс) | `200` | |
||||
| `log_file` | Путь к лог-файлу | — | |
||||
| `debug_level` | Уровень отладки: `error`, `warn`, `info`, `debug`, `trace` | `error` | |
||||
| `tun_test_mode` | 1 — не создавать реальный TUN (тесты) | `0` | |
||||
| `enable_timestamp` | Метки времени в логах | `1` | |
||||
| `enable_function_names` | Имена функций в логах | `1` | |
||||
| `enable_file_lines` | Файл:строка в логах | `0` | |
||||
| `enable_colors` | ANSI-цвета в терминале | `0` | |
||||
|
||||
#### `[debug]` — по-категорийные уровни (переопределяют `debug_level`) |
||||
|
||||
Доступные категории: |
||||
`uasync`, `ll_queue`, `connection`, `etcp`, `crypto`, `memory`, `timing`, `config`, `tun`, `routing`, `timers`, `normalizer`, `bgp`, `socket`, `general`, `control`, `dump`, `traffic` |
||||
|
||||
Пример: |
||||
```ini |
||||
[debug] |
||||
etcp=info |
||||
routing=trace |
||||
bgp=trace |
||||
crypto=error |
||||
``` |
||||
|
||||
#### `[server: ИМЯ]` — локальные UDP-сокеты (обязательно хотя бы один) |
||||
|
||||
| Ключ | Значение | По умолчанию | |
||||
|---|---|---| |
||||
| `addr` | `IP:port` для bind | **обязателен** | |
||||
| `type` | `public`, `nat`, `private` | `unknown` | |
||||
| `mtu` | MTU для этого сокета | из `[global]` | |
||||
| `loss_rate` | % потерь пакетов (0–100) | `0` | |
||||
| `so_mark` | Linux SO_MARK для policy routing | — | |
||||
| `netif` | Привязка к интерфейсу (по имени) | — | |
||||
| `fib` | FreeBSD FIB (таблица маршрутизации) | `-1` (не задан) | |
||||
| `only_local` | 1 — только локальные соединения, без forwarding | `0` | |
||||
|
||||
Пример: |
||||
```ini |
||||
[server: lan1] |
||||
addr=192.168.29.117:1333 |
||||
type=public |
||||
``` |
||||
|
||||
#### `[client: ИМЯ]` — исходящие подключения к пиру |
||||
|
||||
| Ключ | Значение | По умолчанию | |
||||
|---|---|---| |
||||
| `link` | `сервер:удалённый_IP:порт` (можно несколько) | **обязателен** | |
||||
| `peer_public_key` | Публичный ключ пира (128 hex) | **обязателен** | |
||||
| `keepalive` | 1 — включить keepalive | `1` | |
||||
|
||||
Пример: |
||||
```ini |
||||
[client: aeza] |
||||
link=lan1:85.192.42.96:1333 |
||||
peer_public_key=04c1cae041a8e6bfba5245f6669c73f0793d7f9929300a2ba2e123ca55260d6e47... |
||||
``` |
||||
|
||||
> **Важно:** в серверном конфиге секции `[client]` нет. В клиентском — есть свои ключи + `peer_public_key` каждого сервера в `[client]`. |
||||
|
||||
#### `[routing]` — маршруты |
||||
|
||||
| Ключ | Значение | |
||||
|---|---| |
||||
| `route_subnet` | Подсеть, доступная через этот TUN (добавляется в системную таблицу маршрутизации). Можно несколько | |
||||
| `my_subnet` | Своя подсеть, анонсируемая через BGP | |
||||
|
||||
Пример: |
||||
```ini |
||||
[routing] |
||||
route_subnet=10.23.0.0/16 |
||||
route_subnet=10.24.0.0/16 |
||||
my_subnet=10.23.1.0/24 |
||||
``` |
||||
|
||||
#### `[firewall]` — контроль доступа |
||||
|
||||
| Ключ | Значение | |
||||
|---|---| |
||||
| `allow` | `all` — разрешить всё. Или `IP` / `IP:port` (можно несколько) | |
||||
|
||||
По умолчанию: deny all (если нет allow=all). |
||||
|
||||
Пример: |
||||
```ini |
||||
[firewall] |
||||
allow=192.168.1.100 |
||||
allow=10.0.0.0/8:443 |
||||
``` |
||||
|
||||
#### `[control]` — сервер мониторинга |
||||
|
||||
| Ключ | Значение | По умолчанию | |
||||
|---|---|---| |
||||
| `control_ip` | IP для прослушивания | `127.0.0.1` | |
||||
| `control_port` | Порт | — | |
||||
| `control_allow` | `IP/mask` (можно несколько) | deny all | |
||||
|
||||
Пример: |
||||
```ini |
||||
[control] |
||||
control_ip=192.168.29.117 |
||||
control_port=9090 |
||||
control_allow=192.168.0.0/16 |
||||
``` |
||||
|
||||
#### `[allowed_keys]` — белый список публичных ключей пиров |
||||
|
||||
| Ключ | Значение | По умолчанию | |
||||
|---|---|---| |
||||
| `allow_all` | 1 — разрешить все ключи | — | |
||||
| `key` | Публичный ключ (128 hex, можно несколько) | — | |
||||
|
||||
Поведение: |
||||
- Секция отсутствует: разрешить все (совместимость) |
||||
- `allow_all=0` + нет ключей: запретить все |
||||
- `allow_all=0` + ключи: только перечисленные ключи |
||||
|
||||
Пример: |
||||
```ini |
||||
[allowed_keys] |
||||
key=c6c8a8a9616ffa6cf44d4757e2bc9f7bd78a1d3cbbe75cffaf3cab8885ad48e7677b... |
||||
key=04c1cae041a8e6bfba5245f6669c73f0793d7f9929300a2ba2e123ca55260d6e4748... |
||||
``` |
||||
|
||||
### Пример минимального конфига |
||||
|
||||
```ini |
||||
[global] |
||||
tun_ip=10.23.1.1 |
||||
my_node_name=vmL1 |
||||
my_node_id=5f75c7445af88e1f |
||||
my_private_key=b0e8b68679db468979906894fe36239dbda910e06435361d696c7f5a8e2009c4 |
||||
my_public_key=c6c8a8a9616ffa6cf44d4757e2bc9f7bd78a1d3cbbe75cffaf3cab8885ad48e7677b... |
||||
|
||||
[server: lan1] |
||||
addr=192.168.29.117:1333 |
||||
type=public |
||||
|
||||
[routing] |
||||
route_subnet=10.23.0.0/16 |
||||
my_subnet=10.23.1.0/24 |
||||
``` |
||||
Loading…
Reference in new issue