Browse Source

chat: invite-ссылки с несколькими адресами + ncd-адреса узлов (join)

v2
evgeny 2 weeks ago
parent
commit
481872d5db
  1. 20
      src/chat/chat_headless_control.c
  2. 3
      src/chat/chat_status.c
  3. 65
      src/chat/chat_sync.c
  4. 47
      src/chat/invite_build.c
  5. 60
      src/chat/invite_link.c
  6. 14
      src/chat/invite_link.h
  7. 38
      src/config_parser.c
  8. 2
      src/routing_layer/topo_group_invite.c
  9. 16
      src/routing_layer/topo_node.c
  10. 14
      src/routing_layer/topo_node.h
  11. 88
      src/routing_layer/topo_node_sqlite.c
  12. 2
      src/transport_layer/node_conn_direct.c
  13. 77
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt
  14. 61
      tools/chatgui-android/libutun_lite/invite_link_c.c
  15. 15
      tools/chatgui-android/libutun_lite/invite_link_c.h
  16. 2
      tools/chatgui/db/db_manager.h
  17. 14
      tools/chatgui/src/accountlist.cpp
  18. 1
      tools/chatgui/src/accountlist.h
  19. 45
      tools/chatgui/src/invite_link.cpp
  20. 24
      tools/chatgui/src/invite_link.h
  21. 10
      tools/chatgui/src/inviteby.cpp
  22. 10
      tools/chatgui/src/joindialog.cpp
  23. 1
      tools/chatgui/src/nodespage.cpp
  24. 2
      tools/chatgui/transport/gui_bridge.h

20
src/chat/chat_headless_control.c

@ -501,18 +501,20 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
int port = (int)strtol(colon + 1, NULL, 10);
if (port <= 0 || port > 65535) { send_response(cli, id, NULL, "invalid port"); return; }
uint8_t addrs_data[21]; int addrs_len = 0;
uint8_t addrs_data[22]; int addrs_len = 0;
struct in_addr a4; struct in6_addr a6;
if (inet_pton(AF_INET, ip, &a4) == 1) {
addrs_data[0] = 4; addrs_data[1] = 0; addrs_data[2] = (uint8_t)proto;
memcpy(addrs_data + 3, &a4.s_addr, 4);
addrs_data[7] = (uint8_t)(port >> 8); addrs_data[8] = (uint8_t)(port & 0xFF);
addrs_len = 9;
addrs_data[0] = 0; /* reality-префикс: has=0 */
addrs_data[1] = 4; addrs_data[2] = 0; addrs_data[3] = (uint8_t)proto;
memcpy(addrs_data + 4, &a4.s_addr, 4);
addrs_data[8] = (uint8_t)(port >> 8); addrs_data[9] = (uint8_t)(port & 0xFF);
addrs_len = 10;
} else if (inet_pton(AF_INET6, ip, &a6) == 1) {
addrs_data[0] = 6; addrs_data[1] = 0; addrs_data[2] = (uint8_t)proto;
memcpy(addrs_data + 3, &a6, 16);
addrs_data[19] = (uint8_t)(port >> 8); addrs_data[20] = (uint8_t)(port & 0xFF);
addrs_len = 21;
addrs_data[0] = 0; /* reality-префикс: has=0 */
addrs_data[1] = 6; addrs_data[2] = 0; addrs_data[3] = (uint8_t)proto;
memcpy(addrs_data + 4, &a6, 16);
addrs_data[20] = (uint8_t)(port >> 8); addrs_data[21] = (uint8_t)(port & 0xFF);
addrs_len = 22;
} else { send_response(cli, id, NULL, "invalid addr ip"); return; }
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",

3
src/chat/chat_status.c

@ -316,7 +316,7 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) {
#define MEMBER_DETAIL_MAX_LINKS 16
#define MEMBER_DETAIL_MAX_SOCKS 16
#define MEMBER_DETAIL_LINK_SIZE 63
#define MEMBER_DETAIL_LINK_SIZE 64
#define MEMBER_DETAIL_SOCK_SIZE 48
/* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */
@ -434,6 +434,7 @@ static void collect_member_detail(uint64_t node_id) {
if (lk->conn) strncpy(sn, lk->conn->name, 19);
memcpy(p, sn, 20); p += 20;
}
*p++ = lk->reality_set ? 1 : 0;
}
*p++ = sock_count;

65
src/chat/chat_sync.c

@ -719,11 +719,46 @@ struct chat_invite {
uint64_t node_id;
uint8_t pubkey[32];
uint8_t* addrs_data;
int addrs_data_len;
int addr_count;
};
struct cm_invite_wrap { struct chat_invite inv; };
/* Парсит reality-префикс addrs_data: has(1) + [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)].
Сдвигает *ap за префикс. Возвращает 1, если reality присутствует (и params заполнены). */
static int invite_reality_parse(const uint8_t** ap, const uint8_t* end,
uint8_t short_id[8], uint8_t pubkey[32], uint8_t ver[3],
char* sname, size_t sname_sz) {
const uint8_t* p = *ap;
if (p + 1 > end) return 0;
if (!*p++) { *ap = p; return 0; }
if (p + 8 + 32 + 3 + 1 > end) { *ap = end; return 0; }
memcpy(short_id, p, 8); p += 8;
memcpy(pubkey, p, 32); p += 32;
memcpy(ver, p, 3); p += 3;
uint8_t snlen = *p++;
if (snlen > (uint8_t)(sname_sz - 1)) snlen = (uint8_t)(sname_sz - 1);
if (p + snlen > end) { *ap = end; return 0; }
memcpy(sname, p, snlen); sname[snlen] = '\0'; p += snlen;
*ap = p;
return 1;
}
/* Добавляет reality-сокет узла с node-global параметрами. */
static void invite_reality_sock_append(struct TOPO_NODE* ni, uint8_t socket_id,
const uint8_t short_id[8], const uint8_t pubkey[32],
const uint8_t ver[3], const char* sname) {
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (!r) return;
r->socket_id = socket_id;
memcpy(r->short_id, short_id, 8);
memcpy(r->server_pubkey, pubkey, 32);
memcpy(r->version, ver, 3);
snprintf(r->server_name, sizeof(r->server_name), "%s", sname);
r->next = ni->reality_socks; ni->reality_socks = r;
}
static void cm_invite_trampoline(void* arg) {
struct cm_invite_wrap* w = (struct cm_invite_wrap*)arg;
struct chat_invite* inv = &w->inv;
@ -736,27 +771,28 @@ static void cm_invite_trampoline(void* arg) {
if (!ni) { u_free(w); return; }
ni->node_id = node_id; memcpy(ni->public_key, inv->pubkey, 32);
const uint8_t* ap = inv->addrs_data;
for (int i = 0; i < inv->addr_count; i++) {
const uint8_t* ap_end = inv->addrs_data + inv->addrs_data_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < inv->addr_count && ap + 3 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
uint16_t port = 0;
if (family == 4) { port = ((uint16_t)ap[4] << 8) | ap[5]; }
else if (family == 6) { port = ((uint16_t)ap[16] << 8) | ap[17]; }
const char* proto_str = proto == 1 ? "UDP" : proto == 2 ? "TCP" : proto == 3 ? "UDP+TCP" : "?";
if (family == 4) {
if (family == 4 && ap + 6 <= ap_end) {
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
} else if (family == 6) {
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
} else { ap += 18; }
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}
{ char buf[512]; int off = 0; int n4 = 0, n6 = 0;
@ -836,6 +872,7 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
inv->addrs_data = u_malloc(addrs_sz);
if (!inv->addrs_data) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: connect_from_invite malloc(%zu) failed", CS_ID, addrs_sz); u_free(inv); return; }
memcpy(inv->addrs_data, addrs_data, addrs_sz);
inv->addrs_data_len = (int)addrs_sz;
inv->addr_count = addr_count;
g_cs->pending_invite_ch_id = channel_id;
@ -1563,6 +1600,8 @@ static void cs_invite_trampoline(void* arg) {
const uint8_t* ap = w->addrs_data;
const uint8_t* ap_end = ap + w->addrs_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < w->addr_count && ap + 1 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family == 4 && ap + 6 <= ap_end) {
@ -1570,15 +1609,17 @@ static void cs_invite_trampoline(void* arg) {
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}

47
src/chat/invite_build.c

@ -172,14 +172,15 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
for (s = g_cc.inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
if (!s->is_tcp) continue;
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
uint8_t proto = INVITE_PROTO_TCP | (s->reality_enabled ? INVITE_PROTO_REALITY : 0);
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_TCP;
out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = proto;
memcpy(out[cnt].address, &sin->sin_addr, 4);
out[cnt].port = ntohs(sin->sin_port); cnt++;
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_TCP;
out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = proto;
memcpy(out[cnt].address, &sin6->sin6_addr, 16);
out[cnt].port = ntohs(sin6->sin6_port); cnt++;
}
@ -187,17 +188,39 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
return cnt;
}
/* reality-параметры self-узла из конфига (node-global, как в topo_node_update_my_addresses). */
static void collect_self_reality(struct InviteReality* out) {
memset(out, 0, sizeof(*out));
if (!g_cc.inst || !g_cc.inst->config) return;
const struct reality_config* rc = &g_cc.inst->config->global.reality;
if (!rc->enabled || !rc->has_private_key) return;
int any = 0;
for (struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets; s; s = s->next)
if (s->is_tcp && s->reality_enabled) { any = 1; break; }
if (!any) return;
uint8_t pub[32];
if (reality_pubkey_from_priv(rc->private_key, pub) != REALITY_OK) return;
out->has = 1;
if (rc->short_id_count > 0) memcpy(out->short_id, rc->short_ids[0], 8);
else memcpy(out->short_id, rc->short_id, 8);
memcpy(out->server_pubkey, pub, 32);
memcpy(out->version, rc->version, 3);
strncpy(out->server_name, rc->server_name, INVITE_SNAME_MAX - 1);
out->server_name[INVITE_SNAME_MAX - 1] = '\0';
}
/* ── Фаза 4b: адреса узла из БД ── */
static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only) {
static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
struct InviteReality* reality) {
char sql[256];
if (dir_only)
snprintf(sql, sizeof(sql),
"SELECT family,socket_id,protocol,address,port FROM node_addresses"
"SELECT family,socket_id,protocol,address,port,options FROM node_addresses"
" WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT);
else
snprintf(sql, sizeof(sql),
"SELECT family,socket_id,protocol,address,port FROM node_addresses WHERE node_id=?");
"SELECT family,socket_id,protocol,address,port,options FROM node_addresses WHERE node_id=?");
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
@ -215,6 +238,17 @@ static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out,
memset(out[cnt].address, 0, sizeof(out[cnt].address));
if (addr && addr_len >= ip_len) memcpy(out[cnt].address, addr, (size_t)ip_len);
out[cnt].port = (uint16_t)sqlite3_column_int(st, 4);
if (reality && !reality->has && (out[cnt].proto & INVITE_PROTO_REALITY)
&& sqlite3_column_type(st, 5) == SQLITE_BLOB
&& sqlite3_column_bytes(st, 5) == TOPO_ADDR_REALITY_OPTS_SIZE) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(st, 5);
reality->has = 1;
memcpy(reality->short_id, o->short_id, 8);
memcpy(reality->server_pubkey, o->server_pubkey, 32);
memcpy(reality->version, o->version, 3);
memcpy(reality->server_name, o->server_name, INVITE_SNAME_MAX);
reality->server_name[INVITE_SNAME_MAX - 1] = '\0';
}
cnt++;
}
sqlite3_finalize(st);
@ -284,9 +318,10 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
/* ── адреса ── */
if (use_self) {
data.addrCount = (uint8_t)collect_self_addrs(data.addrs, INVITE_ADDR_MAX);
collect_self_reality(&data.reality);
} else {
int dir_only = has_both_public ? 1 : 0;
data.addrCount = (uint8_t)collect_node_addrs_from_db(target_nid, data.addrs, INVITE_ADDR_MAX, dir_only);
data.addrCount = (uint8_t)collect_node_addrs_from_db(target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
}
if (data.addrCount == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no addrs for node=0x%016llx",

60
src/chat/invite_link.c

@ -63,12 +63,13 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteData* out
int off = 0;
uint8_t ver = raw[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02) {
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "unsupported version 0x%02x", ver); return -1;
}
out->password[0] = '\0'; out->password_len = 0;
out->reality.has = 0;
if (ver == 0x02) {
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pass_len"); return -1; }
uint8_t plen = raw[off++];
if (plen > 0) {
@ -89,6 +90,22 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteData* out
for (int i = 0; i < 8; i++) jk = (jk << 8) | raw[off++];
out->join_key = jk;
if (ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality"); return -1; }
if (raw[off++]) {
if (off + 8 + 32 + 3 + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality params"); return -1; }
memcpy(out->reality.short_id, raw + off, 8); off += 8;
memcpy(out->reality.server_pubkey, raw + off, 32); off += 32;
memcpy(out->reality.version, raw + off, 3); off += 3;
uint8_t snlen = raw[off++];
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
if (off + snlen > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality server_name"); return -1; }
memcpy(out->reality.server_name, raw + off, snlen); out->reality.server_name[snlen] = '\0';
off += snlen;
out->reality.has = 1;
}
}
memset(out->pubkey, 0, INVITE_PUBKEY_SIZE);
out->addrCount = 0;
@ -117,10 +134,25 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteData* out
int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t buf_size) {
if (!data || !buf) return -1;
size_t needed = 0;
size_t needed = 1;
if (data->reality.has) {
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
needed += 8 + 32 + 3 + 1 + snlen;
}
for (int i = 0; i < (int)data->addrCount; i++) needed += (data->addrs[i].family == 6) ? 21U : 9U;
if (needed > buf_size) return -1;
size_t pos = 0;
buf[pos++] = data->reality.has ? 1 : 0;
if (data->reality.has) {
memcpy(buf + pos, data->reality.short_id, 8); pos += 8;
memcpy(buf + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(buf + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
buf[pos++] = (uint8_t)snlen;
memcpy(buf + pos, data->reality.server_name, snlen); pos += snlen;
}
for (int i = 0; i < (int)data->addrCount; i++) {
const struct InviteAddr* a = &data->addrs[i];
int ip_len = (a->family == 6) ? 16 : 4;
@ -134,16 +166,28 @@ int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t b
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) {
if (!data || !out || data->pubkey[0] == 0) return -1;
if (data->addrCount == 0) return -1;
int has_pass = (password && password[0]) ? 1 : 0;
uint8_t raw[4096]; size_t pos = 0;
raw[pos++] = has_pass ? 0x02 : INVITE_LINK_VERSION;
if (has_pass) {
size_t plen = strlen(password);
raw[pos++] = INVITE_LINK_VERSION;
{
size_t plen = (password && password[0]) ? strlen(password) : 0;
if (plen > INVITE_PASS_MAX - 1) return -1;
raw[pos++] = (uint8_t)plen; memcpy(raw + pos, password, plen); pos += plen;
raw[pos++] = (uint8_t)plen;
if (plen) { memcpy(raw + pos, password, plen); pos += plen; }
}
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->channelId >> (i * 8)) & 0xFF);
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->join_key >> (i * 8)) & 0xFF);
raw[pos++] = data->reality.has ? 1 : 0;
if (data->reality.has) {
memcpy(raw + pos, data->reality.short_id, 8); pos += 8;
memcpy(raw + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(raw + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
raw[pos++] = (uint8_t)snlen;
memcpy(raw + pos, data->reality.server_name, snlen); pos += snlen;
}
uint8_t pk[32]; memcpy(pk, data->pubkey, 32);
int ia = 0;

14
src/chat/invite_link.h

@ -10,13 +10,15 @@
#include <stdint.h>
#include <stddef.h>
#define INVITE_LINK_VERSION 0x01
#define INVITE_LINK_VERSION 0x03
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
#define INVITE_PASS_MAX 128
#define INVITE_SNAME_MAX 64
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_REALITY 0x04 /* флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06) */
#ifdef __cplusplus
extern "C" {
@ -30,6 +32,15 @@ struct InviteAddr {
uint16_t port;
};
/* Reality-параметры узла (node-global: одинаковы для всех reality-сокетов). */
struct InviteReality {
uint8_t has; /* 1 = reality присутствует в ссылке */
uint8_t short_id[8];
uint8_t server_pubkey[32];
uint8_t version[3];
char server_name[INVITE_SNAME_MAX];
};
struct InviteData {
uint64_t channelId;
uint64_t join_key; /* 64-битный ключ входа (генерируется инвайтером, хранится на connection-узле 10 мин) */
@ -39,6 +50,7 @@ struct InviteData {
struct InviteAddr addrs[INVITE_ADDR_MAX];
char password[INVITE_PASS_MAX];
uint8_t password_len;
struct InviteReality reality;
};
int invite_link_decode(const char* link, size_t link_len, struct InviteData* out,

38
src/config_parser.c

@ -1631,20 +1631,23 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct
uint8_t proto;
} addrs[32];
int addr_count = 0;
int has_reality = 0;
for (const struct CFG_SERVER *s = servers; s && addr_count < 32; s = s->next) {
uint8_t proto = (s->transport == 1) ? 0x02 : 0x01;
if (s->transport == 1 && s->reality_enabled) { proto |= 0x04; has_reality = 1; }
if (s->ip.ss_family == AF_INET) {
struct sockaddr_in *sin = (struct sockaddr_in *)&s->ip;
addrs[addr_count].family = 4;
memcpy(addrs[addr_count].addr, &sin->sin_addr, 4);
addrs[addr_count].port = ntohs(sin->sin_port);
addrs[addr_count].proto = (s->transport == 1) ? 0x02 : 0x01;
addrs[addr_count].proto = proto;
addr_count++;
} else if (s->ip.ss_family == AF_INET6) {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&s->ip;
addrs[addr_count].family = 6;
memcpy(addrs[addr_count].addr, &sin6->sin6_addr, 16);
addrs[addr_count].port = ntohs(sin6->sin6_port);
addrs[addr_count].proto = (s->transport == 1) ? 0x02 : 0x01;
addrs[addr_count].proto = proto;
addr_count++;
}
}
@ -1653,18 +1656,37 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct
return;
}
/* reality-параметры (node-global) из секции [reality] */
uint8_t r_pubkey[32];
int r_valid = 0;
if (has_reality && g->reality.enabled && g->reality.has_private_key
&& reality_pubkey_from_priv(g->reality.private_key, r_pubkey) == REALITY_OK)
r_valid = 1;
/* Build binary blob */
uint8_t raw[4096];
size_t pos = 0;
int has_pass = g->chatserver_join_password[0] ? 1 : 0;
raw[pos++] = has_pass ? 0x02 : 0x01; /* version */
if (has_pass) {
size_t plen = strlen(g->chatserver_join_password);
raw[pos++] = 0x03; /* version */
{ size_t plen = g->chatserver_join_password[0] ? strlen(g->chatserver_join_password) : 0;
raw[pos++] = (uint8_t)plen;
memcpy(raw + pos, g->chatserver_join_password, plen); pos += plen;
}
if (plen) { memcpy(raw + pos, g->chatserver_join_password, plen); pos += plen; } }
/* channel_id = 0 (8 bytes BE) */
for (int i = 0; i < 8; i++) raw[pos++] = 0;
/* join_key = 0 (8 bytes BE) */
for (int i = 0; i < 8; i++) raw[pos++] = 0;
/* reality-блок */
raw[pos++] = r_valid ? 1 : 0;
if (r_valid) {
if (g->reality.short_id_count > 0) memcpy(raw + pos, g->reality.short_ids[0], 8);
else memcpy(raw + pos, g->reality.short_id, 8);
pos += 8;
memcpy(raw + pos, r_pubkey, 32); pos += 32;
memcpy(raw + pos, g->reality.version, 3); pos += 3;
size_t snlen = strlen(g->reality.server_name);
if (snlen > 63) snlen = 63;
raw[pos++] = (uint8_t)snlen;
memcpy(raw + pos, g->reality.server_name, snlen); pos += snlen;
}
int ia = 0;
while (ia < addr_count) {

2
src/routing_layer/topo_group_invite.c

@ -495,7 +495,7 @@ int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id,
for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next)
aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port,
a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : "?");
a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : (a->protocol & TOPO_PROTO_REALITY) ? "TCP+REALITY" : "?");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%dms",
(unsigned long long)nid, addrs, TGI_TIMEOUT_MS);
}

16
src/routing_layer/topo_node.c

@ -654,7 +654,7 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
}}
{ const char* atn[] = {"INTERFACE","NAT","REAL"};
int ai = 0; struct TOPO_ADDR4* a = ni->v4_addrs; while (a) {
const char* proto = (a->protocol & TOPO_PROTO_TCP) ? "tcp" : (a->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
const char* proto = (a->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a->protocol & TOPO_PROTO_TCP) ? "tcp" : (a->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v4addr[%d]: %s:%u %s sock=%u proto=%s", ai++,
ip_to_str(a->addr, AF_INET).str, a->port, (a->type<=2)?atn[a->type]:"?", a->socket_id, proto);
a = a->next;
@ -664,7 +664,7 @@ void topo_node_dump_all(struct TOPO_GROUP* group) {
sm6 = sm6->next;
}}
{ int ai6 = 0; struct TOPO_ADDR6* a6 = ni->v6_addrs; while (a6) {
const char* proto = (a6->protocol & TOPO_PROTO_TCP) ? "tcp" : (a6->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
const char* proto = (a6->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a6->protocol & TOPO_PROTO_TCP) ? "tcp" : (a6->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
DEBUG_INFO(DEBUG_CATEGORY_BGP, " v6addr[%d]: %s:%u type=%u sock=%u proto=%s", ai6++,
ip_to_str(a6->addr, AF_INET6).str, a6->port, a6->type, a6->socket_id, proto);
a6 = a6->next;
@ -742,7 +742,7 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
FMT_ADD(" v4sock[%d]: id=%u config=%u nat=%u\n", si++, sm->id, sm->config_type, sm->nat_type); sm = sm->next;
}}
{ const char* atn[] = {"INTERFACE","NAT","REAL"}; struct TOPO_ADDR4* a = ni->v4_addrs; int ai = 0; while (a) {
const char* proto = (a->protocol & TOPO_PROTO_TCP) ? "tcp" : (a->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
const char* proto = (a->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a->protocol & TOPO_PROTO_TCP) ? "tcp" : (a->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
FMT_ADD(" v4addr[%d]: %s:%u %s sock=%u proto=%s\n",
ai++, ip_to_str(a->addr, AF_INET).str, a->port, (a->type<=2)?atn[a->type]:"?", a->socket_id, proto);
a = a->next;
@ -751,7 +751,7 @@ int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size) {
FMT_ADD(" v6sock[%d]: id=%u config=%u nat=%u\n", s6i++, sm6->id, sm6->config_type, sm6->nat_type); sm6 = sm6->next;
}}
{ struct TOPO_ADDR6* a6 = ni->v6_addrs; int a6i = 0; while (a6) {
const char* proto = (a6->protocol & TOPO_PROTO_TCP) ? "tcp" : (a6->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
const char* proto = (a6->protocol & TOPO_PROTO_REALITY) ? "tcp+reality" : (a6->protocol & TOPO_PROTO_TCP) ? "tcp" : (a6->protocol & TOPO_PROTO_UDP) ? "udp" : "?";
FMT_ADD(" v6addr[%d]: %s:%u type=%u sock=%u proto=%s\n",
a6i++, ip_to_str(a6->addr, AF_INET6).str, a6->port, a6->type, a6->socket_id, proto);
a6 = a6->next;
@ -976,6 +976,8 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
if (ts->type == CFG_SERVER_TYPE_PRIVATE) { ts = ts->next; continue; } /* private скрыт, LOCAL рекламируем (LAN) */
struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr;
if (!addr || !addr->ss_family) { ts = ts->next; continue; }
const int is_reality = r_valid && ts->reality_enabled;
uint8_t proto = TOPO_PROTO_TCP | (is_reality ? TOPO_PROTO_REALITY : 0);
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
{ struct TOPO_SOCKMETA4* sm = memory_pool_alloc(instance->topo_groups->v4_sock_meta_pool);
@ -983,7 +985,7 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
sm->next = m4_head; m4_head = sm; }
{ struct TOPO_ADDR4* a = memory_pool_alloc(instance->topo_groups->v4_addr_pool);
memcpy(a->addr, &sin->sin_addr.s_addr, 4); a->port = ntohs(sin->sin_port);
a->type = TOPO_ADDR_INTERFACE; a->socket_id = ts->sock_id; a->protocol = TOPO_PROTO_TCP;
a->type = TOPO_ADDR_INTERFACE; a->socket_id = ts->sock_id; a->protocol = proto;
a->next = v4_head; v4_head = a; }
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
@ -992,10 +994,10 @@ int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
sm6->next = m6_head; m6_head = sm6; }
{ struct TOPO_ADDR6* a6 = memory_pool_alloc(instance->topo_groups->v6_addr_pool);
memcpy(a6->addr, &sin6->sin6_addr, 16); a6->port = ntohs(sin6->sin6_port);
a6->type = TOPO_ADDR_INTERFACE; a6->socket_id = ts->sock_id; a6->protocol = TOPO_PROTO_TCP;
a6->type = TOPO_ADDR_INTERFACE; a6->socket_id = ts->sock_id; a6->protocol = proto;
a6->next = v6_head; v6_head = a6; }
}
if (r_valid && ts->reality_enabled) {
if (is_reality) {
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 {

14
src/routing_layer/topo_node.h

@ -42,8 +42,9 @@ struct UTUN_INSTANCE;
#define TOPO_ADDR_NAT 1
#define TOPO_ADDR_REAL 2
#define TOPO_PROTO_UDP 0x01
#define TOPO_PROTO_TCP 0x02
#define TOPO_PROTO_UDP 0x01
#define TOPO_PROTO_TCP 0x02
#define TOPO_PROTO_REALITY 0x04 /* флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06) */
#define ADDR_TYPE_NETIF 0
#define ADDR_TYPE_DIRECT 1
@ -156,6 +157,15 @@ struct TOPO_REALITY_SOCK {
char server_name[TOPO_REALITY_SNAME_MAX];
};
/* Reality-параметры адреса для персистенции в node_addresses.options (BLOB, фиксированный размер). */
#define TOPO_ADDR_REALITY_OPTS_SIZE 107
struct TOPO_ADDR_REALITY_OPTS {
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; }

88
src/routing_layer/topo_node_sqlite.c

@ -86,6 +86,21 @@ static void smeta6_lookup(struct TOPO_SOCKMETA6* head, uint8_t sid, uint8_t* cfg
if (m->id == sid) { *cfg = m->config_type; *nat = m->nat_type; return; }
}
/* reality-параметры сокета → фиксированный blob для node_addresses.options.
Возвращает 1 и заполняет opts, если у сокета есть reality; иначе 0. */
static int reality_opts_build(struct TOPO_NODE* ni, uint8_t sid, struct TOPO_ADDR_REALITY_OPTS* opts) {
if (!ni || !opts) return 0;
for (const struct TOPO_REALITY_SOCK* r = ni->reality_socks; r; r = r->next) {
if (r->socket_id != sid) continue;
memcpy(opts->short_id, r->short_id, 8);
memcpy(opts->server_pubkey, r->server_pubkey, 32);
memcpy(opts->version, r->version, 3);
memcpy(opts->server_name, r->server_name, TOPO_REALITY_SNAME_MAX);
return 1;
}
return 0;
}
/* ── Персистенция адресов узла (источник истины — BGP/topo_group) ── */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni) {
@ -109,17 +124,19 @@ int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE*
sqlite3_stmt* is = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,nat_type,socket_id)"
" VALUES(?,?,?,?,?,?,?,?,?)", -1, &is, NULL) != SQLITE_OK) {
"INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,nat_type,socket_id,options)"
" VALUES(?,?,?,?,?,?,?,?,?,?)", -1, &is, NULL) != SQLITE_OK) {
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
return -1;
}
int written = 0;
struct TOPO_ADDR_REALITY_OPTS ropts;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
uint8_t cfg, nat; smeta4_lookup(ni->v4_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? addr_type_from_ip(4, a->addr)
: addr_type_classify(cfg, nat);
int has_reality = (a->protocol & TOPO_PROTO_REALITY) && reality_opts_build(ni, a->socket_id, &ropts);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 4);
sqlite3_bind_int(is, 3, (int)a->protocol);
@ -129,6 +146,8 @@ int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE*
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
if (has_reality) sqlite3_bind_blob(is, 10, &ropts, sizeof(ropts), SQLITE_STATIC);
else sqlite3_bind_null(is, 10);
if (sqlite3_step(is) != SQLITE_DONE)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "addrs_put: INSERT failed node=%016llx fam=4 sock=%d at=%d cfg=%d nat=%d addr=%d.%d.%d.%d:%d: %s",
(unsigned long long)node_id, a->socket_id, at, cfg, nat,
@ -140,6 +159,7 @@ int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE*
uint8_t cfg, nat; smeta6_lookup(ni->v6_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? ADDR_TYPE_NETIF
: addr_type_classify(cfg, nat);
int has_reality = (a->protocol & TOPO_PROTO_REALITY) && reality_opts_build(ni, a->socket_id, &ropts);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 6);
sqlite3_bind_int(is, 3, (int)a->protocol);
@ -149,6 +169,8 @@ int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE*
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
if (has_reality) sqlite3_bind_blob(is, 10, &ropts, sizeof(ropts), SQLITE_STATIC);
else sqlite3_bind_null(is, 10);
if (sqlite3_step(is) != SQLITE_DONE)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "addrs_put: INSERT failed node=%016llx fam=6 sock=%d at=%d cfg=%d nat=%d: %s",
(unsigned long long)node_id, a->socket_id, at, cfg, nat, sqlite3_errmsg(db));
@ -255,6 +277,7 @@ int topo_node_sqlite_init(sqlite3* db) {
" config_type INTEGER DEFAULT 0,"
" nat_type INTEGER DEFAULT 0,"
" socket_id INTEGER NOT NULL DEFAULT 0,"
" options BLOB," // reality-параметры (TOPO_ADDR_REALITY_OPTS)
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);"
@ -284,6 +307,29 @@ int topo_node_sqlite_init(sqlite3* db) {
return -1;
}
/* migration: добавить колонку options в уже существующую node_addresses (idempotent) */
{
sqlite3_stmt* cs = NULL;
int has_options = 0;
if (sqlite3_prepare_v2(db, "PRAGMA table_info(node_addresses)", -1, &cs, NULL) == SQLITE_OK) {
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* cn = (const char*)sqlite3_column_text(cs, 1);
if (cn && strcmp(cn, "options") == 0) { has_options = 1; break; }
}
sqlite3_finalize(cs);
}
if (!has_options) {
char* aerr = NULL;
int arc = sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN options BLOB", NULL, NULL, &aerr);
if (arc != SQLITE_OK) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: ALTER TABLE ADD options failed: %s", aerr ? aerr : "?");
if (aerr) sqlite3_free(aerr);
} else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: migrated node_addresses (added options column)");
}
}
}
/* backfill: вывести deleted из adm_tags (idempotent, самовосстановление) */
{
sqlite3_stmt* s = NULL;
@ -945,11 +991,12 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL;
struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL;
struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL;
uint8_t sock_id_counter = 0;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type,options FROM node_addresses"
" WHERE node_id=? AND family=4 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
@ -983,6 +1030,22 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
else { m4_tail->next = m4; m4_tail = m4; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
}
sqlite3_finalize(s);
}
@ -993,7 +1056,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type,options FROM node_addresses"
" WHERE node_id=? AND family=6 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
@ -1027,6 +1090,22 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
else { m6_tail->next = m6; m6_tail = m6; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto6 & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
}
sqlite3_finalize(s);
}
@ -1043,6 +1122,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
ni->v6_addrs = v6_head;
ni->v4_sock_meta = m4_head;
ni->v6_sock_meta = m6_head;
ni->reality_socks = r_head;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx loaded from DB name=%s v4=%d v6=%d",
(unsigned long long)node_id, name ? name : "",

2
src/transport_layer/node_conn_direct.c

@ -1098,6 +1098,7 @@ static int ncd_add_tcp_link_random(struct ETCP_CONN* conn, struct TOPO_NODE* ni,
struct ETCP_LINK* tlink = etcp_link_new(conn, sock, &sa, 0);
if (!tlink) return 0;
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);
return 1;
}
@ -1120,6 +1121,7 @@ static int ncd_add_tcp_link_random(struct ETCP_CONN* conn, struct TOPO_NODE* ni,
struct ETCP_LINK* tlink = etcp_link_new(conn, sock, &sa, 0);
if (!tlink) return 0;
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);
return 1;
}

77
tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt

@ -4,19 +4,28 @@ import android.util.Base64
import com.utun.chat.data.LogManager
import java.security.MessageDigest
const val INVITE_LINK_VERSION: Int = 0x01
const val INVITE_LINK_VERSION: Int = 0x03
const val INVITE_PUBKEY_SIZE: Int = 32
const val INVITE_SNAME_MAX: Int = 64
const val INVITE_PROTO_UDP: Int = 0x01
const val INVITE_PROTO_TCP: Int = 0x02
const val INVITE_PROTO_BOTH: Int = 0x03
const val INVITE_PROTO_REALITY: Int = 0x04 // флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06)
data class InviteReality(
val has: Boolean,
val shortId: ByteArray,
val serverPubkey: ByteArray,
val version: ByteArray,
val serverName: String
)
data class InviteAddr(
val family: Int, // 4 or 6
val address: ByteArray,
val port: Int,
val socketId: Int,
val proto: Int // INVITE_PROTO_UDP/TCP/BOTH
val proto: Int // INVITE_PROTO_*
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -32,13 +41,26 @@ data class InviteData(
val joinKey: Long,
val pubkey: ByteArray,
val nodeId: Long,
val addrs: List<InviteAddr>
val addrs: List<InviteAddr>,
val password: String = "",
val reality: InviteReality? = null
) {
/** Serialize addresses for C bridge: family(1) + socketId(1) + proto(1) + address(4|16) + port(2 BE) */
/** Serialize addresses for C bridge: reality-префикс + family(1)+socketId(1)+proto(1)+address(4|16)+port(2 BE) */
fun serializeAddrs(): ByteArray {
val size = addrs.fold(0) { acc, a -> acc + if (a.family == 4) 9 else 21 }
var size = 1
if (reality != null && reality.has) size += 8 + 32 + 3 + 1 + reality.serverName.toByteArray(Charsets.UTF_8).let { minOf(it.size, INVITE_SNAME_MAX - 1) }
size = addrs.fold(size) { acc, a -> acc + if (a.family == 4) 9 else 21 }
val buf = ByteArray(size)
var pos = 0
buf[pos] = (if (reality != null && reality.has) 1 else 0).toByte(); pos++
if (reality != null && reality.has) {
System.arraycopy(reality.shortId, 0, buf, pos, 8); pos += 8
System.arraycopy(reality.serverPubkey, 0, buf, pos, 32); pos += 32
System.arraycopy(reality.version, 0, buf, pos, 3); pos += 3
val sn = reality.serverName.toByteArray(Charsets.UTF_8).let { if (it.size > INVITE_SNAME_MAX - 1) it.copyOf(INVITE_SNAME_MAX - 1) else it }
buf[pos] = sn.size.toByte(); pos++
System.arraycopy(sn, 0, buf, pos, sn.size); pos += sn.size
}
for (a in addrs) {
buf[pos] = a.family.toByte(); pos++
buf[pos] = a.socketId.toByte(); pos++
@ -85,13 +107,25 @@ object InviteLink {
}
var off = 0
if ((raw[off].toInt() and 0xFF) != INVITE_LINK_VERSION) {
val ver = raw[off].toInt() and 0xFF
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
LogManager.addLog("ERROR", "INVITE", "decode: wrong version 0x%02x expected 0x%02x".format(
raw[off].toInt() and 0xFF, INVITE_LINK_VERSION))
return null
}
off++
var password = ""
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val plen = raw[off].toInt() and 0xFF; off++
if (plen > 0) {
if (off + plen > raw.size) return null
password = String(raw.copyOfRange(off, off + plen), Charsets.UTF_8)
off += plen
}
}
var chId = 0L
for (i in 0 until 8) {
if (off + i >= raw.size) return null
@ -106,6 +140,24 @@ object InviteLink {
}
off += 8
var reality: InviteReality? = null
if (ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val has = (raw[off].toInt() and 0xFF) != 0; off++
if (has) {
if (off + 8 + 32 + 3 + 1 > raw.size) return null
val shortId = raw.copyOfRange(off, off + 8); off += 8
val serverPubkey = raw.copyOfRange(off, off + 32); off += 32
val version = raw.copyOfRange(off, off + 3); off += 3
var snlen = raw[off].toInt() and 0xFF; off++
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1
if (off + snlen > raw.size) return null
val serverName = String(raw.copyOfRange(off, off + snlen), Charsets.UTF_8)
off += snlen
reality = InviteReality(has = true, shortId = shortId, serverPubkey = serverPubkey, version = version, serverName = serverName)
}
}
LogManager.addLog("DEBUG", "INVITE", "decode: chId=0x%016x joinKey=0x%016x".format(chId, joinKey))
var pubkey: ByteArray? = null
@ -143,14 +195,15 @@ object InviteLink {
val pkHex = pubkey.take(8).joinToString("") { "%02x".format(it) }
LogManager.addLog("INFO", "INVITE", "decode: ch=0x%016x node=0x%016x pk=%s... addrs=%d".format(
chId, nodeId, pkHex, addrs.size))
return InviteData(channelId = chId, joinKey = joinKey, pubkey = pubkey, nodeId = nodeId, addrs = addrs)
return InviteData(channelId = chId, joinKey = joinKey, pubkey = pubkey, nodeId = nodeId,
addrs = addrs, password = password, reality = reality)
}
fun addrToString(addr: InviteAddr): String {
val protoStr = when (addr.proto) {
INVITE_PROTO_UDP -> "UDP"
INVITE_PROTO_TCP -> "TCP"
INVITE_PROTO_BOTH -> "UDP+TCP"
val protoStr = when {
(addr.proto and INVITE_PROTO_REALITY) != 0 -> "TCP+REALITY"
addr.proto == INVITE_PROTO_UDP -> "UDP"
addr.proto == INVITE_PROTO_TCP -> "TCP"
else -> "?${addr.proto}"
}
val ip = if (addr.family == 4 && addr.address.size == 4) {

61
tools/chatgui-android/libutun_lite/invite_link_c.c

@ -72,14 +72,15 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
int off = 0;
uint8_t ver = raw[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02) {
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "unsupported version 0x%02x", ver);
return -1;
}
out->password[0] = '\0'; out->password_len = 0;
out->reality.has = 0;
/* v2: password field before addresses */
if (ver == 0x02) {
/* v2/v3: password field before addresses */
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pass_len");
return -1;
@ -114,6 +115,22 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
jk = (jk << 8) | raw[off++];
out->join_key = jk;
/* v3: reality-блок */
if (ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality"); return -1; }
if (raw[off++]) {
if (off + 8 + 32 + 3 + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality params"); return -1; }
memcpy(out->reality.short_id, raw + off, 8); off += 8;
memcpy(out->reality.server_pubkey, raw + off, 32); off += 32;
memcpy(out->reality.version, raw + off, 3); off += 3;
uint8_t snlen = raw[off++];
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
if (off + snlen > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality server_name"); return -1; }
memcpy(out->reality.server_name, raw + off, snlen); out->reality.server_name[snlen] = '\0'; off += snlen;
out->reality.has = 1;
}
}
memset(out->pubkey, 0, INVITE_PUBKEY_SIZE);
out->addrCount = 0;
@ -158,11 +175,26 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
int invite_serialize_addrs(const struct InviteDataC* data, uint8_t* buf, size_t buf_size) {
if (!data || !buf) return -1;
size_t needed = 0;
size_t needed = 1;
if (data->reality.has) {
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
needed += 8 + 32 + 3 + 1 + snlen;
}
for (int i = 0; i < (int)data->addrCount; i++)
needed += (data->addrs[i].family == 6) ? 21U : 9U;
if (needed > buf_size) return -1;
size_t pos = 0;
buf[pos++] = data->reality.has ? 1 : 0;
if (data->reality.has) {
memcpy(buf + pos, data->reality.short_id, 8); pos += 8;
memcpy(buf + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(buf + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
buf[pos++] = (uint8_t)snlen;
memcpy(buf + pos, data->reality.server_name, snlen); pos += snlen;
}
for (int i = 0; i < (int)data->addrCount; i++) {
const struct InviteAddrC* a = &data->addrs[i];
int ip_len = (a->family == 6) ? 16 : 4;
@ -180,18 +212,29 @@ int invite_link_encode(const struct InviteDataC* data, const char* password, cha
if (!data || !out || data->pubkey[0] == 0) return -1;
if (data->addrCount == 0) return -1;
int has_pass = (password && password[0]) ? 1 : 0;
uint8_t raw[4096];
size_t pos = 0;
raw[pos++] = has_pass ? 0x02 : INVITE_LINK_VERSION;
if (has_pass) {
size_t plen = strlen(password);
raw[pos++] = INVITE_LINK_VERSION;
{
size_t plen = (password && password[0]) ? strlen(password) : 0;
if (plen > INVITE_PASS_MAX - 1) return -1;
raw[pos++] = (uint8_t)plen;
memcpy(raw + pos, password, plen); pos += plen;
if (plen) { memcpy(raw + pos, password, plen); pos += plen; }
}
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->channelId >> (i * 8)) & 0xFF);
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->join_key >> (i * 8)) & 0xFF);
raw[pos++] = data->reality.has ? 1 : 0;
if (data->reality.has) {
memcpy(raw + pos, data->reality.short_id, 8); pos += 8;
memcpy(raw + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(raw + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
raw[pos++] = (uint8_t)snlen;
memcpy(raw + pos, data->reality.server_name, snlen); pos += snlen;
}
uint8_t pk[32]; memcpy(pk, data->pubkey, 32);
int ia = 0;

15
tools/chatgui-android/libutun_lite/invite_link_c.h

@ -4,14 +4,15 @@
#include <stdint.h>
#include <stddef.h>
#define INVITE_LINK_VERSION 0x01
#define INVITE_LINK_VERSION 0x03
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
#define INVITE_PASS_MAX 128
#define INVITE_SNAME_MAX 64
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_BOTH 0x03
#define INVITE_PROTO_REALITY 0x04 /* флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06) */
#ifdef __cplusplus
extern "C" {
@ -25,6 +26,15 @@ struct InviteAddrC {
uint16_t port;
};
/* Reality-параметры узла (node-global: одинаковы для всех reality-сокетов). */
struct InviteRealityC {
uint8_t has;
uint8_t short_id[8];
uint8_t server_pubkey[32];
uint8_t version[3];
char server_name[INVITE_SNAME_MAX];
};
struct InviteDataC {
uint64_t channelId;
uint64_t join_key; /* 64-битный ключ входа (генерируется инвайтером) */
@ -34,6 +44,7 @@ struct InviteDataC {
struct InviteAddrC addrs[INVITE_ADDR_MAX];
char password[INVITE_PASS_MAX]; /* empty = no password (v1 compat) */
uint8_t password_len;
struct InviteRealityC reality;
};
/* decode utun://base64 link string, returns 0 on success, -1 on error.

2
tools/chatgui/db/db_manager.h

@ -52,7 +52,7 @@ struct AccountRow {
struct NodeAddr {
quint64 nodeId = 0;
int family = 0;
int protocol = 0; // битмаска: 1=UDP, 2=TCP, 3=TCP+UDP
int protocol = 0; // битмаска: 1=UDP, 2=TCP, 4=REALITY (6=TCP+REALITY)
QByteArray address;
quint16 port = 0;
int rtt = 0;

14
tools/chatgui/src/accountlist.cpp

@ -312,6 +312,7 @@ void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int l
lk.addr = QByteArray((const char*)p + 25, 16);
memcpy(&lk.port, p + 41, 2);
memcpy(lk.sockName, p + 43, 20); lk.sockName[19] = '\0';
lk.isReality = p[63] != 0;
p += MEMBER_DETAIL_LINK_SIZE;
m_snapLinks.append(lk);
}
@ -367,14 +368,17 @@ void AccountList::updateDetailDisplay() {
/* ── Body ── */
QStringList lines;
auto protoName = [](int protocol) -> QString {
if (protocol & 0x04) return (protocol & 0x02) ? "TCP+REALITY" : "REALITY";
switch (protocol) { case 1: return "UDP"; case 2: return "TCP"; default: return "?"; }
};
if (!m_detailAddrs.isEmpty()) {
lines.append("── Addresses ──");
for (const auto& a : m_detailAddrs) {
QString proto;
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "?"; break; }
lines.append(QString(" [sock%1] %2 %3 %4:%5 %6")
.arg(a.socket_id)
.arg(proto)
.arg(protoName(a.protocol))
.arg(a.ifname)
.arg(addrStr(a.addr.constData(), a.family))
.arg(a.port)
@ -389,7 +393,7 @@ void AccountList::updateDetailDisplay() {
for (const auto& da : m_detailAddrs) {
if (da.socket_id == s.sockId) { a = &da; break; }
}
QString proto = a ? (a->protocol == 2 ? "TCP" : "UDP") : "?";
QString proto = a ? protoName(a->protocol) : "?";
QString ip = a ? addrStr(a->addr.constData(), a->family) : "?";
lines.append(QString(" [%1/%2] %3:%4 %5")
.arg(s.linksUp).arg(s.linksDown)
@ -404,7 +408,7 @@ void AccountList::updateDetailDisplay() {
QString c = up ? "#4CAF50" : "#E53935";
lines.append(QString("<span style='color:%1;'>●</span> %2 %3:%4 %5 %6:%7")
.arg(c)
.arg(lk.isTcp ? "TCP" : "UDP")
.arg(lk.isReality ? "TCP+REALITY" : (lk.isTcp ? "TCP" : "UDP"))
.arg(addrStr(lk.localAddr.constData(), lk.localFamily)).arg(lk.localPort)
.arg(lk.isServer ? "\u25c0" : "\u25b6")
.arg(addrStr(lk.addr.constData(), lk.family)).arg(lk.port));

1
tools/chatgui/src/accountlist.h

@ -36,6 +36,7 @@ struct MemberLinkSnap {
QByteArray addr;
uint16_t port;
char sockName[20];
bool isReality;
};
struct MemberSockSnap {

45
tools/chatgui/src/invite_link.cpp

@ -82,14 +82,14 @@ InviteData decodeInviteLink(const QString& link) {
int off = 0;
uint8_t ver = data[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02) {
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
d.error = QStringLiteral("unsupported version 0x%1").arg(ver, 2, 16, QLatin1Char('0'));
GUI_ERROR("decodeInviteLink: unsupported version %d", ver);
return d;
}
/* v2: password field before channel_id */
if (ver == 0x02) {
/* v2/v3: password field before channel_id */
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off >= len) {
d.error = QStringLiteral("truncated at pass_len");
return d;
@ -127,6 +127,23 @@ InviteData decodeInviteLink(const QString& link) {
jk = (jk << 8) | data[off++];
d.joinKey = jk;
/* v3: reality-блок (node-global параметры) */
if (ver == INVITE_LINK_VERSION) {
if (off >= len) { d.error = QStringLiteral("truncated at reality"); return d; }
if (data[off++] != 0) {
if (off + 8 + 32 + 3 + 1 > len) { d.error = QStringLiteral("truncated at reality params"); return d; }
d.reality.shortId = QByteArray(reinterpret_cast<const char*>(data + off), 8); off += 8;
d.reality.serverPubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32); off += 32;
d.reality.version = QByteArray(reinterpret_cast<const char*>(data + off), 3); off += 3;
uint8_t snlen = data[off++];
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
if (off + snlen > len) { d.error = QStringLiteral("truncated at reality server_name"); return d; }
d.reality.serverName = QString::fromUtf8(reinterpret_cast<const char*>(data + off), snlen);
off += snlen;
d.reality.has = true;
}
}
bool firstBlock = true;
while (off + 1 <= len) {
uint8_t header = data[off++];
@ -169,3 +186,25 @@ InviteData decodeInviteLink(const QString& link) {
return d;
}
QByteArray serializeInviteAddrs(const InviteData& d) {
QByteArray buf;
buf.append(static_cast<char>(d.reality.has ? 1 : 0));
if (d.reality.has) {
buf.append(d.reality.shortId.leftJustified(8, '\0', true));
buf.append(d.reality.serverPubkey.leftJustified(32, '\0', true));
buf.append(d.reality.version.leftJustified(3, '\0', true));
QByteArray sn = d.reality.serverName.toUtf8().left(INVITE_SNAME_MAX - 1);
buf.append(static_cast<char>(sn.size()));
buf.append(sn);
}
for (const auto& a : d.addrs) {
buf.append(static_cast<char>(a.family));
buf.append(static_cast<char>(a.socketId));
buf.append(static_cast<char>(a.proto));
buf.append(a.address);
buf.append(static_cast<char>((a.port >> 8) & 0xFF));
buf.append(static_cast<char>(a.port & 0xFF));
}
return buf;
}

24
tools/chatgui/src/invite_link.h

@ -6,17 +6,29 @@
#include <cstdint>
#include "../db/db_manager.h"
#define INVITE_LINK_VERSION 0x01
#define INVITE_LINK_VERSION 0x03
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_REALITY 0x04 // флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06)
#define INVITE_SNAME_MAX 64
struct InviteAddr {
int family; // 4=IPv4, 6=IPv6
QByteArray address;
quint16 port;
uint8_t socketId; // socket_id из ETCP
uint8_t proto; // INVITE_PROTO_UDP/TCP/BOTH
uint8_t proto; // INVITE_PROTO_*
};
/* Reality-параметры узла (node-global: одинаковы для всех reality-сокетов). */
struct InviteReality {
bool has = false;
QByteArray shortId; // 8 байт
QByteArray serverPubkey; // 32 байта
QByteArray version; // 3 байта
QString serverName;
};
struct InviteData {
@ -26,6 +38,7 @@ struct InviteData {
QList<InviteAddr> addrs;
QString password;
QString error;
InviteReality reality;
};
QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
@ -33,3 +46,6 @@ QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
const QString& password = QString());
InviteData decodeInviteLink(const QString& link);
/* reality-префикс + адреса: has(1) [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)] addrs*(family sockId proto addr port) */
QByteArray serializeInviteAddrs(const InviteData& d);

10
tools/chatgui/src/inviteby.cpp

@ -179,15 +179,7 @@ void InviteByLinkDialog::onSendInvite() {
m_channelId.toUtf8().constData(), targetNodeId, d.addrs.size());
/* Save addresses to DB */
QByteArray addrsBuf;
for (const auto& a : d.addrs) {
addrsBuf.append(static_cast<char>(a.family));
addrsBuf.append(static_cast<char>(a.socketId));
addrsBuf.append(static_cast<char>(a.proto));
addrsBuf.append(a.address);
addrsBuf.append(static_cast<char>((a.port >> 8) & 0xFF));
addrsBuf.append(static_cast<char>(a.port & 0xFF));
}
QByteArray addrsBuf = serializeInviteAddrs(d);
chat_sync_invite_to_channel_with_addrs(
chat_core_get_inst(), m_channelId.toUtf8().constData(), targetNodeId,

10
tools/chatgui/src/joindialog.cpp

@ -188,15 +188,7 @@ void JoinDialog::onConnectClicked() {
setStatus("Connecting to peer...");
QByteArray addrsBuf;
for (const auto& a : d.addrs) {
addrsBuf.append(static_cast<char>(a.family));
addrsBuf.append(static_cast<char>(a.socketId));
addrsBuf.append(static_cast<char>(a.proto));
addrsBuf.append(a.address);
addrsBuf.append(static_cast<char>((a.port >> 8) & 0xFF));
addrsBuf.append(static_cast<char>(a.port & 0xFF));
}
QByteArray addrsBuf = serializeInviteAddrs(d);
int addrCount = d.addrs.size();
const char* pass = d.password.isEmpty() ? NULL : d.password.toUtf8().constData();

1
tools/chatgui/src/nodespage.cpp

@ -38,6 +38,7 @@ QString NodesPage::addrStr(const void* addr, int family) {
}
QString NodesPage::protoStr(int protocol) {
if (protocol & 0x04) return (protocol & 0x02) ? "TCP+REALITY" : "REALITY";
switch (protocol) {
case 1: return "UDP";
case 2: return "TCP";

2
tools/chatgui/transport/gui_bridge.h

@ -40,7 +40,7 @@ struct TOPO_GROUP_NODE;
#define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
/* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */
#define MEMBER_DETAIL_LINK_SIZE 63
#define MEMBER_DETAIL_LINK_SIZE 64
#define MEMBER_DETAIL_MAX_LINKS 16
#define MEMBER_DETAIL_MAX_SOCKS 16
#define MEMBER_DETAIL_SOCK_SIZE 48

Loading…
Cancel
Save