chat: invite-ссылки с выбором достижимого узла + tcp_io фикс half-close FIN
- Общий модуль invite_build: сборка utun://-ссылок, авто-выбор лучшего достижимого узла, список кандидатов (вынесено из android jni_bridge) - chatgui: диалог invite с выбором узла (комбобокс), события INVITE_CANDIDATES / INVITE_LINK_READY через gui_bridge - headless/chatcli: invite <ch_id> [node_id], команда invite_nodes - tcp_io: error_cb отличает грациозный EPOLLHUP (SO_ERROR==0) от реальной ошибки — при приостановленном чтении FIN не роняет недослейные данные (фикс flaky half-close) - run_test.sh: half-close echo на выделенном порту 19190 + отдельный DNAT (был недостижим)
This commit is contained in:
1 parent
a17a85a383
commit
1345f44e10
23 files changed
+872
-539
No files matched your search
+12
-4
@@ -14,7 +14,8 @@
|
||||
# members <ch_id> list members with full state
|
||||
# messages <ch_id> [count] read last messages
|
||||
# send <ch_id> <text> send text message
|
||||
# invite <ch_id> create invite link
|
||||
# invite <ch_id> [node_id] create invite link (auto best node by default)
|
||||
# invite_nodes <ch_id> list reachable nodes for invite
|
||||
# connect <utun://...> join channel via invite link
|
||||
# create <name> create new channel
|
||||
# invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]
|
||||
@@ -97,8 +98,14 @@ def cmd_messages(ch_id, count=None):
|
||||
def cmd_send(ch_id, text):
|
||||
_req("send", ch=ch_id, content_type="text", data=text)
|
||||
|
||||
def cmd_invite(ch_id):
|
||||
_req("invite", ch=ch_id)
|
||||
def cmd_invite(ch_id, node_id=None):
|
||||
params = {"ch": ch_id}
|
||||
if node_id:
|
||||
params["node_id"] = node_id
|
||||
_req("invite", **params)
|
||||
|
||||
def cmd_invite_nodes(ch_id):
|
||||
_req("invite_nodes", ch=ch_id)
|
||||
|
||||
def cmd_connect(link):
|
||||
_req("connect", link=link)
|
||||
@@ -166,7 +173,8 @@ def main():
|
||||
elif cmd == "members": cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members <ch_id>"),))
|
||||
elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages <ch_id> [count]"),))
|
||||
elif cmd == "send": cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send <ch_id> <text>"),))
|
||||
elif cmd == "invite": cmd_invite(*args[1:2] if len(args) > 1 else (_die("usage: invite <ch_id>"),))
|
||||
elif cmd == "invite": cmd_invite(*args[1:3] if len(args) > 1 else (_die("usage: invite <ch_id> [node_id]"),))
|
||||
elif cmd == "invite_nodes": cmd_invite_nodes(*args[1:2] if len(args) > 1 else (_die("usage: invite_nodes <ch_id>"),))
|
||||
elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
|
||||
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
|
||||
elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]"),))
|
||||
|
||||
@@ -49,10 +49,19 @@ send <ch_id> <text>
|
||||
Отправить текстовое сообщение в канал.
|
||||
Пример: chatcli send 12345 "hello world"
|
||||
|
||||
invite <ch_id>
|
||||
invite <ch_id> [node_id]
|
||||
Создать invite-ссылку для канала (формат utun://...).
|
||||
Ссылка включает pubkey, адреса сервера и ID канала.
|
||||
Без node_id — автоматический выбор лучшего достижимого узла.
|
||||
С node_id — ссылка ведёт на указанный узел (hex, например 0x1234).
|
||||
Ссылка включает pubkey, адреса выбранного узла и ID канала.
|
||||
Пример: chatcli invite 12345
|
||||
chatcli invite 12345 0x1a2b3c
|
||||
|
||||
invite_nodes <ch_id>
|
||||
Список достижимых узлов канала, пригодных для invite (включая свой узел).
|
||||
Ответ содержит auto_node_id — узел, выбранный авто-логикой,
|
||||
и список nodes (node_id, имя).
|
||||
Пример: chatcli invite_nodes 12345
|
||||
|
||||
connect <utun://...>
|
||||
Подключиться к каналу по invite-ссылке.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "../../../lib/mem.h"
|
||||
#include "../../../lib/sqlite3.h"
|
||||
#include "../../../src/chat/chat_core.h"
|
||||
#include "../../../src/chat/invite_build.h"
|
||||
#include "../../../src/chat/chat_setting.h"
|
||||
#include "../../../src/utun_instance.h"
|
||||
#include "../../../src/transport_layer/etcp.h"
|
||||
@@ -229,202 +230,11 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
|
||||
chat_sync_connect_from_invite(channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL);
|
||||
}
|
||||
|
||||
/* Collect listening addresses from local sockets (for self-node fallback) */
|
||||
static int bridge_collect_invite_addrs(struct InviteAddrC* out, int max_cnt) {
|
||||
int cnt = 0;
|
||||
struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets;
|
||||
while (s) {
|
||||
if (s->is_tcp) { s = s->next; continue; }
|
||||
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
|
||||
if (sa && sa->ss_family == AF_INET && cnt < max_cnt) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP;
|
||||
memcpy(out[cnt].address, &sin->sin_addr, 4);
|
||||
out[cnt].port = ntohs(sin->sin_port); cnt++;
|
||||
} else if (sa && sa->ss_family == AF_INET6 && cnt < max_cnt) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP;
|
||||
memcpy(out[cnt].address, &sin6->sin6_addr, 16);
|
||||
out[cnt].port = ntohs(sin6->sin6_port); cnt++;
|
||||
}
|
||||
s = s->next;
|
||||
}
|
||||
{ struct ETCP_SOCKET* s2 = g_cc.inst->etcp_sockets;
|
||||
while (s2) {
|
||||
if (!s2->is_tcp) { s2 = s2->next; continue; }
|
||||
struct sockaddr_storage* sa = s2->interface_addr.ss_family ? &s2->interface_addr : &s2->local_addr;
|
||||
if (sa && sa->ss_family == AF_INET && cnt < max_cnt) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
out[cnt].family = 4; out[cnt].socketId = s2->sock_id; out[cnt].proto = INVITE_PROTO_TCP;
|
||||
memcpy(out[cnt].address, &sin->sin_addr, 4);
|
||||
out[cnt].port = ntohs(sin->sin_port); cnt++;
|
||||
} else if (sa && sa->ss_family == AF_INET6 && cnt < max_cnt) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
out[cnt].family = 6; out[cnt].socketId = s2->sock_id; out[cnt].proto = INVITE_PROTO_TCP;
|
||||
memcpy(out[cnt].address, &sin6->sin6_addr, 16);
|
||||
out[cnt].port = ntohs(sin6->sin6_port); cnt++;
|
||||
}
|
||||
s2 = s2->next; }
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* Collect addresses from node_addresses table for a remote node.
|
||||
If dir_only=1 → only ADDR_TYPE_DIRECT, otherwise all addr_types */
|
||||
static int bridge_collect_invite_addrs_from_db(sqlite3* db, uint64_t node_id,
|
||||
struct InviteAddrC* out, int max_cnt, int dir_only) {
|
||||
int cnt = 0;
|
||||
char sql[256];
|
||||
if (dir_only)
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT family,socket_id,protocol,address,port 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=?");
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
while (sqlite3_step(st) == SQLITE_ROW && cnt < max_cnt) {
|
||||
int family = sqlite3_column_int(st, 0);
|
||||
out[cnt].family = family;
|
||||
out[cnt].socketId = (uint8_t)sqlite3_column_int(st, 1);
|
||||
out[cnt].proto = (uint8_t)sqlite3_column_int(st, 2);
|
||||
const uint8_t* addr = sqlite3_column_blob(st, 3);
|
||||
int addr_len = sqlite3_column_bytes(st, 3);
|
||||
int ip_len = (family == 6) ? 16 : 4;
|
||||
if (addr && addr_len >= ip_len) memcpy(out[cnt].address, addr, (size_t)ip_len);
|
||||
else { memset(out[cnt].address, 0, sizeof(out[cnt].address)); }
|
||||
out[cnt].port = (uint16_t)sqlite3_column_int(st, 4);
|
||||
cnt++;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return cnt;
|
||||
}
|
||||
|
||||
char* utun_bridge_encode_invite_link(uint64_t channel_id, const char* password) {
|
||||
if (!chat_core_is_initialized() || !g_cc.inst || !g_cc.db) {
|
||||
bridge_log(BLEV_ERROR, "encode_invite_link: not initialized inst=%p db=%p",
|
||||
(void*)g_cc.inst, g_cc.inst ? (void*)g_cc.db : NULL);
|
||||
return NULL;
|
||||
}
|
||||
uint64_t my_nid = g_cc.my_node_id;
|
||||
sqlite3* db = g_cc.db;
|
||||
struct TOPO_GROUPS* groups = g_cc.inst->topo_groups;
|
||||
|
||||
/* ── Фаза 1: поиск лучшего узла в канале ── */
|
||||
char ch_str[32]; snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)channel_id);
|
||||
char peers_tbl[80]; peers_table_name(ch_str, peers_tbl, sizeof(peers_tbl));
|
||||
|
||||
uint64_t best_nid = 0;
|
||||
int best_priority = 4; /* 1=regular, 2=self, 3=super, 4=invalid */
|
||||
int best_has_both_public = 0;
|
||||
|
||||
sqlite3_stmt* pst = NULL;
|
||||
char sql[512];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT node_id, node_type FROM \"%s\"", peers_tbl);
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &pst, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(pst) == SQLITE_ROW) {
|
||||
uint64_t nid = (uint64_t)sqlite3_column_int64(pst, 0);
|
||||
int ntype = sqlite3_column_int(pst, 1);
|
||||
|
||||
/* alive = BGP visible */
|
||||
struct TOPO_GROUP* grp = groups ? topo_groups_find(groups, channel_id) : NULL;
|
||||
if (!grp) continue;
|
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, nid);
|
||||
if (!nq || !(nq->conn_presence & (NCONN_DIRECT | NCONN_INDIRECT | NCONN_BGP)))
|
||||
continue;
|
||||
|
||||
/* public v4+v6? */
|
||||
int has_v4 = 0, has_v6 = 0;
|
||||
sqlite3_stmt* ast = NULL;
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d",
|
||||
ADDR_TYPE_DIRECT);
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &ast, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(ast, 1, (sqlite3_int64)nid);
|
||||
while (sqlite3_step(ast) == SQLITE_ROW) {
|
||||
if (sqlite3_column_int(ast, 0) == 4) has_v4 = 1;
|
||||
if (sqlite3_column_int(ast, 0) == 6) has_v6 = 1;
|
||||
}
|
||||
sqlite3_finalize(ast);
|
||||
}
|
||||
int has_both = (has_v4 && has_v6) ? 1 : 0;
|
||||
|
||||
/* priority */
|
||||
int priority;
|
||||
if (nid == my_nid) priority = 2; /* self */
|
||||
else if (ntype == 4) priority = 3; /* supernode */
|
||||
else priority = 1; /* regular */
|
||||
|
||||
/* pick best (lower priority, or same → later) */
|
||||
if (priority <= best_priority) {
|
||||
best_nid = nid; best_priority = priority;
|
||||
best_has_both_public = has_both;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(pst);
|
||||
}
|
||||
|
||||
/* ── Фаза 2: fallback ── */
|
||||
uint64_t target_nid = best_nid ? best_nid : my_nid;
|
||||
int use_self = (target_nid == my_nid) ? 1 : 0;
|
||||
|
||||
bridge_log(BLEV_INFO, "encode_invite_link: ch=%llu best=0x%016llx prio=%d self=%d both=%d tot_peer_rows=%d",
|
||||
(unsigned long long)channel_id, (unsigned long long)target_nid,
|
||||
best_priority, use_self, best_has_both_public,
|
||||
best_nid ? 1 : 0);
|
||||
|
||||
/* ── Фаза 3: pubkey ── */
|
||||
struct InviteDataC data; memset(&data, 0, sizeof(data));
|
||||
data.channelId = channel_id; data.nodeId = target_nid;
|
||||
|
||||
if (use_self) {
|
||||
memcpy(data.pubkey, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
|
||||
} else {
|
||||
sqlite3_stmt* pkst = NULL;
|
||||
snprintf(sql, sizeof(sql), "SELECT x25519_pubkey FROM nodes WHERE node_id=?");
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &pkst, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(pkst, 1, (sqlite3_int64)target_nid);
|
||||
if (sqlite3_step(pkst) == SQLITE_ROW) {
|
||||
const void* pk = sqlite3_column_blob(pkst, 0);
|
||||
int pklen = sqlite3_column_bytes(pkst, 0);
|
||||
if (pk && pklen >= 32) memcpy(data.pubkey, pk, INVITE_PUBKEY_SIZE);
|
||||
}
|
||||
sqlite3_finalize(pkst);
|
||||
}
|
||||
if (data.pubkey[0] == 0) {
|
||||
bridge_log(BLEV_ERROR, "encode_invite_link: no pubkey for selected node=0x%016llx",
|
||||
(unsigned long long)target_nid);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Фаза 4: адреса ── */
|
||||
if (use_self) {
|
||||
data.addrCount = (uint8_t)bridge_collect_invite_addrs(data.addrs, INVITE_ADDR_MAX);
|
||||
} else {
|
||||
int dir_only = best_has_both_public ? 1 : 0;
|
||||
data.addrCount = (uint8_t)bridge_collect_invite_addrs_from_db(
|
||||
db, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only);
|
||||
}
|
||||
if (data.addrCount == 0) {
|
||||
bridge_log(BLEV_ERROR, "encode_invite_link: no addrs for node=0x%016llx",
|
||||
(unsigned long long)target_nid);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bridge_log(BLEV_INFO, "encode_invite_link: node=0x%016llx addrs=%d dir_only=%d",
|
||||
(unsigned long long)target_nid, data.addrCount,
|
||||
(!use_self && best_has_both_public) ? 1 : 0);
|
||||
|
||||
/* ── Фаза 5: encode ── */
|
||||
char out[512];
|
||||
int r = invite_link_encode(&data, password, out, sizeof(out));
|
||||
if (r < 0) {
|
||||
bridge_log(BLEV_ERROR, "encode_invite_link: encode failed r=%d", r);
|
||||
char out[1024];
|
||||
if (chat_invite_build_link(channel_id, 0, password, out, sizeof(out)) < 0) {
|
||||
bridge_log(BLEV_ERROR, "encode_invite_link: chat_invite_build_link failed ch=%llu",
|
||||
(unsigned long long)channel_id);
|
||||
return NULL;
|
||||
}
|
||||
return u_strdup(out);
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
#include "invitedialog.h"
|
||||
#include "qrcode_utils.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "debug_ui.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QSignalBlocker>
|
||||
#include <cstring>
|
||||
|
||||
ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent)
|
||||
: QDialog(parent), m_link(link)
|
||||
extern "C" {
|
||||
#include "../../../src/chat/invite_build.h"
|
||||
#include "../../../lib/mem.h"
|
||||
}
|
||||
|
||||
ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_channelId(channelId)
|
||||
, m_db(db)
|
||||
{
|
||||
setWindowTitle(QString::fromUtf8("Пригласительная ссылка"));
|
||||
setMinimumWidth(420);
|
||||
@@ -18,33 +32,152 @@ ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent)
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(12);
|
||||
|
||||
auto *linkEdit = new QLineEdit(m_link, this);
|
||||
linkEdit->setReadOnly(true);
|
||||
linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
|
||||
layout->addWidget(linkEdit);
|
||||
auto *nodeLabel = new QLabel(QString::fromUtf8("Узел входа:"), this);
|
||||
nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);");
|
||||
layout->addWidget(nodeLabel);
|
||||
|
||||
m_nodeCombo = new QComboBox(this);
|
||||
m_nodeCombo->setStyleSheet(
|
||||
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
|
||||
" padding: 6px 10px; font-size: 13px; background: palette(base); }");
|
||||
layout->addWidget(m_nodeCombo);
|
||||
|
||||
m_linkEdit = new QLineEdit(this);
|
||||
m_linkEdit->setReadOnly(true);
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки..."));
|
||||
m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
|
||||
layout->addWidget(m_linkEdit);
|
||||
|
||||
auto *btnLayout = new QHBoxLayout();
|
||||
auto *copyBtn = new QPushButton(QString::fromUtf8("Копировать"), this);
|
||||
connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() {
|
||||
connect(copyBtn, &QPushButton::clicked, this, [this]() {
|
||||
QClipboard* cb = QApplication::clipboard();
|
||||
cb->setText(m_link, QClipboard::Clipboard);
|
||||
cb->setText(m_linkEdit->text(), QClipboard::Clipboard);
|
||||
if (cb->supportsSelection())
|
||||
cb->setText(m_link, QClipboard::Selection);
|
||||
linkEdit->setFocus();
|
||||
cb->setText(m_linkEdit->text(), QClipboard::Selection);
|
||||
m_linkEdit->setFocus();
|
||||
});
|
||||
btnLayout->addWidget(copyBtn);
|
||||
btnLayout->addStretch();
|
||||
layout->addLayout(btnLayout);
|
||||
|
||||
auto *qrLabel = new QLabel(this);
|
||||
QImage qrImage = generateQRCode(m_link);
|
||||
if (!qrImage.isNull())
|
||||
qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
|
||||
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
qrLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(qrLabel, 0, Qt::AlignCenter);
|
||||
m_qrLabel = new QLabel(this);
|
||||
m_qrLabel->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(m_qrLabel, 0, Qt::AlignCenter);
|
||||
|
||||
auto *closeBtn = new QPushButton("OK", this);
|
||||
connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept);
|
||||
layout->addWidget(closeBtn, 0, Qt::AlignRight);
|
||||
|
||||
connect(m_nodeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ShareInviteDialog::onNodeSelected);
|
||||
|
||||
/* колбэки результатов (по образцу JoinDialog) */
|
||||
gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, const char* link) {
|
||||
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
|
||||
if (dlg) dlg->onLinkReady(chId, chIdLen, link);
|
||||
});
|
||||
gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) {
|
||||
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
|
||||
if (dlg) dlg->onCandidates(chId, chIdLen, data, len);
|
||||
});
|
||||
|
||||
/* запросить кандидатов; линк строится после их прихода */
|
||||
QByteArray chBytes = m_channelId.toUtf8();
|
||||
gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, u_strdup(chBytes.constData()));
|
||||
}
|
||||
|
||||
ShareInviteDialog::~ShareInviteDialog() {
|
||||
gui_bridge_set_invite_link_ready_cb(nullptr);
|
||||
gui_bridge_set_invite_candidates_cb(nullptr);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::onNodeSelected(int index) {
|
||||
if (index < 0) return;
|
||||
qulonglong nodeId = m_nodeCombo->itemData(index).toULongLong();
|
||||
requestLink((uint64_t)nodeId);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
|
||||
struct chat_invite_build_req* req = (struct chat_invite_build_req*)u_malloc(sizeof(*req));
|
||||
if (!req) {
|
||||
GUI_ERROR("invite: requestLink — alloc failed");
|
||||
return;
|
||||
}
|
||||
memset(req, 0, sizeof(*req));
|
||||
QByteArray chBytes = m_channelId.toUtf8();
|
||||
strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1);
|
||||
req->target_node_id = targetNodeId;
|
||||
m_linkEdit->setText(QString());
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки..."));
|
||||
m_qrLabel->clear();
|
||||
gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::setLink(const QString& link) {
|
||||
m_linkEdit->setText(link);
|
||||
QImage qrImage = generateQRCode(link);
|
||||
if (!qrImage.isNull())
|
||||
m_qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
|
||||
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len) {
|
||||
QString cid = QString::fromUtf8(chId, chIdLen);
|
||||
if (cid != m_channelId) return;
|
||||
if (!data || len < 10) return;
|
||||
|
||||
uint64_t autoId = 0;
|
||||
memcpy(&autoId, data, 8);
|
||||
uint16_t count = 0;
|
||||
memcpy(&count, data + 8, 2);
|
||||
|
||||
if (autoId == 0) {
|
||||
m_linkEdit->setText(QString());
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось определить узел (см. лог)"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto nodeLabel = [this](uint64_t nid) {
|
||||
QString name = m_db ? m_db->getDisplayName(nid) : QString();
|
||||
if (name.isEmpty())
|
||||
name = QString("0x%1").arg(nid, 16, 16, QChar('0'));
|
||||
return name;
|
||||
};
|
||||
|
||||
/* блокируем сигналы, чтобы не перезапрашивать линк при добавлении пунктов */
|
||||
QSignalBlocker blocker(m_nodeCombo);
|
||||
m_nodeCombo->clear();
|
||||
|
||||
/* пункт 0 — узел, найденный авто-выбором */
|
||||
m_nodeCombo->addItem(nodeLabel(autoId) + QString::fromUtf8(" (auto)"),
|
||||
QVariant::fromValue((qulonglong)autoId));
|
||||
|
||||
/* остальные узлы (пропуская авто), свой помечаем (self) */
|
||||
int off = 10;
|
||||
for (int i = 0; i < (int)count && off + 8 <= len; i++) {
|
||||
uint64_t nid = 0;
|
||||
memcpy(&nid, data + off, 8);
|
||||
off += 8;
|
||||
if (nid == autoId) continue;
|
||||
QString label = nodeLabel(nid);
|
||||
if (m_db && nid == m_db->myNodeId())
|
||||
label += QString::fromUtf8(" (self)");
|
||||
m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid));
|
||||
}
|
||||
|
||||
/* строим линк для авто-узла (сигнал заблокирован) */
|
||||
requestLink(autoId);
|
||||
}
|
||||
|
||||
void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, const char* link) {
|
||||
QString cid = QString::fromUtf8(chId, chIdLen);
|
||||
if (cid != m_channelId) return;
|
||||
if (!link || link[0] == '\0') {
|
||||
m_linkEdit->setText(QString());
|
||||
m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось собрать ссылку (см. лог)"));
|
||||
m_qrLabel->clear();
|
||||
return;
|
||||
}
|
||||
setLink(QString::fromUtf8(link));
|
||||
}
|
||||
@@ -2,12 +2,31 @@
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <stdint.h>
|
||||
|
||||
class DbManager;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QLabel;
|
||||
|
||||
class ShareInviteDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ShareInviteDialog(const QString& link, QWidget* parent = nullptr);
|
||||
explicit ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent = nullptr);
|
||||
~ShareInviteDialog();
|
||||
|
||||
private slots:
|
||||
void onNodeSelected(int index);
|
||||
|
||||
private:
|
||||
QString m_link;
|
||||
void requestLink(uint64_t targetNodeId);
|
||||
void setLink(const QString& link);
|
||||
void onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len);
|
||||
void onLinkReady(const char* chId, int chIdLen, const char* link);
|
||||
|
||||
QString m_channelId;
|
||||
DbManager* m_db;
|
||||
QComboBox* m_nodeCombo;
|
||||
QLineEdit* m_linkEdit;
|
||||
QLabel* m_qrLabel;
|
||||
};
|
||||
@@ -430,24 +430,7 @@ void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
void MainWindow::showShareInviteDialog(const QString& channelId) {
|
||||
if (!m_db || !m_db->isOpen()) return;
|
||||
|
||||
QByteArray pubkey;
|
||||
if (m_node && m_node->isRunning())
|
||||
pubkey = QByteArray::fromHex(m_node->pubKeyHex().toLatin1());
|
||||
|
||||
if (pubkey.size() != 32) {
|
||||
QMessageBox::warning(this, "Error", "Failed to get node public key.");
|
||||
return;
|
||||
}
|
||||
|
||||
QList<NodeAddr> addrs = m_node->getInviteAddresses(m_db);
|
||||
|
||||
QString link = encodeInviteLink(channelId.toULongLong(), pubkey, addrs);
|
||||
if (link.isEmpty()) {
|
||||
QMessageBox::warning(this, "Error", "Failed to create invite link.");
|
||||
return;
|
||||
}
|
||||
|
||||
ShareInviteDialog dlg(link, this);
|
||||
ShareInviteDialog dlg(channelId, m_db, this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -488,40 +488,6 @@ void MessageList::refresh() {
|
||||
m_readDebounce->start();
|
||||
}
|
||||
|
||||
void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,
|
||||
const QByteArray& content, qint64 timestamp) {
|
||||
if (channelId != m_currentChannelId) return;
|
||||
|
||||
AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow();
|
||||
QString author = m_db ? authorName(authorNodeId) : QString("node_%1").arg(authorNodeId);
|
||||
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
|
||||
QColor color(acc.avatarColor);
|
||||
auto avatar = makeAvatar(color, letter);
|
||||
auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm");
|
||||
|
||||
/* dedup */
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
auto* it = m_model->item(r);
|
||||
if (it->data(MsgTimeRole).toString() == time
|
||||
&& it->data(MsgAuthorRole).toString() == author) return;
|
||||
}
|
||||
|
||||
auto *item = new QStandardItem();
|
||||
setMsg(item, author, QString::fromUtf8(content), time, avatar, false,
|
||||
false, QString(), QString(), QVariantList{});
|
||||
bool isSelf = m_db && (authorNodeId == m_db->myNodeId());
|
||||
item->setData(QVariant::fromValue(authorNodeId), MsgAuthorNodeIdRole);
|
||||
item->setData(isSelf ? 1 : 0, MsgIsOutgoingRole);
|
||||
setVoiceMessageRoles(item, content, QString(),
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, QByteArray());
|
||||
m_model->appendRow(item);
|
||||
|
||||
if (m_model->rowCount() > 50)
|
||||
m_model->removeRow(0);
|
||||
|
||||
m_view->scrollToBottom();
|
||||
}
|
||||
|
||||
void MessageList::saveChannelState() {
|
||||
if (m_currentChannelId.isEmpty() || m_model->rowCount() == 0) return;
|
||||
|
||||
@@ -679,44 +645,36 @@ void MessageList::updateMessageProgress(const QString& chId, int64_t msgId,
|
||||
}
|
||||
|
||||
void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) {
|
||||
QString ct;
|
||||
QString filePath;
|
||||
if (chId != m_currentChannelId || !m_db || !m_db->isOpen()) return;
|
||||
|
||||
MessageRow m = m_db->getMessageById(chId, msgId);
|
||||
if (m.id == 0) return;
|
||||
|
||||
int row = -1;
|
||||
bool wasPending = false;
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* item = m_model->item(r);
|
||||
if (!item) continue;
|
||||
if (item->data(MsgIdRole).toLongLong() == msgId) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
row = r;
|
||||
item->setData(2, MsgFileDownloadStateRole);
|
||||
item->setData(0, MsgMediaBlocksDoneRole);
|
||||
ct = item->data(MsgContentTypeRole).toString();
|
||||
filePath = item->data(MsgVoiceFileRole).toString();
|
||||
wasPending = it->data(MsgVoiceDownloadPendingRole).toBool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row >= 0 && chId == m_currentChannelId) {
|
||||
bool wasPending = false;
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
wasPending = it->data(MsgVoiceDownloadPendingRole).toBool();
|
||||
if (wasPending) it->setData(false, MsgVoiceDownloadPendingRole);
|
||||
break;
|
||||
}
|
||||
}
|
||||
refresh(); /* reload to get updated local_attrs with file path */
|
||||
/* auto-play only if user clicked play to initiate download */
|
||||
if (ct == "audio/opus" && wasPending) {
|
||||
QTimer::singleShot(500, this, [this, msgId]() {
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
QString fp = it->data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp))
|
||||
VoicePlayer::playOpusFile(fp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row < 0) return;
|
||||
|
||||
QStandardItem* item = m_model->item(row);
|
||||
setVoiceMessageRoles(item, m.data, m.contentType,
|
||||
m_db->dbPath() + "/media", chId, m.localAttrs);
|
||||
item->setData(0, MsgMediaBlocksDoneRole);
|
||||
item->setData(false, MsgVoiceDownloadPendingRole);
|
||||
|
||||
if (m.contentType == "audio/opus" && wasPending) {
|
||||
QString fp = item->data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp)) {
|
||||
QTimer::singleShot(500, this, [this, fp]() {
|
||||
MessageDelegate::stopAllAutoPlay();
|
||||
float actualDur = VoicePlayer::playOpusFile(fp);
|
||||
MessageDelegate::startAutoPlay(fp, actualDur);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ public:
|
||||
void reloadLocalNicks();
|
||||
void saveChannelState();
|
||||
static void cleanupAnimations();
|
||||
void addMessage(const QString& channelId, quint64 authorNodeId,
|
||||
const QByteArray& content, qint64 timestamp);
|
||||
void updateMessageProgress(const QString& chId, int64_t msgId, int blocksDone, int numBlocks);
|
||||
void onAttachmentDownloaded(const QString& chId, int64_t msgId);
|
||||
|
||||
|
||||
@@ -32,10 +32,19 @@ bool SoundManager::init() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundManager::clearCurrentPcm() {
|
||||
if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; }
|
||||
if (m_currentPcmBuffer) {
|
||||
ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer;
|
||||
ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr;
|
||||
}
|
||||
m_currentPcmSampleRate = 0;
|
||||
}
|
||||
|
||||
void SoundManager::shutdown() {
|
||||
if (!m_initialized) return;
|
||||
|
||||
stopCurrentPcm();
|
||||
clearCurrentPcm();
|
||||
|
||||
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
|
||||
if (it->sound) { ma_sound_uninit(it->sound); delete it->sound; }
|
||||
@@ -45,11 +54,6 @@ void SoundManager::shutdown() {
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine);
|
||||
if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; }
|
||||
if (m_currentPcmSound) { delete m_currentPcmSound; m_currentPcmSound = nullptr; }
|
||||
if (m_currentPcmBuffer) {
|
||||
ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer;
|
||||
ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit");
|
||||
m_initialized = false;
|
||||
}
|
||||
@@ -152,7 +156,7 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: not initialized");
|
||||
return;
|
||||
}
|
||||
if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound);
|
||||
clearCurrentPcm();
|
||||
m_currentPcmSampleRate = sampleRate;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch", sampleCount, sampleRate, channels);
|
||||
|
||||
@@ -189,8 +193,7 @@ void SoundManager::resumeCurrentPcm() {
|
||||
}
|
||||
|
||||
void SoundManager::stopCurrentPcm() {
|
||||
if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound);
|
||||
m_currentPcmSampleRate = 0;
|
||||
clearCurrentPcm();
|
||||
}
|
||||
|
||||
unsigned long long SoundManager::currentPcmCursor() const {
|
||||
|
||||
@@ -58,6 +58,8 @@ private:
|
||||
SoundManager() = default;
|
||||
~SoundManager();
|
||||
|
||||
void clearCurrentPcm();
|
||||
|
||||
struct LoadedSound {
|
||||
QByteArray mp3Data;
|
||||
ma_decoder* decoder = nullptr;
|
||||
|
||||
@@ -36,6 +36,8 @@ struct TOPO_GROUP_NODE;
|
||||
#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
|
||||
#define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */
|
||||
#define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */
|
||||
#define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */
|
||||
#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
|
||||
@@ -147,6 +149,14 @@ void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb);
|
||||
typedef void (*gui_admin_key_received_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb);
|
||||
|
||||
/* Callback: готова invite-ссылка (link = пустая строка при ошибке, детали в логе) */
|
||||
typedef void (*gui_invite_link_ready_fn)(const char* ch_id, int ch_id_len, const char* link);
|
||||
void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb);
|
||||
|
||||
/* Callback: список достижимых узлов-кандидатов для invite — data: [auto_node_id:8][count:2][node_id:8]* */
|
||||
typedef void (*gui_invite_candidates_fn)(const char* ch_id, int ch_id_len, const uint8_t* data, int len);
|
||||
void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb);
|
||||
|
||||
/* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */
|
||||
void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg);
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ static gui_member_updated_fn g_member_updated_cb = nullptr;
|
||||
static gui_member_removed_fn g_member_removed_cb = nullptr;
|
||||
static gui_bgp_info_fn g_bgp_info_cb = nullptr;
|
||||
static gui_admin_key_received_fn g_admin_key_received_cb = nullptr;
|
||||
static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr;
|
||||
static gui_invite_candidates_fn g_invite_candidates_cb = nullptr;
|
||||
static struct UASYNC* g_ua = nullptr;
|
||||
|
||||
/* ── GuiBridgeReceiver implementation ── */
|
||||
@@ -210,6 +212,29 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GUI_EVT_INVITE_LINK_READY:
|
||||
if (dlen >= 3) {
|
||||
uint8_t chLen = d[0];
|
||||
uint16_t linkLen = 0; memcpy(&linkLen, d + 1 + chLen, 2);
|
||||
if (dlen >= 1 + chLen + 2 + linkLen) {
|
||||
if (linkLen == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: INVITE_LINK_READY ch=%.*s — empty link (build failed)",
|
||||
chLen, (const char*)d + 1);
|
||||
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, "");
|
||||
} else {
|
||||
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, (const char*)(d + 1 + chLen + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GUI_EVT_INVITE_CANDIDATES:
|
||||
if (dlen >= 2) {
|
||||
uint8_t chLen = d[0];
|
||||
if (dlen >= 1 + chLen + 10) {
|
||||
if (g_invite_candidates_cb) g_invite_candidates_cb((const char*)d + 1, chLen, d + 1 + chLen, dlen - 1 - chLen);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
|
||||
break;
|
||||
@@ -335,6 +360,14 @@ void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb) {
|
||||
g_admin_key_received_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb) {
|
||||
g_invite_link_ready_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb) {
|
||||
g_invite_candidates_cb = cb;
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
/* ── uTun nodeinfo callback (called from uasync thread) ── */
|
||||
|
||||
@@ -2,14 +2,9 @@
|
||||
#include "utun_node.h"
|
||||
#include <cinttypes>
|
||||
#include <QDir>
|
||||
#include <QMap>
|
||||
#include <QPair>
|
||||
#include <csignal>
|
||||
#include "../../lib/socket_compat.h"
|
||||
|
||||
#include "../db/db_manager.h"
|
||||
#include "../src/invite_link.h"
|
||||
|
||||
// === C headers ===
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
@@ -139,116 +134,6 @@ bool UtunNode::ntpEnabled() const {
|
||||
return m_instance ? m_instance->ntp.enabled : false;
|
||||
}
|
||||
|
||||
QList<NodeAddr> UtunNode::getInviteAddresses(DbManager* db) {
|
||||
(void)db;
|
||||
QList<NodeAddr> addrs;
|
||||
QMap<QPair<QByteArray, quint16>, uint8_t> addrProto; // key=(addr,port) → proto bits
|
||||
|
||||
auto add_addr = [&](const QByteArray& addr, quint16 port, int family,
|
||||
int sockId, uint8_t proto) {
|
||||
QPair<QByteArray, quint16> key(addr, port);
|
||||
uint8_t existing = addrProto.value(key, 0);
|
||||
addrProto[key] = existing | proto;
|
||||
// Store sockId/family for serialization (overwrite is fine — same port)
|
||||
};
|
||||
|
||||
auto append_from_addr_proto = [&](const QByteArray& addr, quint16 port,
|
||||
int family, int sockId, uint8_t proto) {
|
||||
NodeAddr a;
|
||||
a.family = family;
|
||||
a.address = addr;
|
||||
a.port = port;
|
||||
a.socketId = sockId;
|
||||
a.protocol = proto;
|
||||
addrs.append(a);
|
||||
};
|
||||
if (m_instance) {
|
||||
/* 1) UDP from active ETCP links */
|
||||
struct ll_entry* entry = m_instance->connections->head;
|
||||
while (entry) {
|
||||
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
|
||||
struct ETCP_CONN* conn = ce->conn;
|
||||
if (conn && conn->initialized && conn->links_up > 0) {
|
||||
struct ETCP_LINK* link = conn->links;
|
||||
while (link) {
|
||||
if (!link->is_tcp && link->link_state == 3 && link->nat_type == NAT_TYPE_DIRECT) {
|
||||
struct sockaddr_storage* sa = &link->remote_addr;
|
||||
if (sa->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
QByteArray addr((const char*)&sin->sin_addr, 4);
|
||||
add_addr(addr, ntohs(sin->sin_port), 4,
|
||||
link->conn ? (int)link->conn->sock_id : 0, INVITE_PROTO_UDP);
|
||||
} else {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
QByteArray addr((const char*)&sin6->sin6_addr, 16);
|
||||
add_addr(addr, ntohs(sin6->sin6_port), 6,
|
||||
link->conn ? (int)link->conn->sock_id : 0, INVITE_PROTO_UDP);
|
||||
}
|
||||
}
|
||||
link = link->next;
|
||||
}
|
||||
}
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
/* 2) UDP from local ETCP sockets (fallback) */
|
||||
{
|
||||
struct ETCP_SOCKET* sock = m_instance->etcp_sockets;
|
||||
while (sock) {
|
||||
if (sock->is_tcp) { sock = sock->next; continue; }
|
||||
struct sockaddr_storage* sa = NULL;
|
||||
if (sock->nat_addr.ss_family != 0)
|
||||
sa = &sock->nat_addr;
|
||||
else if (sock->interface_addr.ss_family != 0)
|
||||
sa = &sock->interface_addr;
|
||||
if (sa) {
|
||||
if (sa->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
QByteArray addr((const char*)&sin->sin_addr, 4);
|
||||
add_addr(addr, ntohs(sin->sin_port), 4, (int)sock->sock_id, INVITE_PROTO_UDP);
|
||||
} else {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
QByteArray addr((const char*)&sin6->sin6_addr, 16);
|
||||
add_addr(addr, ntohs(sin6->sin6_port), 6, (int)sock->sock_id, INVITE_PROTO_UDP);
|
||||
}
|
||||
}
|
||||
sock = sock->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) TCP from local TCP sockets */
|
||||
{
|
||||
struct ETCP_SOCKET* s = m_instance->etcp_sockets;
|
||||
while (s) {
|
||||
if (!s->is_tcp) { s = s->next; continue; }
|
||||
struct sockaddr_storage* sa = &s->interface_addr;
|
||||
if (sa->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
QByteArray addr((const char*)&sin->sin_addr, 4);
|
||||
add_addr(addr, ntohs(sin->sin_port), 4, (int)s->sock_id, INVITE_PROTO_TCP);
|
||||
} else if (sa->ss_family == AF_INET6) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
QByteArray addr((const char*)&sin6->sin6_addr, 16);
|
||||
add_addr(addr, ntohs(sin6->sin6_port), 6, (int)s->sock_id, INVITE_PROTO_TCP);
|
||||
}
|
||||
s = s->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* 4) Dedup by (addr,port); emit one entry per proto (UDP и TCP раздельно, без BOTH) */
|
||||
for (auto it = addrProto.begin(); it != addrProto.end(); ++it) {
|
||||
QByteArray addr = it.key().first;
|
||||
quint16 port = it.key().second;
|
||||
uint8_t proto = it.value();
|
||||
int family = (addr.size() == 16) ? 6 : 4;
|
||||
if (proto & INVITE_PROTO_UDP) append_from_addr_proto(addr, port, family, 0, INVITE_PROTO_UDP);
|
||||
if (proto & INVITE_PROTO_TCP) append_from_addr_proto(addr, port, family, 0, INVITE_PROTO_TCP);
|
||||
}
|
||||
}
|
||||
|
||||
return addrs;
|
||||
}
|
||||
|
||||
void UtunNode::recvCallback(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
if (!g_currentNode || !entry || !entry->dgram || entry->len < ROUTER_SVC_HDR_SIZE) return;
|
||||
uint64_t src;
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
class DbManager;
|
||||
struct NodeAddr;
|
||||
|
||||
class UtunNode : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -37,7 +34,6 @@ public:
|
||||
|
||||
QString nodeIdHex() const;
|
||||
QString pubKeyHex() const;
|
||||
QList<NodeAddr> getInviteAddresses(DbManager* db);
|
||||
int64_t ntpOffsetUs() const;
|
||||
bool ntpSynced() const;
|
||||
bool ntpEnabled() const;
|
||||
|
||||
Reference in new issue
Block a user