diff --git a/3.go b/3.go new file mode 100644 index 00000000..9bb19cc2 --- /dev/null +++ b/3.go @@ -0,0 +1,299 @@ +package reality + +import ( + "bytes" + "context" + "crypto/ecdh" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + gotls "crypto/tls" + "crypto/x509" + "encoding/binary" + "fmt" + "io" + "net/http" + "reflect" + "regexp" + "strings" + "sync" + "time" + "unsafe" + + "github.com/cloudflare/circl/sign/mldsa/mldsa65" + utls "github.com/refraction-networking/utls" + "github.com/xtls/reality" + "github.com/xtls/xray-core/common/crypto" + "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/crypto/hkdf" + "golang.org/x/net/http2" +) + +type Conn struct { + *reality.Conn +} + +func (c *Conn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +func Server(c net.Conn, config *reality.Config) (net.Conn, error) { + realityConn, err := reality.Server(context.Background(), c, config) + return &Conn{Conn: realityConn}, err +} + +type UConn struct { + *utls.UConn + Config *Config + ServerName string + AuthKey []byte + Verified bool +} + +func (c *UConn) HandshakeAddress() net.Address { + if err := c.Handshake(); err != nil { + return nil + } + state := c.ConnectionState() + if state.ServerName == "" { + return nil + } + return net.ParseAddress(state.ServerName) +} + +func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if c.Config.Show { + localAddr := c.LocalAddr().String() + fmt.Printf("REALITY localAddr: %v\tis using X25519MLKEM768 for TLS' communication: %v\n", localAddr, c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768) + fmt.Printf("REALITY localAddr: %v\tis using ML-DSA-65 for cert's extra verification: %v\n", localAddr, len(c.Config.Mldsa65Verify) > 0) + } + p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") + certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset)) + if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { + h := hmac.New(sha512.New, c.AuthKey) + h.Write(pub) + if bytes.Equal(h.Sum(nil), certs[0].Signature) { + if len(c.Config.Mldsa65Verify) > 0 { + if len(certs[0].Extensions) > 0 { + h.Write(c.HandshakeState.Hello.Raw) + h.Write(c.HandshakeState.ServerHello.Raw) + verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify) + if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) { + c.Verified = true + return nil + } + } + } else { + c.Verified = true + return nil + } + } + } + opts := x509.VerifyOptions{ + DNSName: c.ServerName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err != nil { + return err + } + return nil +} + +func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) { + localAddr := c.LocalAddr().String() + uConn := &UConn{ + Config: config, + } + utlsConfig := &utls.Config{ + VerifyPeerCertificate: uConn.VerifyPeerCertificate, + ServerName: config.ServerName, + InsecureSkipVerify: true, + SessionTicketsDisabled: true, + KeyLogWriter: KeyLogWriterFromConfig(config), + } + if utlsConfig.ServerName == "" { + utlsConfig.ServerName = dest.Address.String() + } + uConn.ServerName = utlsConfig.ServerName + fingerprint := tls.GetFingerprint(config.Fingerprint) + if fingerprint == nil { + return nil, errors.New("REALITY: failed to get fingerprint").AtError() + } + uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint) + { + uConn.BuildHandshakeState() + hello := uConn.HandshakeState.Hello + hello.SessionId = make([]byte, 32) + copy(hello.Raw[39:], hello.SessionId) // the fixed location of `Session ID` + hello.SessionId[0] = core.Version_x + hello.SessionId[1] = core.Version_y + hello.SessionId[2] = core.Version_z + hello.SessionId[3] = 0 // reserved + binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) + copy(hello.SessionId[8:], config.ShortId) + if config.Show { + fmt.Printf("REALITY localAddr: %v\thello.SessionId[:16]: %v\n", localAddr, hello.SessionId[:16]) + } + publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey) + if err != nil { + return nil, errors.New("REALITY: publicKey == nil") + } + ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe + if ecdhe == nil { + ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe + } + if ecdhe == nil { + return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.") + } + uConn.AuthKey, _ = ecdhe.ECDH(publicKey) + if uConn.AuthKey == nil { + return nil, errors.New("REALITY: SharedKey == nil") + } + if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil { + return nil, err + } + aead := crypto.NewAesGcm(uConn.AuthKey) + if config.Show { + fmt.Printf("REALITY localAddr: %v\tuConn.AuthKey[:16]: %v\tAEAD: %T\n", localAddr, uConn.AuthKey[:16], aead) + } + aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) + copy(hello.Raw[39:], hello.SessionId) + } + if err := uConn.HandshakeContext(ctx); err != nil { + return nil, err + } + if config.Show { + fmt.Printf("REALITY localAddr: %v\tuConn.Verified: %v\n", localAddr, uConn.Verified) + } + if !uConn.Verified { + errors.LogError(ctx, "REALITY: received real certificate (potential MITM or redirection)") + go func() { + client := &http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) { + if config.Show { + fmt.Printf("REALITY localAddr: %v\tDialTLSContext\n", localAddr) + } + return uConn, nil + }, + }, + } + prefix := []byte("https://" + uConn.ServerName) + maps.Lock() + if maps.maps == nil { + maps.maps = make(map[string]map[string]struct{}) + } + paths := maps.maps[uConn.ServerName] + if paths == nil { + paths = make(map[string]struct{}) + paths[config.SpiderX] = struct{}{} + maps.maps[uConn.ServerName] = paths + } + firstURL := string(prefix) + getPathLocked(paths) + maps.Unlock() + get := func(first bool) { + var ( + req *http.Request + resp *http.Response + err error + body []byte + ) + if first { + req, _ = http.NewRequest("GET", firstURL, nil) + } else { + maps.Lock() + req, _ = http.NewRequest("GET", string(prefix)+getPathLocked(paths), nil) + maps.Unlock() + } + if req == nil { + return + } + utils.TryDefaultHeadersWith(req.Header, "nav") + if first && config.Show { + fmt.Printf("REALITY localAddr: %v\treq.UserAgent(): %v\n", localAddr, req.UserAgent()) + } + times := 1 + if !first { + times = int(crypto.RandBetween(config.SpiderY[4], config.SpiderY[5])) + } + for j := 0; j < times; j++ { + if !first && j == 0 { + req.Header.Set("Referer", firstURL) + } + req.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", int(crypto.RandBetween(config.SpiderY[0], config.SpiderY[1])))}) + if resp, err = client.Do(req); err != nil { + break + } + defer resp.Body.Close() + req.Header.Set("Referer", req.URL.String()) + if body, err = io.ReadAll(resp.Body); err != nil { + break + } + maps.Lock() + for _, m := range href.FindAllSubmatch(body, -1) { + m[1] = bytes.TrimPrefix(m[1], prefix) + if !bytes.Contains(m[1], dot) { + paths[string(m[1])] = struct{}{} + } + } + req.URL.Path = getPathLocked(paths) + if config.Show { + fmt.Printf("REALITY localAddr: %v\treq.Referer(): %v\n", localAddr, req.Referer()) + fmt.Printf("REALITY localAddr: %v\tlen(body): %v\n", localAddr, len(body)) + fmt.Printf("REALITY localAddr: %v\tlen(paths): %v\n", localAddr, len(paths)) + } + maps.Unlock() + if !first { + time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[6], config.SpiderY[7])) * time.Millisecond) // interval + } + } + } + get(true) + concurrency := int(crypto.RandBetween(config.SpiderY[2], config.SpiderY[3])) + for i := 0; i < concurrency; i++ { + go get(false) + } + // Do not close the connection + }() + time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[8], config.SpiderY[9])) * time.Millisecond) // return + return nil, errors.New("REALITY: processed invalid connection").AtWarning() + } + return uConn, nil +} + +var ( + href = regexp.MustCompile(`href="([/h].*?)"`) + dot = []byte(".") +) + +var maps struct { + sync.Mutex + maps map[string]map[string]struct{} +} + +func getPathLocked(paths map[string]struct{}) string { + stopAt := int(crypto.RandBetween(0, int64(len(paths)-1))) + i := 0 + for s := range paths { + if i == stopAt { + return s + } + i++ + } + return "/" +} diff --git a/AGENTS.md b/AGENTS.md index 94497bbc..1f0b3150 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -288,6 +288,9 @@ MEMBER_SYNC=27 - `nat_transport.c/h` - NAT transport layer (packet relay) - `ntp_time.c/h` - NTP time synchronization - `ntp_node_time.c/h` - Inter-node time synchronization +- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL) +- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p) +- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки **Transport Layer (src/transport_layer/)** - `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter) @@ -298,6 +301,8 @@ MEMBER_SYNC=27 - `etcp_dump.c/h` - ETCP дамп/декодирование пакетов - `etcp_bbr.c/h` - BBR congestion control for ETCP - `etcp_connect.c/h` - ETCP outbound connections +- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы) +- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`) - `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker) - `packet_dump.c/h` - Packet hex dump utility - `dummynet.c/h` - Network emulator integrated into utun (for testing) @@ -309,6 +314,9 @@ MEMBER_SYNC=27 - `stcp_link.c/h` - STCP линк-уровень - `node_conn_direct.c/h` - Direct config-based connections - `socket_monitor.c/h` - Socket state monitoring +- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP +- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа +- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт **BBR (src/transport_layer/BBR/)** - `bbr_v3.c` - BBR congestion control algorithm v3 @@ -328,6 +336,7 @@ MEMBER_SYNC=27 - `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND) - `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding) - `nat_detection.c/h` - NAT type detection +- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign) **Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера. - `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей @@ -342,6 +351,12 @@ MEMBER_SYNC=27 - `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core) - `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core) - `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core) +- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе +- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу +- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async) +- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol) +- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса) +- `invite_link.c/h` - Формат invite-ссылок utun:// для каналов (base64: version|password|channel_id|pubkey|addrs) **Прокси (src/proxy/)** - `socks_proxy.c/h` - SOCKS5 прокси @@ -383,6 +398,13 @@ MEMBER_SYNC=27 - `opus_codec.c/h` - Opus audio codec wrapper - `audio_compressor.c/h` - Audio dynamic range compressor - `json_flat.c/h` - Flat JSON parser +- `strbuf.c/h` - Безопасный растущий printf-буфер (замена snprintf-цепочек) +- `async_dns.c/h` - Асинхронный (неблокирующий) DNS-резолвер A-записей поверх uasync, адаптер над libdns +- `dns.c/h` - Рекурсивный reentrant DNS-резолвер (libdns, MIT) +- `miniaudio.h` - Воспроизведение/захват аудио (single-header) +- `dr_mp3.h` - MP3-декодер (single-header, на базе minimp3) +- `liblmdb/` - Встраиваемый key-value store LMDB +- `libopus/` - Встраиваемый кодек Opus - `wintun.h` - Wintun API header for Windows TUN driver **Memory Pools in UTUN_INSTANCE:** @@ -563,19 +585,40 @@ cmake .. && make -j4 | `lottieicon.h/cpp` | Загрузка TGS (gzip→zlib), рендер кадров через `lottie_animation_render` | | `animtimer.h/cpp` | Синглтон-таймер 30fps, refcount activate/deactivate, сигнал ticked | | `channellist.h/cpp` `channeldelegate.h/cpp` | Список каналов с аватарами и превью | -| `accountlist.h/cpp` | Список аккаунтов/пользователей | +| `accountlist.h/cpp` `accountdelegate.h/cpp` | Список аккаунтов/пользователей и его делегат отрисовки | +| `memberlistmodel.h/cpp` | Модель списка участников канала | +| `memberpropsdialog.h/cpp` | Диалог свойств участника | +| `flagpainter.h/cpp` | Отрисовка иконок мембера (admin/mod/supernode/storage) | | `joindialog.h/cpp` | Диалог присоединения к группе/каналу | | `invitedialog.h/cpp` | Диалог приглашения пользователя | | `invite_link.h/cpp` | Логика invite-ссылок | +| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) | | `settingsdialog.h/cpp` | Окно настроек | | `creategroupdialog.h/cpp` | Диалог создания группы | +| `channelsettingsdialog.h/cpp` | Диалог настроек канала | +| `renamedialog.h/cpp` | Диалог переименования | | `networksettingspage.h/cpp` | Страница сетевых настроек | +| `nodespage.h/cpp` | Таблица узлов + дамп в настройках | +| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) | +| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках | +| `storagesettingspage.h/cpp` | Страница настроек хранилища | +| `soundsettingspage.h/cpp` | Страница настроек звука | +| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств | +| `sound_manager.h/cpp` | Управление звуками/уведомлениями | +| `audiorecorder.h/cpp` | Запись голосовых сообщений (захват аудио) | +| `voicemessageencoder.h/cpp` | Кодирование голосовых сообщений (Opus) | +| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений | +| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов | +| `imageviewer_window.h/cpp` | Окно просмотра изображений | +| `videoplayer_engine.h/cpp` | Движок декодирования видео | +| `videoplayer_window.h/cpp` | Окно просмотра видео | +| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) | | `qrcode_utils.h/cpp` | Утилиты для QR-кодов | | `debug_ui.h` | Отладочный UI | ### Другие поддиректории - `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_, peers_, local_identity, accounts, ui_state) -- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `chat_core.h/c` (управление), `chat_sync.h/c` (синхронизация), `db_sync_stub.c` (заглушка БД), `node_config.h/cpp`, `config_updater.h/cpp`, `utun_node.h/cpp`, `topo_node_sqlite.h/c` +- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `node_config.h/cpp` (конфиг узла), `config_updater.h/cpp` (правка конфига), `utun_node.h/cpp` (обёртка узла), `miniaudio_impl.c` (single-header miniaudio в отдельной единице трансляции). Chat-логика (`chat_core`, `chat_sync` и т.д.) компилируется напрямую из `src/chat/`, роутинг — из `src/routing_layer/` (см. `libutun/CMakeLists.txt`) - `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs` ### Система анимированных эмодзи diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index ada7b6a4..9e99cf9a 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/src/routing_layer/conn_mgr_core.c @@ -547,7 +547,7 @@ static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* e struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET && s->type != CFG_SERVER_TYPE_PRIVATE) { struct ETCP_LINK *tlink = etcp_link_new(conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); any = 1; v4_tcp++; v4_cnt++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); any = 1; v4_tcp++; v4_cnt++; } } s = s->next; } } if (a->protocol & TOPO_PROTO_UDP) { @@ -575,7 +575,7 @@ static void cm_direct_add_links(struct ETCP_CONN* conn, struct CONN_MGR_ENTRY* e { struct ETCP_SOCKET* s = entry->mgr->instance->etcp_sockets; while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET6 && s->type != CFG_SERVER_TYPE_PRIVATE) { struct ETCP_LINK *tlink = etcp_link_new(conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a6->port); any = 1; v6_tcp++; v6_cnt++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a6->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a6->port); any = 1; v6_tcp++; v6_cnt++; } } s = s->next; } } } diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index 51c5bd46..c88c9953 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -54,6 +54,26 @@ static void free_v4_sub_list(struct memory_pool* pool, struct TOPO_SUBNET4* head static void free_v6_sub_list(struct memory_pool* pool, struct TOPO_SUBNET6* head) { while (head) { struct TOPO_SUBNET6* next = head->next; memory_pool_free(pool, head); head = next; } } +static void free_reality_sock_list(struct TOPO_REALITY_SOCK* head) { + while (head) { struct TOPO_REALITY_SOCK* next = head->next; u_free(head); head = next; } +} + +const struct TOPO_REALITY_SOCK* topo_node_find_reality_sock(const struct TOPO_NODE* ni, uint8_t socket_id) { + if (!ni) return NULL; + for (const struct TOPO_REALITY_SOCK* r = ni->reality_socks; r; r = r->next) + if (r->socket_id == socket_id) return r; + return NULL; +} + +void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_SOCK* rs) { + if (!link || !rs) return; + memcpy(link->reality.server_static_pubkey, rs->server_pubkey, REALITY_AUTH_KEY_SIZE); + memcpy(link->reality.short_id, rs->short_id, REALITY_SHORT_ID_SIZE); + memcpy(link->reality.version, rs->version, REALITY_VERSION_SIZE); + snprintf(link->reality.server_name, sizeof(link->reality.server_name), "%s", rs->server_name); + link->reality.fingerprint = REALITY_FP_CHROME; + link->reality_set = 1; +} /* Упорядоченное сравнение списков адресов/мета (порядок обхода сокетов детерминирован, prepend даёт одинаковый порядок при неизменном наборе сокетов). */ @@ -87,6 +107,17 @@ static int addr6_list_equal(struct TOPO_ADDR6* a, struct TOPO_ADDR6* b) { } return a == NULL && b == NULL; } +static int reality_sock_list_equal(struct TOPO_REALITY_SOCK* a, struct TOPO_REALITY_SOCK* b) { + while (a && b) { + if (a->socket_id != b->socket_id || + memcmp(a->short_id, b->short_id, 8) != 0 || + memcmp(a->server_pubkey, b->server_pubkey, 32) != 0 || + memcmp(a->version, b->version, 3) != 0 || + strncmp(a->server_name, b->server_name, TOPO_REALITY_SNAME_MAX) != 0) return 0; + a = a->next; b = b->next; + } + return a == NULL && b == NULL; +} /* извлекает hop_list из лучшего живого path в nq->paths (или любого, если живых нет). память принадлежит TOPO_NODEPATH в paths, не освобождать */ @@ -109,6 +140,7 @@ void topo_node_free_raw(struct TOPO_GROUPS* groups, struct TOPO_NODE* ni) { free_v6_sock_list(groups->v6_sock_meta_pool, ni->v6_sock_meta); free_v6_addr_list(groups->v6_addr_pool, ni->v6_addrs); } + free_reality_sock_list(ni->reality_socks); u_free(ni->node_name); u_free(ni); } @@ -125,8 +157,10 @@ void topo_nodeq_free_group_fields(struct TOPO_GROUPS* groups, struct TOPO_GROUP_ free_v4_addr_list(groups->v4_addr_pool, ni->v4_addrs); free_v6_sock_list(groups->v6_sock_meta_pool, ni->v6_sock_meta); free_v6_addr_list(groups->v6_addr_pool, ni->v6_addrs); + free_reality_sock_list(ni->reality_socks); ni->v4_sock_meta = NULL; ni->v4_addrs = NULL; ni->v6_sock_meta = NULL; ni->v6_addrs = NULL; + ni->reality_socks = NULL; topo_node_registry_unref(groups, nq->node_id); } if (nq->subnets) { @@ -164,6 +198,7 @@ struct TOPO_NODE* topo_node_registry_store(struct TOPO_GROUPS* groups, struct TO topo_node_ref(existing); if (!existing->v4_addrs && ni->v4_addrs) { existing->v4_addrs = ni->v4_addrs; ni->v4_addrs = NULL; } if (!existing->v6_addrs && ni->v6_addrs) { existing->v6_addrs = ni->v6_addrs; ni->v6_addrs = NULL; } + if (!existing->reality_socks && ni->reality_socks) { existing->reality_socks = ni->reality_socks; ni->reality_socks = NULL; } topo_node_free_raw(groups, ni); return existing; } @@ -235,6 +270,17 @@ int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size) buf[off++] = (uint8_t)(e->port & 0xFF); buf[off++] = (uint8_t)((e->port >> 8) & 0xFF); buf[off++] = e->type; buf[off++] = e->socket_id; buf[off++] = e->protocol; e = e->next; }} + { struct TOPO_REALITY_SOCK* e = ni->reality_socks; while (e) { + size_t nlen = strnlen(e->server_name, TOPO_REALITY_SNAME_MAX); + if (off + 1 + 8 + 32 + 3 + nlen + 1 > buf_size) return -1; + buf[off++] = e->socket_id; + memcpy(buf + off, e->short_id, 8); off += 8; + memcpy(buf + off, e->server_pubkey, 32); off += 32; + memcpy(buf + off, e->version, 3); off += 3; + if (nlen) { memcpy(buf + off, e->server_name, nlen); off += nlen; } + buf[off++] = '\0'; + e = e->next; + }} return (int)off; } @@ -253,6 +299,7 @@ int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) { + msg->local_v4_addrs * sizeof(struct TOPOMSG_ADDR4) + msg->local_v6_sockets * sizeof(struct TOPOMSG_SOCKMETA6) + msg->local_v6_addrs * sizeof(struct TOPOMSG_ADDR6) + + msg->local_reality_socks * sizeof(struct TOPOMSG_REALITY_SOCK) + ((msg->flags & TOPO_FLAG_SEND_SUBNETS) ? (msg->local_v4_subnets * sizeof(struct TOPOMSG_SUBNET4) + msg->local_v6_subnets * sizeof(struct TOPOMSG_SUBNET6)) : 0) @@ -278,6 +325,7 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, msg.local_v4_addrs = topo_list_count((struct _topo_head*)ni->v4_addrs); msg.local_v6_sockets = topo_list_count((struct _topo_head*)ni->v6_sock_meta); msg.local_v6_addrs = topo_list_count((struct _topo_head*)ni->v6_addrs); + msg.local_reality_socks = topo_list_count((struct _topo_head*)ni->reality_socks); msg.local_v4_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v4_subnets) : 0; msg.local_v6_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) : 0; { uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL); @@ -310,6 +358,15 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, struct TOPOMSG_ADDR6 w = { .port=e->port, .type=e->type, .socket_id=e->socket_id, .protocol=e->protocol }; memcpy(w.addr, e->addr, 16); memcpy(dp, &w, sizeof(w)); dp += sizeof(w); e = e->next; }} + { struct TOPO_REALITY_SOCK* e = ni->reality_socks; while (e) { + struct TOPOMSG_REALITY_SOCK w; memset(&w, 0, sizeof(w)); + w.socket_id = e->socket_id; + memcpy(w.short_id, e->short_id, 8); + memcpy(w.server_pubkey, e->server_pubkey, 32); + memcpy(w.version, e->version, 3); + memcpy(w.server_name, e->server_name, TOPO_REALITY_SNAME_MAX); + memcpy(dp, &w, sizeof(w)); dp += sizeof(w); e = e->next; + }} if (msg.flags & TOPO_FLAG_SEND_SUBNETS) { if (nq->subnets) { { struct TOPO_SUBNET4* e = nq->subnets->v4_subnets; while (e) { @@ -393,6 +450,21 @@ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t if (!head) head = tail = e; else { tail->next = e; tail = e; } } ni->v6_addrs = head; } + { struct TOPO_REALITY_SOCK* head = NULL; struct TOPO_REALITY_SOCK* tail = NULL; + for (int i = 0; i < msg->local_reality_socks; i++) { + const struct TOPOMSG_REALITY_SOCK* w = (const struct TOPOMSG_REALITY_SOCK*)dp; dp += sizeof(*w); + struct TOPO_REALITY_SOCK* e = u_calloc(1, sizeof(struct TOPO_REALITY_SOCK)); + if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + e->socket_id = w->socket_id; + memcpy(e->short_id, w->short_id, 8); + memcpy(e->server_pubkey, w->server_pubkey, 32); + memcpy(e->version, w->version, 3); + memcpy(e->server_name, w->server_name, TOPO_REALITY_SNAME_MAX); + e->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0; + e->next = NULL; + if (!head) head = tail = e; else { tail->next = e; tail = e; } + } + ni->reality_socks = head; } if (out_subnets && (msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) { struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); @@ -818,6 +890,23 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { struct TOPO_ADDR4* v4_head = NULL; struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_ADDR6* v6_head = NULL; + struct TOPO_REALITY_SOCK* r_head = NULL; + + /* node-global reality-параметры (одинаковы для всех reality-сокетов): рекламируем первый short_id */ + uint8_t r_short_id[8], r_pubkey[32], r_version[3]; char r_sname[TOPO_REALITY_SNAME_MAX]; + uint8_t r_valid = 0; + { const struct reality_config* rc = &instance->config->global.reality; + if (rc->enabled && rc->has_private_key) { + if (reality_pubkey_from_priv(rc->private_key, r_pubkey) == REALITY_OK) { + if (rc->short_id_count > 0) memcpy(r_short_id, rc->short_ids[0], 8); + else memcpy(r_short_id, rc->short_id, 8); + memcpy(r_version, rc->version, 3); + memset(r_sname, 0, sizeof(r_sname)); + strncpy(r_sname, rc->server_name, sizeof(r_sname) - 1); + r_valid = 1; + } + } + } struct ETCP_SOCKET* e_sock = instance->etcp_sockets; while (e_sock) { @@ -879,18 +968,32 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { a6->type = TOPO_ADDR_INTERFACE; a6->socket_id = ts->sock_id; a6->protocol = TOPO_PROTO_TCP; a6->next = v6_head; v6_head = a6; } } + if (r_valid && ts->reality_enabled) { + struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(struct TOPO_REALITY_SOCK)); + if (!r) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "reality_sock alloc failed"); } + else { + r->socket_id = ts->sock_id; + memcpy(r->short_id, r_short_id, 8); + memcpy(r->server_pubkey, r_pubkey, 32); + memcpy(r->version, r_version, 3); + memcpy(r->server_name, r_sname, TOPO_REALITY_SNAME_MAX); + r->next = r_head; r_head = r; + } + } ts = ts->next; } } /* Без изменений — подпись/рассылку гасим, но БД всё равно синхронизируем: она могла быть не готова (или пуста) при предыдущем вызове. */ int changed = !(sockmeta4_list_equal(ni->v4_sock_meta, m4_head) && addr4_list_equal(ni->v4_addrs, v4_head) && - sockmeta6_list_equal(ni->v6_sock_meta, m6_head) && addr6_list_equal(ni->v6_addrs, v6_head)); + sockmeta6_list_equal(ni->v6_sock_meta, m6_head) && addr6_list_equal(ni->v6_addrs, v6_head) && + reality_sock_list_equal(ni->reality_socks, r_head)); if (changed) { free_v4_sock_list(instance->topo_groups->v4_sock_meta_pool, ni->v4_sock_meta); ni->v4_sock_meta = m4_head; free_v4_addr_list(instance->topo_groups->v4_addr_pool, ni->v4_addrs); ni->v4_addrs = v4_head; free_v6_sock_list(instance->topo_groups->v6_sock_meta_pool, ni->v6_sock_meta); ni->v6_sock_meta = m6_head; free_v6_addr_list(instance->topo_groups->v6_addr_pool, ni->v6_addrs); ni->v6_addrs = v6_head; + free_reality_sock_list(ni->reality_socks); ni->reality_socks = r_head; ni->ver = (ni->ver % 255) + 1; topo_node_sign_self(instance, ni); DEBUG_INFO(DEBUG_CATEGORY_BGP, "my addresses updated, new ver=%d", ni->ver); @@ -899,6 +1002,7 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { free_v4_addr_list(instance->topo_groups->v4_addr_pool, v4_head); free_v6_sock_list(instance->topo_groups->v6_sock_meta_pool, m6_head); free_v6_addr_list(instance->topo_groups->v6_addr_pool, v6_head); + free_reality_sock_list(r_head); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "my addresses unchanged (ver=%d), persist DB", ni->ver); } diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index 3a6b57b2..b09b1404 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -33,6 +33,7 @@ extern "C" { struct TOPO_GROUP; struct TOPO_GROUPS; struct ETCP_SOCKET; +struct ETCP_LINK; struct NODE_CONN_DIRECT; struct ETCP_CONN; struct UTUN_INSTANCE; @@ -112,12 +113,24 @@ struct TOPOMSG_ADDR6 { uint8_t addr[16]; uint16_t port; uint8_t type, socket struct TOPOMSG_SUBNET4 { uint8_t addr[4]; uint8_t prefix_length; } __attribute__((packed)); struct TOPOMSG_SUBNET6 { uint8_t addr[16]; uint8_t prefix_length; } __attribute__((packed)); +/* Reality-камуфляж сокета: что нужно клиенту для подключения к reality TCP-сокету. + * short_id(8) + server_pubkey(32) + version(3) + server_name(фиксированный 64). */ +#define TOPO_REALITY_SNAME_MAX 64 +struct TOPOMSG_REALITY_SOCK { + uint8_t socket_id; + uint8_t short_id[8]; + uint8_t server_pubkey[32]; + uint8_t version[3]; + uint8_t server_name[TOPO_REALITY_SNAME_MAX]; +} __attribute__((packed)); + struct TOPOMSG_NODE { uint8_t flags; uint64_t group_id, node_id; uint8_t ver; uint8_t public_key[SC_PUBKEY_SIZE], ed25519_public_key[SC_PUBKEY_SIZE]; uint8_t x25519_self_sig[64]; uint8_t node_name_len, local_v4_sockets, local_v4_addrs, local_v6_sockets, local_v6_addrs; uint8_t local_v4_subnets, local_v6_subnets, hop_count; + uint8_t local_reality_socks; // кол-во reality-сокетов uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE uint16_t cumulative_rtt; @@ -132,6 +145,16 @@ struct TOPO_ADDR6 { struct TOPO_ADDR6* next; uint8_t addr[16]; uint16_t port struct TOPO_SUBNET4 { struct TOPO_SUBNET4* next; uint8_t addr[4]; uint8_t prefix_length; }; struct TOPO_SUBNET6 { struct TOPO_SUBNET6* next; uint8_t addr[16]; uint8_t prefix_length; }; +/* Reality-камуфляж сокета (in-memory): per-socket список, значения на узел одинаковы. */ +struct TOPO_REALITY_SOCK { + struct TOPO_REALITY_SOCK* next; + uint8_t socket_id; + uint8_t short_id[8]; + uint8_t server_pubkey[32]; + uint8_t version[3]; + char server_name[TOPO_REALITY_SNAME_MAX]; +}; + struct _topo_head { struct _topo_head* next; }; static inline int topo_list_count(struct _topo_head* head) { int n = 0; while (head) { n++; head = head->next; } return n; } @@ -149,6 +172,7 @@ struct TOPO_NODE { struct TOPO_ADDR4* v4_addrs; struct TOPO_SOCKMETA6* v6_sock_meta; struct TOPO_ADDR6* v6_addrs; + struct TOPO_REALITY_SOCK* reality_socks; // reality-камуфляж сокетов (per-socket, значения на узел) }; struct TOPO_NODESUBNETS { @@ -222,6 +246,12 @@ static inline const struct TOPO_ADDR6* topo_v6_addrs(const struct TOPO_NODE* static inline const struct TOPO_SUBNET4* topo_v4_subnets(const struct TOPO_NODESUBNETS* r) { return r ? r->v4_subnets : NULL; } static inline const struct TOPO_SUBNET6* topo_v6_subnets(const struct TOPO_NODESUBNETS* r) { return r ? r->v6_subnets : NULL; } +/* Найти reality-инфо сокета по socket_id (NULL, если сокет без reality). */ +const struct TOPO_REALITY_SOCK* topo_node_find_reality_sock(const struct TOPO_NODE* ni, uint8_t socket_id); + +/* Заполнить link->reality из reality-инфо пира (rs из BGP). link->reality_set = 1. */ +void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_SOCK* rs); + #ifdef __cplusplus } #endif diff --git a/src/transport_layer/etcp_connect.c b/src/transport_layer/etcp_connect.c index 231f75fe..9d5d0f70 100644 --- a/src/transport_layer/etcp_connect.c +++ b/src/transport_layer/etcp_connect.c @@ -87,7 +87,7 @@ static void connect_create_links_v4(struct ETCP_CONNECT* ctx, struct TOPO_GROUP_ struct ETCP_SOCKET* s = ctx->instance->etcp_sockets; while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET) { struct ETCP_LINK *tlink = etcp_link_new(ctx->conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; addr_links++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; addr_links++; } } s = s->next; } } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[etcp_connect] link_v4: %d.%d.%d.%d:%d proto=%d links=%d conn=[%s]", @@ -128,7 +128,7 @@ static void connect_create_links_v6(struct ETCP_CONNECT* ctx, struct TOPO_GROUP_ struct ETCP_SOCKET* s = ctx->instance->etcp_sockets; while (s) { if (s->is_tcp && s->local_addr.ss_family == AF_INET6) { struct ETCP_LINK *tlink = etcp_link_new(ctx->conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; addr_links++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; addr_links++; } } s = s->next; } } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[etcp_connect] link_v6: %s:%d proto=%d links=%d conn=[%s]", diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index e44c6f0f..00b335ba 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -729,6 +729,7 @@ struct ETCP_SOCKET* tcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SE if (!ts) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_socket_add: alloc failed"); return NULL; } ts->instance = instance; ts->is_tcp = 1; + ts->reality_enabled = server->reality_enabled; ts->fd = SOCKET_INVALID; ts->type = server->type; if (server->name && server->name[0]) { diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 71a810f4..8ba17b5c 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -9,6 +9,7 @@ extern "C" { // подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover) #include "secure_channel.h" +#include "reality.h" #include "utun_instance.h" #include "etcp_bbr.h" #include "../lib/socket_compat.h" @@ -146,6 +147,7 @@ struct ETCP_SOCKET { uint8_t type; // CFG_SERVER_TYPE_* uint8_t sock_id; // unique id (0-255) for NAT matching uint8_t is_tcp; // 1=TCP (STCP), 0=UDP + uint8_t reality_enabled; // 1=TCP-порт с REALITY-камуфляжем uint8_t only_local; // 1 = only local, no forwarding (UDP) uint32_t netif_index; // для sin6_scope_id struct sockaddr_storage local_addr; @@ -322,6 +324,8 @@ struct ETCP_LINK { struct sockaddr_storage local_bound_addr; // локальный адрес к которому привязан линк (interface_addr на момент создания) uint8_t is_tcp; // 1 = TCP транспорт (STCP) + uint8_t reality_set; // 1 = link->reality валиден (реклама пира из BGP) + struct reality_client_config reality; // reality-параметры пира (для исходящего TCP) struct stcp_link *tcp_link; // STCP линк (для TCP) void *tcp_reconnect_timer; // таймер реконнекта (TCP клиент) uint32_t tcp_reconnect_delay_ms; // задержка реконнекта (экспоненциальный backoff) diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index b221bf73..4a0f5df0 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -169,7 +169,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { if (!s->is_tcp || s->local_addr.ss_family != AF_INET || s->type == CFG_SERVER_TYPE_PRIVATE) continue; struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } } } if (a->protocol & TOPO_PROTO_UDP) { @@ -201,7 +201,7 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni, for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) { if (!s->is_tcp || s->local_addr.ss_family != AF_INET6 || s->type == CFG_SERVER_TYPE_PRIVATE) continue; struct ETCP_LINK* tlink = etcp_link_new(conn, s, &sa, 0); - if (tlink) { tlink->is_tcp = 1; etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } + if (tlink) { tlink->is_tcp = 1; topo_node_apply_reality(tlink, topo_node_find_reality_sock(ni, a->socket_id)); etcp_tcp_link_start_connect(tlink, &sa, a->port); link_count++; } } } if (a->protocol & TOPO_PROTO_UDP) { diff --git a/src/transport_layer/stcp_link.c b/src/transport_layer/stcp_link.c index 413ad149..383ec8d7 100644 --- a/src/transport_layer/stcp_link.c +++ b/src/transport_layer/stcp_link.c @@ -236,19 +236,9 @@ struct stcp_link *stcp_link_connect(struct ETCP_LINK *etcp_link, const uint8_t *pubkey = etcp->crypto_ctx.peer_public_key; - // reality-камуфляж на клиенте: включается глобально + есть pubkey сервера + SNI - const struct reality_client_config *reality = NULL; - struct reality_client_config rcc; - if (inst->config && inst->config->global.reality.enabled && - inst->config->global.reality.has_public_key && - inst->config->global.reality.server_name[0]) { - memcpy(rcc.server_static_pubkey, inst->config->global.reality.public_key, REALITY_AUTH_KEY_SIZE); - memcpy(rcc.short_id, inst->config->global.reality.short_id, REALITY_SHORT_ID_SIZE); - memcpy(rcc.version, inst->config->global.reality.version, REALITY_VERSION_SIZE); - snprintf(rcc.server_name, sizeof(rcc.server_name), "%s", inst->config->global.reality.server_name); - rcc.fingerprint = inst->config->global.reality.fingerprint; - reality = &rcc; - } + // reality-камуфляж на клиенте: только если у пира есть реклама reality-сокета в BGP + // (etcp_link->reality_set). Глобальный [reality] для исходящих не используется. + const struct reality_client_config *reality = etcp_link->reality_set ? &etcp_link->reality : NULL; link->cli = stcp_client_connect(inst->ua, addr_str, rport, &inst->my_keys, pubkey, inst->my_ed25519_pubkey, diff --git a/tests/Makefile.am b/tests/Makefile.am index ea433063..6dbef537 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -76,6 +76,7 @@ check_PROGRAMS = \ test_media_delivery_full \ test_etcp_link_stress \ test_reality_hello \ + test_reality_bgp \ bench_timeout_heap \ bench_uasync_timeouts @@ -127,6 +128,10 @@ test_reality_hello_SOURCES = test_reality_hello.c test_reality_hello_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib test_reality_hello_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_reality_bgp_SOURCES = test_reality_bgp.c +test_reality_bgp_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/lib +test_reality_bgp_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_transport_SOURCES = test_stcp_link.c test_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_transport_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_reality_bgp.c b/tests/test_reality_bgp.c new file mode 100644 index 00000000..449b2c39 --- /dev/null +++ b/tests/test_reality_bgp.c @@ -0,0 +1,94 @@ +// test_reality_bgp.c — тест reality-сокетов в BGP (topo_node) +// +// Проверяет: +// 1. topo_node_find_reality_sock — поиск по socket_id. +// 2. topo_node_apply_reality — заполнение reality_client_config линка. +// 3. Serialize → deserialize round-trip reality-сокетов. +#include "reality.h" +#include "topo_node.h" +#include "topo_group.h" +#include "etcp_connections.h" +#include "../lib/debug_config.h" +#include +#include + +static int test_failed = 0; + +#define CHECK(expr, msg) do { \ + if (!(expr)) { \ + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: %s", msg); \ + test_failed = 1; \ + } else { \ + DEBUG_INFO(DEBUG_CATEGORY_BGP, "PASS: %s", msg); \ + } \ +} while (0) + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_INFO); + debug_set_categories(DEBUG_CATEGORY_BGP); + + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== Reality BGP Test ==="); + + // 1. узел с двумя reality-сокетами + struct TOPO_NODE ni; memset(&ni, 0, sizeof(ni)); + struct TOPO_REALITY_SOCK r1, r2; + memset(&r1, 0, sizeof(r1)); memset(&r2, 0, sizeof(r2)); + r1.socket_id = 3; r2.socket_id = 7; + r1.short_id[0] = 0xAA; r2.short_id[0] = 0xBB; + r1.version[0] = 1; r1.version[1] = 2; r1.version[2] = 3; + for (int i = 0; i < 32; i++) r1.server_pubkey[i] = (uint8_t)(i + 1); + strcpy(r1.server_name, "www.example.com"); + strcpy(r2.server_name, "api.example.com"); + r1.next = &r2; r2.next = NULL; + ni.reality_socks = &r1; + + // 2. find + CHECK(topo_node_find_reality_sock(&ni, 3) == &r1, "find sock 3"); + CHECK(topo_node_find_reality_sock(&ni, 7) == &r2, "find sock 7"); + CHECK(topo_node_find_reality_sock(&ni, 99) == NULL, "find missing socket"); + CHECK(topo_node_find_reality_sock(NULL, 3) == NULL, "find on NULL node"); + + // 3. apply + struct ETCP_LINK link; memset(&link, 0, sizeof(link)); + topo_node_apply_reality(&link, &r1); + CHECK(link.reality_set == 1, "apply sets reality_set"); + CHECK(memcmp(link.reality.short_id, r1.short_id, 8) == 0, "apply short_id"); + CHECK(memcmp(link.reality.server_static_pubkey, r1.server_pubkey, 32) == 0, "apply pubkey"); + CHECK(memcmp(link.reality.version, r1.version, 3) == 0, "apply version"); + CHECK(strcmp(link.reality.server_name, "www.example.com") == 0, "apply server_name"); + topo_node_apply_reality(&link, NULL); + CHECK(link.reality_set == 1, "apply NULL rs is no-op"); + + // 4. serialize → deserialize round-trip + struct TOPO_GROUP_NODE nq; memset(&nq, 0, sizeof(nq)); + uint8_t buf[2048]; + int len = topo_node_serialize(&ni, &nq, 0x1234, 0, buf, sizeof(buf), 0); + CHECK(len > 0, "serialize ok"); + struct TOPO_GROUP g; memset(&g, 0, sizeof(g)); + struct TOPO_NODE* out_ni = NULL; + struct TOPO_NODESUBNETS* out_sub = NULL; + uint64_t* hop = NULL; uint8_t hopc = 0; uint16_t rtt = 0; + int rc = topo_node_deserialize(&g, buf, len, &out_ni, &out_sub, &hop, &hopc, &rtt); + CHECK(rc == 0 && out_ni != NULL, "deserialize ok"); + if (out_ni) { + int cnt = 0; for (const struct TOPO_REALITY_SOCK* r = out_ni->reality_socks; r; r = r->next) cnt++; + CHECK(cnt == 2, "round-trip: 2 reality socks"); + const struct TOPO_REALITY_SOCK* rr = topo_node_find_reality_sock(out_ni, 7); + CHECK(rr != NULL, "round-trip: find sock 7"); + if (rr) { + CHECK(rr->short_id[0] == 0xBB, "round-trip: short_id"); + CHECK(strcmp(rr->server_name, "api.example.com") == 0, "round-trip: server_name"); + } + const struct TOPO_REALITY_SOCK* rr1 = topo_node_find_reality_sock(out_ni, 3); + CHECK(rr1 != NULL && memcmp(rr1->server_pubkey, r1.server_pubkey, 32) == 0, "round-trip: pubkey"); + CHECK(rr1 != NULL && rr1->version[0] == 1 && rr1->version[1] == 2 && rr1->version[2] == 3, "round-trip: version"); + } + + if (test_failed) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: FAILED ==="); + return 1; + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: PASSED ==="); + return 0; +} diff --git a/utun.conf.sample b/utun.conf.sample index e2b2a8b8..fa082325 100644 --- a/utun.conf.sample +++ b/utun.conf.sample @@ -113,6 +113,29 @@ allow=all #netif=eth1 ; Linux: SO_BINDTODEVICE (interface name) #bind_ip=10.0.0.1 ; bind() local IP before connect() +# --- REALITY-камуфляж (reality socket) --- +# REALITY маскирует TCP-порт STCP под обычный TLS-трафик к реальному сайту. +# Сервер принимает TLS ClientHello, проверяет авторизацию (short_id + X25519) +# и отвечает ServerHello; неавторизованных клиентов релеит на реальный сайт (dest). +# Для включения нужен TCP-сокет: в [server: ...] задать transport=tcp и reality=1. +#[reality] +#enabled=1 +#server_name=www.microsoft.com # SNI-таргет (каким сайтом прикидываемся) +#dest=www.microsoft.com:443 # host:port реального сайта для релея +#private_key=<64 hex X25519> # static приватный ключ сервера +#short_ids=0102030405060708,aabbccddeeff0011 # список short_id (по 16 hex, через запятую) +##short_id=0102030405060708 # одиночный short_id (альтернатива short_ids) +#version=1.0.0 # версия протокола utun (по умолчанию 1.0.0) +#time_window=30 # допуск timestamp, сек (антиреплей) +#fingerprint=chrome # TLS-отпечаток (только chrome) + +## Пример TCP-сокета с reality-камуфляжем: +#[server: reality_srv] +#addr=0.0.0.0:1443 +#type=public +#transport=tcp +#reality=1 + # --- Chatserver: headless supernode config --- #[chatserver] #db_path=/var/lib/utun # путь к SQLite базе данных (обязательно)