diff --git a/lib/platform_compat.c b/lib/platform_compat.c index 6e14af8d..80d4656c 100644 --- a/lib/platform_compat.c +++ b/lib/platform_compat.c @@ -281,26 +281,94 @@ uint32_t get_default_route_netif_index(int family) { freeifaddrs(ifaddr); return result; } -#else +#else /* _WIN32 */ + +/* ── Windows: реализация через IP Helper (GetAdaptersAddresses) ── */ + +static PIP_ADAPTER_ADDRESSES plat_win_get_adapters(ULONG family) { + PIP_ADAPTER_ADDRESSES adapters = NULL; + ULONG size = 0; + ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + if (GetAdaptersAddresses(family, flags, NULL, NULL, &size) != ERROR_BUFFER_OVERFLOW) return NULL; + adapters = (PIP_ADAPTER_ADDRESSES)malloc(size); + if (!adapters) return NULL; + if (GetAdaptersAddresses(family, flags, NULL, adapters, &size) != NO_ERROR) { free(adapters); return NULL; } + return adapters; +} + +static PIP_ADAPTER_ADDRESSES plat_win_find_adapter(PIP_ADAPTER_ADDRESSES adapters, uint32_t ifindex) { + for (PIP_ADAPTER_ADDRESSES a = adapters; a; a = a->Next) { + if (a->IfIndex == ifindex || a->Ipv4IfIndex == ifindex || a->Ipv6IfIndex == ifindex) return a; + } + return NULL; +} + uint32_t get_interface_ip_by_index(uint32_t netif_index) { - (void)netif_index; - return 0; + if (!netif_index) return 0; + PIP_ADAPTER_ADDRESSES adapters = plat_win_get_adapters(AF_INET); + if (!adapters) return 0; + uint32_t result = 0; + PIP_ADAPTER_ADDRESSES a = plat_win_find_adapter(adapters, netif_index); + if (a) { + for (PIP_ADAPTER_UNICAST_ADDRESS u = a->FirstUnicastAddress; u; u = u->Next) { + if (u->Address.lpSockaddr && u->Address.lpSockaddr->sa_family == AF_INET) { + result = ((const struct sockaddr_in*)u->Address.lpSockaddr)->sin_addr.s_addr; + break; + } + } + } + free(adapters); + return result; } int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out) { - (void)netif_index; (void)temporary; - if (addr_out) memset(addr_out, 0, 16); - return -1; + /* Windows IP Helper не различает temporary/stable без запроса Addresses — отдаём первый не-link-local */ + (void)temporary; + return get_interface_ipv6_addr_nl(netif_index, 1, addr_out); } int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out) { - (void)netif_index; (void)prefer_stable; - if (addr_out) memset(addr_out, 0, 16); - return -1; + (void)prefer_stable; + if (!addr_out) return -1; + memset(addr_out, 0, 16); + if (!netif_index) return -1; + PIP_ADAPTER_ADDRESSES adapters = plat_win_get_adapters(AF_INET6); + if (!adapters) return -1; + int found = 0; + PIP_ADAPTER_ADDRESSES a = plat_win_find_adapter(adapters, netif_index); + if (a) { + for (PIP_ADAPTER_UNICAST_ADDRESS u = a->FirstUnicastAddress; u; u = u->Next) { + if (!u->Address.lpSockaddr || u->Address.lpSockaddr->sa_family != AF_INET6) continue; + const uint8_t* b = ((const struct sockaddr_in6*)u->Address.lpSockaddr)->sin6_addr.s6_addr; + if ((b[0] == 0xfe && (b[1] & 0xc0) == 0x80)) continue; /* link-local */ + { static const uint8_t lb[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; + if (memcmp(b, lb, 16) == 0) continue; } /* loopback */ + { static const uint8_t z[16]; if (memcmp(b, z, 16) == 0) continue; } /* :: */ + memcpy(addr_out, b, 16); + found = 1; + break; + } + } + free(adapters); + return found ? 0 : -1; } uint32_t get_default_route_netif_index(int family) { - (void)family; - return 0; + struct sockaddr_storage dst; + memset(&dst, 0, sizeof(dst)); + if (family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)&dst; + sin->sin_family = AF_INET; + inet_pton(AF_INET, "8.8.8.8", &sin->sin_addr); + } else if (family == AF_INET6) { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&dst; + sin6->sin6_family = AF_INET6; + inet_pton(AF_INET6, "2001:4860:4860::8888", &sin6->sin6_addr); + } else { + return 0; + } + DWORD idx = 0; + if (GetBestInterfaceEx((const SOCKADDR*)&dst, &idx) != NO_ERROR) return 0; + return (uint32_t)idx; } #endif diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c index 358ab730..65709dfe 100644 --- a/src/transport_layer/auto_socket.c +++ b/src/transport_layer/auto_socket.c @@ -698,19 +698,91 @@ static void auto_socket_scan_all(struct AUTO_SOCKET* as) { #else /* _WIN32 */ +/* Собирает IfIndex всех UP не-loopback адаптеров. Возвращает число найденных (до max_count). */ +static int win_collect_up_ifaces(uint32_t* out_ifindex, int max_count) { + PIP_ADAPTER_ADDRESSES adapters = NULL; + ULONG size = 0; + ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + DWORD rv = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, NULL, &size); + if (rv != ERROR_BUFFER_OVERFLOW) return 0; + adapters = (PIP_ADAPTER_ADDRESSES)u_malloc(size); + if (!adapters) return 0; + rv = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapters, &size); + if (rv != NO_ERROR) { u_free(adapters); return 0; } + + int count = 0; + for (PIP_ADAPTER_ADDRESSES a = adapters; a && count < max_count; a = a->Next) { + if (a->OperStatus != IfOperStatusUp) continue; + if (a->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + out_ifindex[count++] = a->IfIndex; + } + u_free(adapters); + return count; +} + static int scan_and_classify_iface(uint32_t ifindex, const char* ifname, int* out_has_v4, int* out_has_v6, uint8_t* out_v4_type, uint8_t* out_v6_type) { - (void)ifindex; (void)ifname; + (void)ifname; *out_has_v4 = 0; *out_has_v6 = 0; *out_v4_type = CFG_SERVER_TYPE_NAT; *out_v6_type = CFG_SERVER_TYPE_NAT; + + PIP_ADAPTER_ADDRESSES adapters = NULL; + ULONG size = 0; + ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER; + DWORD rv = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, NULL, &size); + if (rv != ERROR_BUFFER_OVERFLOW) return -1; + adapters = (PIP_ADAPTER_ADDRESSES)u_malloc(size); + if (!adapters) return -1; + rv = GetAdaptersAddresses(AF_UNSPEC, flags, NULL, adapters, &size); + if (rv != NO_ERROR) { u_free(adapters); return -1; } + + PIP_ADAPTER_ADDRESSES a = adapters; + while (a && a->IfIndex != ifindex) a = a->Next; + if (!a || a->OperStatus != IfOperStatusUp || a->IfType == IF_TYPE_SOFTWARE_LOOPBACK) { + u_free(adapters); + return 0; + } + + for (PIP_ADAPTER_UNICAST_ADDRESS u = a->FirstUnicastAddress; u; u = u->Next) { + if (!u->Address.lpSockaddr) continue; + if (u->Address.lpSockaddr->sa_family == AF_INET) { + const struct sockaddr_in* sin = (const struct sockaddr_in*)u->Address.lpSockaddr; + if (sin->sin_addr.s_addr == INADDR_ANY) continue; + *out_has_v4 = 1; + if (is_ipv4_public(sin->sin_addr.s_addr)) *out_v4_type = CFG_SERVER_TYPE_PUBLIC; + } else if (u->Address.lpSockaddr->sa_family == AF_INET6) { + const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)u->Address.lpSockaddr; + int cls = v6_classify(sin6->sin6_addr.s6_addr); + if (cls == AS_V6_OTH || cls == AS_V6_LL) continue; + *out_has_v6 = 1; + if (cls == AS_V6_DIR) *out_v6_type = CFG_SERVER_TYPE_PUBLIC; + } + } + u_free(adapters); return 0; } static void auto_socket_scan_all(struct AUTO_SOCKET* as) { - (void)as; - DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] Windows: auto_socket_scan_all not yet implemented"); + uint32_t up[64]; + int up_count = win_collect_up_ifaces(up, 64); + + for (int i = 0; i < up_count; i++) { + char ifname[IF_NAMESIZE]; + if (!if_indextoname(up[i], ifname)) snprintf(ifname, sizeof(ifname), "if%u", up[i]); + reconcile_iface(as, up[i], ifname); + } + + /* prune: удаляем сокеты адаптеров, которых больше нет в UP-списке (или они DOWN) */ + struct auto_sock_iface** pp = &as->ifaces; + while (*pp) { + struct auto_sock_iface* ifa = *pp; + int found = 0; + for (int i = 0; i < up_count; i++) if (up[i] == ifa->netif_index) { found = 1; break; } + if (!found) { remove_iface_sockets(as, ifa->netif_index); continue; } + pp = &ifa->next; + } } #endif /* _WIN32 */ diff --git a/tools/chatgui/src/networksettingspage.cpp b/tools/chatgui/src/networksettingspage.cpp index 5e084042..1cebb6d1 100644 --- a/tools/chatgui/src/networksettingspage.cpp +++ b/tools/chatgui/src/networksettingspage.cpp @@ -1,14 +1,19 @@ -// networksettingspage.cpp — Network configuration form +// networksettingspage.cpp — Network configuration form (список listen-сокетов) #include "networksettingspage.h" #include "../transport/config_updater.h" #include #include +#include #include +#include +#include +#include +#include #include #include #include -static const char* SECTION = "server:lan"; +static const QString AUTO_IP_LABEL = "Auto (0.0.0.0)"; NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) : QWidget(parent) @@ -17,175 +22,298 @@ NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) layout->setSpacing(12); layout->setContentsMargins(0, 0, 0, 0); - auto* title = new QLabel("Listen Address", this); + auto* title = new QLabel("Listen Sockets", this); title->setStyleSheet("font-weight: bold; font-size: 13px;"); layout->addWidget(title); - // Interface - auto* ifRow = new QHBoxLayout(); - ifRow->addWidget(new QLabel("Interface:", this)); - m_interfaceCombo = new QComboBox(this); - m_interfaceCombo->setMinimumWidth(180); - ifRow->addWidget(m_interfaceCombo, 1); - layout->addLayout(ifRow); + m_autoSocketsCheck = new QCheckBox("Auto sockets (UDP+TCP from interfaces)", this); + layout->addWidget(m_autoSocketsCheck); + + m_serverList = new QListWidget(this); + m_serverList->setMinimumHeight(110); + layout->addWidget(m_serverList); + + auto* btnRow = new QHBoxLayout(); + m_addBtn = new QPushButton("Add", this); + m_removeBtn = new QPushButton("Remove", this); + btnRow->addWidget(m_addBtn); + btnRow->addWidget(m_removeBtn); + btnRow->addStretch(); + layout->addLayout(btnRow); + + // --- форма правки выбранного сокета --- + auto* formLayout = new QGridLayout(); + + formLayout->addWidget(new QLabel("Name:", this), 0, 0); + m_nameEdit = new QLineEdit(this); + m_nameEdit->setMinimumWidth(180); + formLayout->addWidget(m_nameEdit, 0, 1); - // IP - auto* ipRow = new QHBoxLayout(); - ipRow->addWidget(new QLabel("IP Address:", this)); + formLayout->addWidget(new QLabel("IP Address:", this), 1, 0); m_ipCombo = new QComboBox(this); m_ipCombo->setEditable(true); m_ipCombo->setMinimumWidth(180); - ipRow->addWidget(m_ipCombo, 1); - layout->addLayout(ipRow); + formLayout->addWidget(m_ipCombo, 1, 1); - // Port - auto* portRow = new QHBoxLayout(); - portRow->addWidget(new QLabel("Port:", this)); + formLayout->addWidget(new QLabel("Port:", this), 2, 0); m_portSpin = new QSpinBox(this); m_portSpin->setRange(1, 65535); m_portSpin->setValue(1333); - m_portSpin->setMinimumWidth(100); - portRow->addWidget(m_portSpin); - portRow->addStretch(); - layout->addLayout(portRow); - - // Protocol - auto* protoRow = new QHBoxLayout(); - protoRow->addWidget(new QLabel("Protocol:", this)); + formLayout->addWidget(m_portSpin, 2, 1); + + formLayout->addWidget(new QLabel("Protocol:", this), 3, 0); m_protoCombo = new QComboBox(this); m_protoCombo->addItems({"UDP", "TCP"}); - m_protoCombo->setMinimumWidth(100); - protoRow->addWidget(m_protoCombo); - protoRow->addStretch(); - layout->addLayout(protoRow); + formLayout->addWidget(m_protoCombo, 3, 1); + + formLayout->addWidget(new QLabel("Type:", this), 4, 0); + m_typeCombo = new QComboBox(this); + m_typeCombo->addItems({"public", "nat", "private", "local"}); + formLayout->addWidget(m_typeCombo, 4, 1); + + formLayout->addWidget(new QLabel("Interface:", this), 5, 0); + m_interfaceCombo = new QComboBox(this); + m_interfaceCombo->setMinimumWidth(180); + formLayout->addWidget(m_interfaceCombo, 5, 1); + + layout->addLayout(formLayout); auto* note = new QLabel("Changes take effect after restart.", this); note->setStyleSheet("color: gray; font-size: 11px;"); layout->addWidget(note); - layout->addStretch(); populateInterfaces(); - connect(m_interfaceCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &NetworkSettingsPage::onInterfaceChanged); + populateAddresses(); + + connect(m_autoSocketsCheck, &QCheckBox::toggled, this, &NetworkSettingsPage::onAutoSocketsToggled); + connect(m_addBtn, &QPushButton::clicked, this, &NetworkSettingsPage::onAddServer); + connect(m_removeBtn, &QPushButton::clicked, this, &NetworkSettingsPage::onRemoveServer); + connect(m_serverList, &QListWidget::currentRowChanged, this, [this](int) { onServerSelected(); }); + + connect(m_nameEdit, &QLineEdit::textChanged, this, [this](const QString&) { onFieldChanged(); }); + connect(m_ipCombo, &QComboBox::editTextChanged, this, [this](const QString&) { onFieldChanged(); }); + connect(m_portSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int) { onFieldChanged(); }); + connect(m_protoCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { onFieldChanged(); }); + connect(m_typeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { onFieldChanged(); }); + connect(m_interfaceCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { onFieldChanged(); }); +} + +static void splitAddr(const QString& addr, QString& ip, int& port) { + int lastColon = addr.lastIndexOf(':'); + if (lastColon > 0) { + ip = addr.left(lastColon); + port = addr.mid(lastColon + 1).toInt(); + } else { + ip = addr; + port = 0; + } +} + +void NetworkSettingsPage::populateInterfaces() { + m_interfaceCombo->addItem("Auto (any)"); + for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::IsLoopBack) continue; + if (!(iface.flags() & QNetworkInterface::IsUp)) continue; + m_interfaceCombo->addItem(iface.name()); + } +} + +void NetworkSettingsPage::populateAddresses() { + m_ipCombo->addItem(AUTO_IP_LABEL); + for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::IsLoopBack) continue; + if (!(iface.flags() & QNetworkInterface::IsUp)) continue; + for (const QNetworkAddressEntry& entry : iface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) + m_ipCombo->addItem(entry.ip().toString()); + } + } +} + +void NetworkSettingsPage::applyAutoSocketsState() { + bool manual = !m_autoSocketsCheck->isChecked(); + m_serverList->setEnabled(manual); + m_addBtn->setEnabled(manual); + m_removeBtn->setEnabled(manual); + m_nameEdit->setEnabled(manual); + m_ipCombo->setEnabled(manual); + m_portSpin->setEnabled(manual); + m_protoCombo->setEnabled(manual); + m_typeCombo->setEnabled(manual); + m_interfaceCombo->setEnabled(manual); +} + +void NetworkSettingsPage::refreshServerList(int selectRow) { + m_updating = true; + m_serverList->clear(); + for (const ConfigServer& s : m_servers) { + m_serverList->addItem(QString("%1 %2 (%3)").arg(s.name, s.addr, s.transport)); + } + if (selectRow >= 0 && selectRow < m_serverList->count()) + m_serverList->setCurrentRow(selectRow); + else if (m_serverList->count() > 0) + m_serverList->setCurrentRow(0); + m_updating = false; +} + +void NetworkSettingsPage::loadFormFromSelected() { + int row = m_serverList->currentRow(); + if (row < 0 || row >= m_servers.size()) return; + const ConfigServer& s = m_servers[row]; + + m_updating = true; + m_nameEdit->setText(s.name); + QString ip; int port = 0; + splitAddr(s.addr, ip, port); + if (ip == "0.0.0.0") ip = AUTO_IP_LABEL; + m_ipCombo->setCurrentText(ip); + m_portSpin->setValue(port > 0 ? port : 1333); + m_protoCombo->setCurrentIndex(s.transport == "tcp" ? 1 : 0); + int typeIdx = m_typeCombo->findText(s.type); + m_typeCombo->setCurrentIndex(typeIdx >= 0 ? typeIdx : 0); + int ifIdx = m_interfaceCombo->findText(s.netif); + m_interfaceCombo->setCurrentIndex(ifIdx >= 0 ? ifIdx : 0); + m_updating = false; +} + +void NetworkSettingsPage::updateSelectedServerFromForm() { + int row = m_serverList->currentRow(); + if (row < 0 || row >= m_servers.size()) return; + + ConfigServer& s = m_servers[row]; + s.name = m_nameEdit->text().trimmed(); + + QString ip = m_ipCombo->currentText().trimmed(); + if (ip == AUTO_IP_LABEL) ip = "0.0.0.0"; + s.addr = ip + ":" + QString::number(m_portSpin->value()); + + s.transport = (m_protoCombo->currentIndex() == 1) ? "tcp" : "udp"; + s.type = m_typeCombo->currentText().trimmed(); + QString iface = m_interfaceCombo->currentText(); + s.netif = iface.startsWith("Auto") ? QString() : iface; +} + +void NetworkSettingsPage::updateSelectedListItemText() { + int row = m_serverList->currentRow(); + if (row < 0 || row >= m_servers.size()) return; + const ConfigServer& s = m_servers[row]; + m_updating = true; + m_serverList->item(row)->setText(QString("%1 %2 (%3)").arg(s.name, s.addr, s.transport)); + m_updating = false; } void NetworkSettingsPage::loadFromConfig(const QString& configPath) { - // Read [server:lan] section from INI file - QString addr, netif, transport; + m_configPath = configPath; + m_servers.clear(); + QFile f(configPath); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - bool inSection = false; + QString section; + ConfigServer cur; + bool inServer = false; QTextStream in(&f); while (!in.atEnd()) { - QString line = in.readLine(); + QString line = in.readLine().trimmed(); if (line.startsWith('[') && line.endsWith(']')) { - inSection = (line.mid(1, line.length() - 2) == SECTION); + if (inServer) m_servers.append(cur); + inServer = false; + section = line.mid(1, line.length() - 2); + if (section.startsWith("server:")) { + cur = ConfigServer(); + cur.name = section.mid(7); + cur.transport = "udp"; + cur.type = "public"; + inServer = true; + } continue; } - if (!inSection || !line.contains('=')) continue; + if (!inServer || !line.contains('=')) continue; QString key = line.section('=', 0, 0).trimmed(); QString val = line.section('=', 1).trimmed(); - if (key == "addr") addr = val; - else if (key == "netif") netif = val; - else if (key == "transport") transport = val; + if (key == "addr") cur.addr = val; + else if (key == "transport") cur.transport = val; + else if (key == "type") cur.type = val; + else if (key == "netif") cur.netif = val; } + if (inServer) m_servers.append(cur); f.close(); } - // Parse addr: IP:port - QString ip; - if (!addr.isEmpty()) { - int lastColon = addr.lastIndexOf(':'); - if (lastColon > 0) { - ip = addr.left(lastColon); - int port = addr.mid(lastColon + 1).toInt(); - if (port > 0 && port <= 65535) m_portSpin->setValue(port); + /* auto_sockets из [global] */ + bool autoSockets = false; + f.setFileName(configPath); + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString section; + QTextStream in(&f); + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + if (line.startsWith('[') && line.endsWith(']')) { + section = line.mid(1, line.length() - 2); + continue; + } + if ((section == "global" || section.isEmpty()) && line.startsWith("auto_sockets=")) { + QString v = line.section('=', 1).trimmed(); + autoSockets = (v.compare("yes", Qt::CaseInsensitive) == 0 || v == "1"); + break; + } } + f.close(); } + m_autoSocketsCheck->setChecked(autoSockets); - // Set protocol - int protoIdx = (transport == "tcp") ? 1 : 0; - m_protoCombo->setCurrentIndex(protoIdx); - - // Set interface - int ifIdx = m_interfaceCombo->findText(netif); - if (ifIdx >= 0) m_interfaceCombo->setCurrentIndex(ifIdx); - - // Set IP — populate addresses first, then select - populateAddresses(netif.isEmpty() ? QString() : netif); - if (ip.isEmpty() || ip == "0.0.0.0") ip = "Auto (0.0.0.0)"; - int ipIdx = m_ipCombo->findText(ip); - if (ipIdx >= 0) - m_ipCombo->setCurrentIndex(ipIdx); - else - m_ipCombo->setCurrentText(ip); + refreshServerList(); + applyAutoSocketsState(); } void NetworkSettingsPage::saveToConfig(const QString& configPath) const { - // addr - QString ip = m_ipCombo->currentText().trimmed(); - if (ip.isEmpty() || ip == "Auto (0.0.0.0)") ip = "0.0.0.0"; - config_update_option(configPath, SECTION, "addr", - QString("%1:%2").arg(ip).arg(m_portSpin->value()), - CONFIG_UPDATE_REPLACE); - - // netif: set or remove (empty value means no netif binding) - QString iface = m_interfaceCombo->currentText(); - if (iface.startsWith("Auto")) - iface.clear(); - config_update_option(configPath, SECTION, "netif", iface, CONFIG_UPDATE_REPLACE); - - // transport - config_update_option(configPath, SECTION, "transport", - m_protoCombo->currentText().toLower(), - CONFIG_UPDATE_REPLACE); + /* 1. Серверные секции — полная перезапись */ + config_rewrite_servers(configPath, m_servers); - // IPv6 dual-stack: always add [server:lan6] with same port - config_update_option(configPath, "server:lan6", "addr", - QString("[::]:%1").arg(m_portSpin->value()), + /* 2. auto_sockets */ + config_update_option(configPath, "global", "auto_sockets", + m_autoSocketsCheck->isChecked() ? "yes" : "no", CONFIG_UPDATE_REPLACE); } -void NetworkSettingsPage::onInterfaceChanged(int index) { - Q_UNUSED(index); - QString iface = m_interfaceCombo->currentText(); - populateAddresses(iface.startsWith("Auto") ? QString() : iface); +void NetworkSettingsPage::onAutoSocketsToggled(bool /*on*/) { + applyAutoSocketsState(); } -void NetworkSettingsPage::populateInterfaces() { - m_interfaceCombo->addItem("Auto (any)"); - for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { - if (iface.flags() & QNetworkInterface::IsLoopBack) continue; - if (!(iface.flags() & QNetworkInterface::IsUp)) continue; - m_interfaceCombo->addItem(iface.name()); +void NetworkSettingsPage::onAddServer() { + int n = m_servers.size() + 1; + QString name; + for (;;) { + name = QString("srv%1").arg(n); + bool used = false; + for (const ConfigServer& s : m_servers) if (s.name == name) { used = true; break; } + if (!used) break; + n++; } + ConfigServer s; + s.name = name; + s.addr = "0.0.0.0:1333"; + s.transport = "udp"; + s.type = "public"; + m_servers.append(s); + refreshServerList(m_servers.size() - 1); + loadFormFromSelected(); } -void NetworkSettingsPage::populateAddresses(const QString& ifaceName) { - QString current = m_ipCombo->currentText(); - m_ipCombo->clear(); - m_ipCombo->addItem("Auto (0.0.0.0)"); +void NetworkSettingsPage::onRemoveServer() { + int row = m_serverList->currentRow(); + if (row < 0 || row >= m_servers.size()) return; + m_servers.removeAt(row); + refreshServerList(row); + loadFormFromSelected(); +} - if (ifaceName.isEmpty()) { - for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { - if (iface.flags() & QNetworkInterface::IsLoopBack) continue; - if (!(iface.flags() & QNetworkInterface::IsUp)) continue; - for (const QNetworkAddressEntry& entry : iface.addressEntries()) { - if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) - m_ipCombo->addItem(entry.ip().toString()); - } - } - } else { - QNetworkInterface iface = QNetworkInterface::interfaceFromName(ifaceName); - if (iface.isValid()) { - for (const QNetworkAddressEntry& entry : iface.addressEntries()) { - if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) - m_ipCombo->addItem(entry.ip().toString()); - } - } - } +void NetworkSettingsPage::onServerSelected() { + loadFormFromSelected(); +} - int idx = m_ipCombo->findText(current); - if (idx >= 0) m_ipCombo->setCurrentIndex(idx); +void NetworkSettingsPage::onFieldChanged() { + if (m_updating) return; + updateSelectedServerFromForm(); + updateSelectedListItemText(); } diff --git a/tools/chatgui/src/networksettingspage.h b/tools/chatgui/src/networksettingspage.h index 72770c34..aebeabf5 100644 --- a/tools/chatgui/src/networksettingspage.h +++ b/tools/chatgui/src/networksettingspage.h @@ -1,10 +1,17 @@ -// networksettingspage.h — Network configuration form +// networksettingspage.h — Network configuration form (список listen-сокетов) #pragma once #include #include #include #include +#include +#include "../transport/config_updater.h" + +class QCheckBox; +class QLineEdit; +class QListWidget; +class QPushButton; class NetworkSettingsPage : public QWidget { Q_OBJECT @@ -15,14 +22,33 @@ public: void saveToConfig(const QString& configPath) const; private slots: - void onInterfaceChanged(int index); + void onAutoSocketsToggled(bool on); + void onAddServer(); + void onRemoveServer(); + void onServerSelected(); + void onFieldChanged(); private: void populateInterfaces(); - void populateAddresses(const QString& ifaceName); + void populateAddresses(); + void applyAutoSocketsState(); + void refreshServerList(int selectRow = -1); + void loadFormFromSelected(); + void updateSelectedServerFromForm(); + void updateSelectedListItemText(); + + QCheckBox* m_autoSocketsCheck; + QListWidget* m_serverList; + QPushButton* m_addBtn; + QPushButton* m_removeBtn; + QLineEdit* m_nameEdit; + QComboBox* m_ipCombo; + QSpinBox* m_portSpin; + QComboBox* m_protoCombo; + QComboBox* m_typeCombo; + QComboBox* m_interfaceCombo; - QComboBox* m_interfaceCombo; - QComboBox* m_ipCombo; - QSpinBox* m_portSpin; - QComboBox* m_protoCombo; + QList m_servers; + QString m_configPath; + bool m_updating = false; }; diff --git a/tools/chatgui/transport/config_updater.cpp b/tools/chatgui/transport/config_updater.cpp index ffabf088..bce8c062 100644 --- a/tools/chatgui/transport/config_updater.cpp +++ b/tools/chatgui/transport/config_updater.cpp @@ -85,3 +85,75 @@ bool config_update_option(const QString& filePath, const QString& section, f.close(); return true; } + +static bool is_section_header(const QString& line) { + QString t = line.trimmed(); + return t.startsWith('[') && t.endsWith(']'); +} + +bool config_rewrite_servers(const QString& filePath, const QList& servers) { + QFile f(filePath); + QStringList lines; + + if (f.exists()) { + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return false; + QTextStream in(&f); + while (!in.atEnd()) { + QString line = in.readLine(0); + if (line.endsWith('\r')) line.chop(1); + lines.append(line); + } + f.close(); + } + + /* 1. Удалить все [server:*] секции (заголовок + ключи + хвостовую пустую строку-разделитель) */ + QStringList filtered; + filtered.reserve(lines.size()); + for (int i = 0; i < lines.size(); i++) { + QString t = lines[i].trimmed(); + if (t.startsWith("[server:") && t.endsWith(']')) { + int j = i + 1; + while (j < lines.size() && !is_section_header(lines[j])) j++; + i = j - 1; /* for-loop сделает i++ → j: заголовок+ключи+пустая строка пропущены */ + continue; + } + filtered << lines[i]; + } + lines = filtered; + + /* 2. Собрать блок секций [server:*] */ + QStringList block; + for (const ConfigServer& s : servers) { + block << ("[server:" + s.name + "]"); + block << ("addr=" + s.addr); + block << ("transport=" + (s.transport.isEmpty() ? QString("udp") : s.transport)); + block << ("type=" + (s.type.isEmpty() ? QString("public") : s.type)); + if (!s.netif.isEmpty()) block << ("netif=" + s.netif); + block << ""; /* разделитель: одна пустая строка между секциями */ + } + + /* 3. Точка вставки: после секции [global] (перед следующей секцией) */ + int insertPos = lines.size(); + for (int i = 0; i < lines.size(); i++) { + if (lines[i].trimmed() == "[global]") { + int j = i + 1; + while (j < lines.size() && !is_section_header(lines[j])) j++; + insertPos = j; + break; + } + } + + /* 4. Вставить блок (если непустой) */ + if (!block.isEmpty()) { + for (int i = block.size() - 1; i >= 0; i--) + lines.insert(insertPos, block[i]); + } + + /* 5. Записать */ + if (!f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false; + QTextStream out(&f); + for (const QString& l : lines) + out << l << '\n'; + f.close(); + return true; +} diff --git a/tools/chatgui/transport/config_updater.h b/tools/chatgui/transport/config_updater.h index 9d21d6a3..867b68da 100644 --- a/tools/chatgui/transport/config_updater.h +++ b/tools/chatgui/transport/config_updater.h @@ -4,12 +4,22 @@ #include #include +#include enum ConfigUpdateFlag { CONFIG_UPDATE_REPLACE = 0, // replace existing param (single value) CONFIG_UPDATE_APPEND = 1 // add new param even if exists (multi-value) }; +/* Один listen-сокет = секция [server:name]. */ +struct ConfigServer { + QString name; // суффикс секции: "lan", "lan6" + QString addr; // "0.0.0.0:1333" | "[::]:1333" + QString transport; // "udp" | "tcp" + QString type; // "public" | "nat" | "private" | "local" + QString netif; // "" = не привязывать к интерфейсу +}; + // Batch update — for callers that read/write the file once (e.g. NodeConfig::save) bool config_update_in_lines(QStringList& lines, const QString& section, const QString& key, const QString& value, bool replace); @@ -19,4 +29,8 @@ bool config_update_option(const QString& filePath, const QString& section, const QString& param, const QString& value, int flags = CONFIG_UPDATE_REPLACE); +// Полностью заменяет секции [server:*]: удаляет старые, вставляет новые после [global]. +// Внутри секции без пустых строк, между секциями ровно одна пустая строка. +bool config_rewrite_servers(const QString& filePath, const QList& servers); + #endif // CONFIG_UPDATER_H diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 0ed47db6..87635566 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -178,10 +178,8 @@ bool NodeConfig::create(const QString& nodeName, quint16 listenPort) { m_controlAddr = "127.0.0.1:9999"; m_servers.clear(); m_clients.clear(); - if (listenPort > 0) { - m_servers.append({"lan", QString("0.0.0.0:%1").arg(listenPort)}); - m_servers.append({"lan6", QString("[::]:%1").arg(listenPort)}); - } + (void)listenPort; /* автосокеты сами создают UDP+TCP на случайных портах */ + m_autoSockets = true; if (m_dbPath.isEmpty()) m_dbPath = "chat_data"; return save(); @@ -277,6 +275,7 @@ bool NodeConfig::load() { else if (key == "my_node_id") m_nodeId = val; else if (key == "my_public_key") m_pubKey = val; else if (key == "my_private_key") m_privKey = val; + else if (key == "auto_sockets") m_autoSockets = (val.compare("yes", Qt::CaseInsensitive) == 0 || val == "1"); } else if (section.startsWith("server:")) { QString srvName = section.mid(7); if (key == "addr") m_servers.append({srvName, val}); @@ -328,6 +327,7 @@ bool NodeConfig::saveFull() { if (!m_nodeId.isEmpty()) out << "my_node_id=" << m_nodeId << "\n"; if (!m_privKey.isEmpty()) out << "my_private_key=" << m_privKey << "\n"; if (!m_pubKey.isEmpty()) out << "my_public_key=" << m_pubKey << "\n"; + out << "auto_sockets=" << (m_autoSockets ? "yes" : "no") << "\n"; out << "\n"; for (auto& srv : m_servers) { @@ -403,6 +403,7 @@ bool NodeConfig::save() { config_update_in_lines(lines, "global", "my_node_id", m_nodeId, true); config_update_in_lines(lines, "global", "my_private_key", m_privKey, true); config_update_in_lines(lines, "global", "my_public_key", m_pubKey, true); + config_update_in_lines(lines, "global", "auto_sockets", m_autoSockets ? "yes" : "no", true); if (!m_controlAddr.isEmpty()) { int sep = m_controlAddr.lastIndexOf(':'); diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index a4f34123..7ca5256c 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -40,6 +40,9 @@ public: quint16 controlPort() const; void setControlPort(quint16 p); + bool autoSockets() const { return m_autoSockets; } + void setAutoSockets(bool v) { m_autoSockets = v; } + QString filePath() const { return m_path; } /* GUI settings (from [gui] section) */ @@ -65,6 +68,7 @@ private: QList> m_servers; QList m_clients; QString m_controlAddr; + bool m_autoSockets = true; QString m_dbPath; QString m_debugFile; QString m_debugLevel;