From 791688121a5e58dfa2984abf283d32709146cc7f Mon Sep 17 00:00:00 2001 From: donghualin Date: Mon, 10 Aug 2026 10:27:15 +0800 Subject: [PATCH] fix: optimize network switching detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When network connectivity is lost, automatically switch to another NIC that has internet access. Previous approach: After detecting network unreachable, sequentially switch the primary link to the next NIC, then check if the switched NIC has connectivity. If still unreachable, continue switching. This causes NICs to keep switching back and forth. Improved approach: After detecting network unreachable, individually check each alternative NIC to see if it can access the internet. Only switch the primary link to the target NIC after confirming it is online; otherwise, do not switch. fix: 优化网络切换检测功能 在检测到网络不通的情况下,自动切换到其他可以上网的网卡 之前方案:在检测到网络不通后,依次将主链接切换到下一个网卡,再判断切换后的网卡网络是否通,如果还是不通,继续切换,这样就会导致网卡在不停切换 修改方案:在检测到网络不通后,依次针对其他的网卡单独检测这个网卡是否可以上网,只有在检测到可以上网后,才将主链接切换到当前网卡,否则不予切换 Log: 优化网卡自动切换功能 Influence: 切换主链接 Bug: https://pms.uniontech.com/bug-view-372223.html --- .../src/system/internetchecker.cpp | 890 +++++++++++++----- .../src/system/internetchecker.h | 33 +- 2 files changed, 688 insertions(+), 235 deletions(-) mode change 100644 => 100755 network-service-plugin/src/system/internetchecker.cpp mode change 100644 => 100755 network-service-plugin/src/system/internetchecker.h diff --git a/network-service-plugin/src/system/internetchecker.cpp b/network-service-plugin/src/system/internetchecker.cpp old mode 100644 new mode 100755 index 442d50d28..33422d37a --- a/network-service-plugin/src/system/internetchecker.cpp +++ b/network-service-plugin/src/system/internetchecker.cpp @@ -4,313 +4,771 @@ #include "internetchecker.h" -#include "httpmanager.h" #include "settingconfig.h" #include "constants.h" #include -#include #include -#include +#include +#include -#include #include #include #include #include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + using namespace network::systemservice; InternetChecker::InternetChecker(QObject *parent) : QObject(parent) - , m_tryIndex(0) - , m_switchTimer(nullptr) - , m_isSwitching(false) { - connect(NetworkManager::notifier(), &NetworkManager::Notifier::primaryConnectionChanged, - this, &InternetChecker::onPrimaryConnectionChanged); } -InternetChecker::~InternetChecker() +// 网络切换入口:检测到网络不通时,依次检查其他网卡是否能上网, +// 仅在确认目标网卡可上网后才切换主链接,避免网卡反复切换 +void InternetChecker::switchInternetAccess(bool checkPrimaryConnection) { - if (m_switchTimer) { - m_switchTimer->stop(); - m_switchTimer->deleteLater(); + // 获取当前主连接对应的网卡 uni 列表 + QStringList primaryDeviceUnis; + NetworkManager::ActiveConnection::Ptr primaryConnection = NetworkManager::primaryConnection(); + if (!primaryConnection.isNull()) { + primaryDeviceUnis = primaryConnection->devices(); + } + + // 遍历所有网卡,分离出主连接网卡和候选网卡(有线优先于无线) + NetworkManager::Device::Ptr primaryDevice; + NetworkManager::Device::List wiredDevice, wirelessDevice; + NetworkManager::Device::List devices = NetworkManager::networkInterfaces(); + for (const NetworkManager::Device::Ptr &device : devices) { + if (primaryDeviceUnis.contains(device->uni())) { + primaryDevice = device; + continue; + } + + // 跳过未托管或未启动的网卡 + if (!device->managed() || !(device->interfaceFlags() & IFF_UP)) { + continue; + } + + // 跳过没有活跃连接的网卡 + if (device->activeConnection().isNull()) { + continue; + } + + if (device->type() == NetworkManager::Device::Type::Ethernet) { + wiredDevice << device; + } else if (device->type() == NetworkManager::Device::Type::Wifi) { + wirelessDevice << device; + } } + // 有线网卡优先,无线网卡次之 + NetworkManager::Device::List checkedDevices; + checkedDevices << wiredDevice << wirelessDevice; + // 如果没有需要切换的网络,则无需继续切换 + if (checkedDevices.isEmpty()) { + emit switchFailed(); + return; + } + + if (checkPrimaryConnection && !primaryDevice.isNull()) { + // 如果需要检查主链接,且此时主链接可以上网,则无需切换,直接告诉外面当前网络状况正常 + if (checkInterfaceOnline(primaryDevice)) { + qCInfo(DSM) << "primary device " << primaryDevice->interfaceName() << " is online"; + emit switchSuccess(); + return; + } + } + // 依次检测候选网卡,找到第一个能上网的就切换过去 + for (const NetworkManager::Device::Ptr &device : checkedDevices) { + if (checkInterfaceOnline(device) && setPrimaryDevice(device, devices)) { + qCInfo(DSM) << device->interfaceName() << " is online, set it primary device"; + emit switchSuccess(); + return; + } + } + qCWarning(DSM) << "switch primary device failure"; + emit switchFailed(); } -void InternetChecker::resetAllNeverDefault() const +// 获取网卡当前活跃连接的 IPv4 DNS 服务器列表 +QStringList InternetChecker::getDeviceDnsList(const NetworkManager::Device::Ptr &device) const { - const NetworkManager::Device::List devices = NetworkManager::networkInterfaces(); - for (const NetworkManager::Device::Ptr &device : devices) { - NetworkManager::ActiveConnection::Ptr activeConn = device->activeConnection(); - if (!activeConn.isNull()) - setConnectionNeverDefault(activeConn->connection(), device, false); + NetworkManager::ActiveConnection::Ptr activeConnection = device->activeConnection(); + if (activeConnection.isNull()) { + return {}; + } + + QStringList dnsList; + NetworkManager::IpConfig config = activeConnection->ipV4Config(); + QList addresses = config.nameservers(); + for (const QHostAddress &address : addresses) { + QString ip = address.toString(); + if (!ip.isEmpty() && !dnsList.contains(ip)) { + dnsList << ip; + } } + + return dnsList; } -bool InternetChecker::setConnectionNeverDefault(const NetworkManager::Connection::Ptr &conn, - const NetworkManager::Device::Ptr &device, - bool neverDefault) const +// 检测指定网卡是否可以上网: +// 1. 依次尝试配置的检测 URL(IP 地址直接 TCP 连接,域名则先 DNS 解析再连接) +// 2. 如果全部失败,使用公共 DNS 地址作为兜底 TCP 连接检测 +bool InternetChecker::checkInterfaceOnline(const NetworkManager::Device::Ptr &device) const { - if (conn.isNull()) + QStringList dnsList = getDeviceDnsList(device); + if (dnsList.isEmpty()) { + qCWarning(DSM) << "interface " << device->interfaceName() << " doesn't have dns"; return false; + } - NMVariantMapMap settingsMap; - bool changed = false; + const int totalTimeout = SettingConfig::instance()->httpRequestTimeout() * 1000; + QElapsedTimer totalElapsed; + totalElapsed.start(); - NetworkManager::Setting::Ptr ipv4Setting = conn->settings()->setting(NetworkManager::Setting::Ipv4); - if (ipv4Setting) { - NetworkManager::Ipv4Setting::Ptr ipv4 = ipv4Setting.dynamicCast(); - if (ipv4->neverDefault() != neverDefault) { - ipv4->setNeverDefault(neverDefault); - settingsMap.insert(ipv4->name(), ipv4->toMap()); - changed = true; + QStringList remoteAddrs; + QStringList networkUrls = SettingConfig::instance()->networkCheckerUrls(); + int perUrlTimeout = qMax(totalTimeout / qMax(networkUrls.size(), 1), 1000); + for (const QString &url : networkUrls) { + int remainTimeout = totalTimeout - static_cast(totalElapsed.elapsed()); + if (remainTimeout <= 0) + break; + + int curTimeout = qMin(perUrlTimeout, remainTimeout); + QString host = url; + static QStringList schemePrefixes = {"https://", "http://"}; + for (const QString &prefix : schemePrefixes) { + if (host.startsWith(prefix)) { + host = host.remove(0, prefix.length()); + break; + } } - } - NetworkManager::Setting::Ptr ipv6Setting = conn->settings()->setting(NetworkManager::Setting::Ipv6); - if (ipv6Setting) { - NetworkManager::Ipv6Setting::Ptr ipv6 = ipv6Setting.dynamicCast(); - if (ipv6->neverDefault() != neverDefault) { - ipv6->setNeverDefault(neverDefault); - settingsMap.insert(ipv6->name(), ipv6->toMap()); - changed = true; + if (host.endsWith("/")) { + host = host.left(host.length() - 1); + } + in_addr addr; + if (inet_pton(AF_INET, host.toStdString().c_str(), &addr) == 1) { + QString remoteIp = QString::fromLatin1(inet_ntoa(addr)); + if (remoteAddrs.contains(remoteIp)) + continue; + + remoteAddrs << remoteIp; + sockaddr_in target {}; + memset(&target, 0, sizeof(target)); + target.sin_family = AF_INET; + target.sin_port = htons(80); + target.sin_addr = addr; + if (isIfaceReachable(device->interfaceName(), target, curTimeout)) { + qDebug(DSM) << "interface " << device->interfaceName() << " test ip " << url << " ok"; + return true; + } + } else if (checkNetCardOnline(device, host, dnsList, curTimeout)) { + // 如果是域名,先通过指定网卡的 DNS 解析域名,再 TCP 连接解析出的 IP + qDebug(DSM) << "interface " << device->interfaceName() << " test url " << url << " ok"; + return true; } } - if (changed) { - conn->updateUnsaved(settingsMap); - if (!device.isNull()) { - device->reapplyConnection(conn->settings()->toMap(), 0, SettingConfig::instance()->reapplyFlags()); + // 兜底方案:直接 TCP 连接公共 DNS 的 80 端口,检测网卡是否有基本出口连通性(在一些网络结构中例如手机热点,可能屏蔽了53端口,通过DNS获取IP失败,这里就使用兜底方案) + static QStringList fallbackDnsList {"223.5.5.5", "223.6.6.6", "114.114.114.114", "8.8.8.8"}; + for (const QString &dns : fallbackDnsList) { + int remainTimeout = totalTimeout - static_cast(totalElapsed.elapsed()); + if (remainTimeout <= 0) + break; + + if (remoteAddrs.contains(dns)) + continue; + + int curTimeout = qMin(1000, remainTimeout); + sockaddr_in target {}; + memset(&target, 0, sizeof(target)); + target.sin_family = AF_INET; + target.sin_port = htons(80); + inet_pton(AF_INET, dns.toStdString().c_str(), &target.sin_addr); + if (isIfaceReachable(device->interfaceName(), target, curTimeout)) { + qCDebug(DSM) << "interface " << device->interfaceName() << "test dns " << dns << " ok"; + return true; } - qCInfo(DSM) << "Set connection" << conn->name() << "never-default to" << neverDefault; } - return changed; + qCDebug(DSM) << "interface " << device->interfaceName() << " is offline"; + return false; } -void InternetChecker::setPrimaryDeviceNeverDefault(bool neverDefault) const +// 通过非阻塞 TCP 连接检测从指定网卡是否可以到达目标地址 +// 使用 SO_BINDTODEVICE 绑定网卡,确保流量从该网卡发出 +bool InternetChecker::isIfaceReachable(const QString &ifName, const sockaddr_in &dest, int timeoutMs) const { - NetworkManager::ActiveConnection::Ptr primaryConn = NetworkManager::primaryConnection(); - if (primaryConn.isNull()) - return; + int sockFd = socket(AF_INET, SOCK_STREAM, 0); + if (sockFd < 0) + return false; - const QStringList deviceUnis = primaryConn->devices(); - for (const QString &uni : deviceUnis) { - NetworkManager::Device::Ptr device = NetworkManager::findNetworkInterface(uni); - if (!device.isNull()) - changeDeviceNeverDefault(device, neverDefault); + // 绑定网卡设备,所有流量从该网卡发出 + struct ifreq ifr{}; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifName.toStdString().c_str(), IFNAMSIZ - 1); + if (setsockopt(sockFd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) { + close(sockFd); + return false; } + + // 设为非阻塞,用 select 控制连接超时 + int flags = fcntl(sockFd, F_GETFL, 0); + fcntl(sockFd, F_SETFL, flags | O_NONBLOCK); + + int connectRet = ::connect(sockFd, (sockaddr*)&dest, sizeof(dest)); + if (connectRet < 0 && errno != EINPROGRESS) { + close(sockFd); + return false; + } + + // 立即连接成功的情况 + if (connectRet == 0) { + close(sockFd); + return true; + } + + // 等待连接完成(非阻塞模式下 EINPROGRESS 需要通过 select 检测可写事件) + fd_set wfds{}; + FD_ZERO(&wfds); + FD_SET(sockFd, &wfds); + timeval tv{}; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; + int selRet = select(sockFd + 1, nullptr, &wfds, nullptr, &tv); + if (selRet > 0) { + // 检查连接是否真正成功(select 可写不代表连接成功,需通过 SO_ERROR 确认) + int err = 0; + socklen_t errLen = sizeof(err); + getsockopt(sockFd, SOL_SOCKET, SO_ERROR, &err, &errLen); + close(sockFd); + return (err == 0); + } + close(sockFd); + return false; } -bool InternetChecker::checkInternetAccessible() const +// 检测网卡是否可以访问指定域名:先通过该网卡绑定的 DNS 解析域名得到 IP, +// 再通过该网卡 TCP 连接解析出的 IP 地址的 80 端口 +bool InternetChecker::checkNetCardOnline(const NetworkManager::Device::Ptr &device, const QString &domain, const QStringList &dnslist, int timeoutMs) const { - bool timedOut = false; - return checkInternetAccessible(0, timedOut); + in_addr targetIp{}; + QElapsedTimer elapsed; + elapsed.start(); + int halfTimeout = timeoutMs / 2; + if (!resolveByBindIface(device, domain, dnslist, targetIp, halfTimeout)) + return false; + + int remainTimeout = timeoutMs - static_cast(elapsed.elapsed()); + if (remainTimeout <= 0) + return false; + + sockaddr_in dest{}; + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(80); + dest.sin_addr = targetIp; + + return isIfaceReachable(device->interfaceName(), dest, remainTimeout); } -bool InternetChecker::checkInternetAccessible(int timeoutSec, bool &timedOut) const +// 绑定指定网卡进行 DNS 解析,按优先级依次尝试三种方式: +// 1. UDP DNS 查询(常规方式) +// 2. TCP DNS 查询(UDP 53 端口被屏蔽时的 fallback,如手机热点场景) +// 3. 系统 getaddrinfo(兜底方案,使用系统默认路由解析) +bool InternetChecker::resolveByBindIface(const NetworkManager::Device::Ptr &device, const QString &domain, const QStringList &dnslist, in_addr &outIp, int timeout) const { - timedOut = false; - - const QStringList checkUrls = SettingConfig::instance()->networkCheckerUrls(); - for (const QString &url : checkUrls) { - network::service::HttpManager http; - network::service::HttpReply *httpReply = (timeoutSec > 0) - ? http.get(url, timeoutSec) - : http.get(url); - if (httpReply->httpCode() != 0) { + // 将总超时平均分配给每个 DNS 服务器,每个至少 1 秒 + int perDnsTimeout = qMax(timeout / qMax(dnslist.size(), 1), 1000); + int remainTimeout = timeout; + QElapsedTimer elapsed; + elapsed.start(); + + // 第一步:通过 UDP 方式向各 DNS 服务器发送 A 记录查询 + for (const QString &dnsIp : dnslist) { + if (remainTimeout <= 0) + break; + + int curTimeout = qMin(perDnsTimeout, remainTimeout); + if (checkIpAddrByUDP(device, dnsIp, domain, curTimeout, outIp)) { + qCDebug(DSM) << "check dns success by step 1, elapse time: " << elapsed.elapsed() << "seconds"; return true; } - if (httpReply->isTimeout()) { - timedOut = true; + remainTimeout -= elapsed.elapsed(); + elapsed.restart(); + } + + // 第二步:如果 UDP 方式全部失败,改用 TCP 方式重试。 + // 适用场景:连接手机热点时,运营商可能屏蔽 UDP 53 端口,但 TCP 53 端口可用 + for (const QString &dnsIp : dnslist) { + if (remainTimeout <= 0) break; + + int curTimeout = qMin(perDnsTimeout, remainTimeout); + if (checkIpAddrByTCP(device, dnsIp, domain, curTimeout, outIp)) { + qCDebug(DSM) << "check dns success by step 2, elapse time: " << elapsed.elapsed() << "seconds"; + return true; } + remainTimeout -= elapsed.elapsed(); + elapsed.restart(); } + + // 第三步:兜底方案,使用系统 getaddrinfo 解析(走系统默认路由,不绑定网卡) + struct addrinfo hints{}, *res = nullptr; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + int gaiRet = getaddrinfo(domain.toStdString().c_str(), nullptr, &hints, &res); + if (gaiRet == 0 && res != nullptr) { + for (struct addrinfo *p = res; p != nullptr; p = p->ai_next) { + if (p->ai_family == AF_INET) { + outIp = ((struct sockaddr_in *)p->ai_addr)->sin_addr; + freeaddrinfo(res); + qCDebug(DSM) << "search dns success by step 3, elapse time: " << elapsed.elapsed() << "seconds"; + return true; + } + } + freeaddrinfo(res); + } + return false; } -bool InternetChecker::checkInternetAccessibleWithRetry(int maxRetry) const +// 构造标准 DNS A 记录查询包(RFC 1035 格式) +// 返回写入包的字节数,出错返回 -1(域名标签超过 63 字节或包超过 pktSize) +int InternetChecker::buildDnsQueryPacket(const QString &domain, unsigned char *pkt, int pktSize, unsigned short txId) +{ + memset(pkt, 0, pktSize); + int needed = 12; // DNS 头部固定 12 字节 + + // 计算问段域名编码后所需的长度:每个标签为 1 字节长度 + 标签内容 + QStringList seg = domain.split("."); + for (const auto &s : seg) { + QByteArray buf = s.toUtf8(); + if (buf.size() > 63) // RFC 1035: 单个标签不超过 63 字节 + return -1; + needed += 1 + buf.size(); + } + needed += 1 + 4; // 域名结尾 null 字节 + QTYPE(2) + QCLASS(2) + + if (needed > pktSize) + return -1; + + // 填充 DNS 头部:ID、标准查询标志、QDCOUNT=1 + unsigned short id = htons(txId); + memcpy(pkt, &id, 2); + pkt[2] = 0x01; pkt[3] = 0x00; // Flags: RD=1, 其余为 0(标准查询) + unsigned short qd = htons(1); + memcpy(pkt + 4, &qd, 2); + + // 填充问段:将域名按 "." 分割,每段前加长度字节,以 null 字节结尾 + int ptr = 12; + for (const auto &s : seg) { + QByteArray buf = s.toUtf8(); + pkt[ptr++] = static_cast(buf.size()); + memcpy(pkt + ptr, buf.data(), buf.size()); + ptr += buf.size(); + } + pkt[ptr++] = 0; // 域名结束标记 + // QTYPE=A(1), QCLASS=IN(1) + unsigned short typeA = htons(1), cls = htons(1); + memcpy(pkt + ptr, &typeA, 2); ptr += 2; + memcpy(pkt + ptr, &cls, 2); ptr += 2; + + return ptr; +} + +// 解析 DNS 响应包,在 Answer Section 中查找 A 记录(TYPE=1), +// 找到后将 IPv4 地址写入 outIp 并返回 true +// 跳过非 A 记录(如 CNAME 等),遇到解析异常时跳过当前记录继续尝试,而非直接失败 +bool InternetChecker::parseDnsResponse(const unsigned char *buf, int len, unsigned short txId, in_addr &outIp) { - int httpTimeout = SettingConfig::instance()->httpRequestTimeout(); - for (int i = 0; i < maxRetry; ++i) { - // 每次检测前等待1秒,让NM路由表和DHCP先稳定 - { - QEventLoop loop; - QTimer::singleShot(1000, &loop, &QEventLoop::quit); - loop.exec(); + if (len <= 12) + return false; + + // 校验响应 ID 和 QR 标志位(bit 15: 1=响应) + unsigned short respId; + memcpy(&respId, buf, 2); + respId = ntohs(respId); + if (respId != txId || !(buf[2] & 0x80)) + return false; + + int p = 12; // 跳过 12 字节 DNS 头部 + unsigned short qnum; + memcpy(&qnum, buf + 4, 2); + qnum = ntohs(qnum); + + unsigned short anNum; + memcpy(&anNum, buf + 6, 2); + anNum = ntohs(anNum); + + // 跳过 Question Section:每个问题包含域名 + QTYPE(2) + QCLASS(2) + for (int i = 0; i < qnum; i++) { + bool compressed = false; + // 跳过域名:逐标签跳过,遇到压缩指针(高两 bit 为 11)则跳过 2 字节 + while (p < len && buf[p] != 0) { + if ((buf[p] & 0xC0) == 0xC0) { + if (p + 2 > len) + break; + p += 2; + compressed = true; + break; + } + p += buf[p] + 1; } - bool timedOut = false; - // 首次使用20秒超时,避免在无网线路由器的环境中长时间阻塞 - QElapsedTimer timer; - timer.start(); - if (checkInternetAccessible((i == 0) ? httpTimeout : 0, timedOut)) { - return true; + // 压缩指针后紧跟 TYPE 字段,没有 null 终止符,所以只在非压缩时跳过 null + if (!compressed && p < len && buf[p] == 0) + p++; + if (p + 4 > len) + break; + p += 4; // 跳过 QTYPE(2) + QCLASS(2) + } + + // 遍历 Answer Section,查找 A 记录 + for (int i = 0; i < anNum && p + 12 <= len; i++) { + bool compressed = false; + // 跳过 Answer 中的域名(可能使用压缩指针指向 Question 中的域名) + while (p < len && buf[p] != 0) { + if ((buf[p] & 0xC0) == 0xC0) { + if (p + 2 > len) + break; + p += 2; + compressed = true; + break; + } + p += buf[p] + 1; } - if (timedOut) { - qCDebug(DSM) << "Request timed out after" << timer.elapsed() << "ms, skipping retries"; - return false; + if (!compressed && p < len && buf[p] == 0) + p++; + // 剩余至少需要 10 字节:TYPE(2) + CLASS(2) + TTL(4) + RDLENGTH(2) + if (p + 10 > len) + break; + + unsigned short rtype; + memcpy(&rtype, buf + p, 2); + rtype = ntohs(rtype); + p += 8; // 跳过 TYPE(2) + CLASS(2) + TTL(4) + + unsigned short rdLen; + memcpy(&rdLen, buf + p, 2); + rdLen = ntohs(rdLen); + p += 2; // 跳过 RDLENGTH(2) + + // 找到 A 记录:TYPE=1,RDATA 长度固定 4 字节(IPv4 地址) + if (rtype == 1 && rdLen == 4 && p + 4 <= len) { + memcpy(&outIp, buf + p, 4); + return true; } + // 非 A 记录(如 CNAME、TXT 等),根据 RDLENGTH 跳过 RDATA + if (p + rdLen > len) + break; + p += rdLen; } + return false; } -void InternetChecker::changeDeviceNeverDefault(const NetworkManager::Device::Ptr &device, bool neverDefault) const +// 通过 UDP 向指定 DNS 服务器发送 A 记录查询,绑定网卡确保流量从该网卡发出 +bool InternetChecker::checkIpAddrByUDP(const NetworkManager::Device::Ptr &device, const QString &dnsIp, const QString &domain, + int curTimeout, in_addr &outIp) const { - if (device.isNull()) - return; + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + return false; - NetworkManager::ActiveConnection::Ptr activeConn = device->activeConnection(); - if (activeConn.isNull()) - return; + // 绑定网卡设备 + struct ifreq ifr{}; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, device->interfaceName().toStdString().c_str(), IFNAMSIZ - 1); + if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) { + close(sock); + return false; + } - NetworkManager::Connection::Ptr conn = activeConn->connection(); - setConnectionNeverDefault(conn, device, neverDefault); -} + // 使用网卡自身的 IPv4 地址绑定源地址,确保从该网卡发出 + bool bound = false; + NetworkManager::IpAddresses ipv4AddrList = device->ipV4Config().addresses(); + for (const NetworkManager::IpAddress &ipAddr : ipv4AddrList) { + sockaddr_in localAddr{}; + memset(&localAddr, 0, sizeof(localAddr)); + localAddr.sin_family = AF_INET; + localAddr.sin_port = 0; + inet_pton(AF_INET, ipAddr.ip().toString().toUtf8().constData(), &localAddr.sin_addr); + if (::bind(sock, (sockaddr *)&localAddr, sizeof(localAddr)) == 0) { + bound = true; + break; + } + } -void InternetChecker::onPrimaryConnectionChanged(const QString &uni) -{ - Q_UNUSED(uni) - - // 停止超时定时器 - if (m_switchTimer) { - m_switchTimer->stop(); - } - - qCInfo(DSM) << "Primary connection changed:" << uni; - // NM已切换主连接,通过默认路由检测整体是否可以上网(重试3次,等待DHCP/路由生效) - if (checkInternetAccessibleWithRetry(3)) { - qCInfo(DSM) << "Found accessible device, switching done"; - // 当前主设备已经是能上网的,它的never-default本来就是false,无需额外操作 - // 此处不能恢复其他设备never default, 因为如果恢复后,NM 内部会根据每个设备的metric值又将主链接设置回去了,导致无法上网 - m_tryIndex = 0; - m_isSwitching = false; - emit switchSuccess(); - } else if (m_tryIndex < m_tryDevices.size()) { - // 当前设备不能上网,把当前主设备的never-default设为true,让下一个设备接住默认路由 - qCDebug(DSM) << "Current device still cannot access internet, trying next device, index:" << m_tryIndex; - setPrimaryDeviceNeverDefault(true); - NetworkManager::Device::Ptr nextDevice = m_tryDevices.at(m_tryIndex); - changeDeviceNeverDefault(nextDevice, false); - m_tryIndex++; - } else { - // 所有设备都尝试过了,全部恢复 - qCInfo(DSM) << "All devices tried, none can access internet, resetting all never-default"; - resetAllNeverDefault(); - m_tryIndex = 0; - m_isSwitching = false; - emit switchFailed(); + if (!bound) { + close(sock); + return false; } -} -void InternetChecker::onPrimaryConnectionTimeout() -{ - // NM在超时时间内没有切换主连接,可能是只有一个可切换的设备 - qCWarning(DSM) << "PrimaryConnection change timeout, trying next device manually, index:" << m_tryIndex; - if (m_tryIndex < m_tryDevices.size()) { - // 把当前主设备的never-default设为true,让下一个设备接住默认路由 - setPrimaryDeviceNeverDefault(true); - NetworkManager::Device::Ptr nextDevice = m_tryDevices.at(m_tryIndex); - changeDeviceNeverDefault(nextDevice, false); - m_tryIndex++; - // 重置超时定时器 - m_switchTimer->start(5000); - } else { - resetAllNeverDefault(); - m_tryIndex = 0; - m_isSwitching = false; - emit switchFailed(); + // 构造 DNS 服务器地址(端口 53) + sockaddr_in dnsAddr; + memset(&dnsAddr, 0, sizeof(dnsAddr)); + dnsAddr.sin_family = AF_INET; + dnsAddr.sin_port = htons(53); + if (inet_pton(AF_INET, dnsIp.toStdString().c_str(), &dnsAddr.sin_addr) <= 0) { + close(sock); + return false; } + + // 构造并发送 DNS 查询包 + unsigned short txId = static_cast(QRandomGenerator::global()->generate() % 65536); + unsigned char pkt[512]; + int pktLen = buildDnsQueryPacket(domain, pkt, sizeof(pkt), txId); + if (pktLen < 0) { + qCWarning(DSM) << "DNS query packet too large for domain:" << domain; + close(sock); + return false; + } + + ssize_t sent = sendto(sock, pkt, pktLen, MSG_NOSIGNAL, (sockaddr*)&dnsAddr, sizeof(dnsAddr)); + if (sent < 0) { + qCWarning(DSM) << "sendto failed for" << device->interfaceName() << "dns:" << dnsIp << "errno:" << errno << strerror(errno); + close(sock); + return false; + } + + // 等待响应并用 select 控制超时 + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + timeval tv; + tv.tv_sec = curTimeout / 1000; + tv.tv_usec = (curTimeout % 1000) * 1000; + + bool succ = false; + if (select(sock + 1, &rfds, nullptr, nullptr, &tv) > 0) { + unsigned char respBuf[512]; + sockaddr_in fromAddr{}; + socklen_t fromLen = sizeof(fromAddr); + ssize_t rlen = recvfrom(sock, reinterpret_cast(respBuf), sizeof(respBuf), 0, + (sockaddr *)&fromAddr, &fromLen); + if (rlen > 12 + && fromAddr.sin_family == AF_INET + && fromAddr.sin_addr.s_addr == dnsAddr.sin_addr.s_addr + && fromAddr.sin_port == dnsAddr.sin_port) { + succ = parseDnsResponse(respBuf, static_cast(rlen), txId, outIp); + } else if (rlen > 0) { + qCWarning(DSM) << "DNS response from unexpected source, discarded:" << inet_ntoa(fromAddr.sin_addr); + } + } + + close(sock); + return succ; } -void InternetChecker::switchInternetAccess(bool checkPrimaryConnection) +// 通过 TCP 向指定 DNS 服务器发送 A 记录查询(DNS over TCP,RFC 7766) +// TCP DNS 消息前有 2 字节长度前缀,与 UDP 格式不同 +// 适用场景:手机热点等环境下 UDP 53 端口可能被屏蔽,但 TCP 53 端口可用 +bool InternetChecker::checkIpAddrByTCP(const NetworkManager::Device::Ptr &device, const QString &dnsIp, const QString &domain, + int curTimeout, in_addr &outIp) const { - // 正在切换中,忽略新的切换请求 - if (m_isSwitching) { - qCDebug(DSM) << "current is switching"; - return; + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) + return false; + + // 绑定网卡设备 + struct ifreq ifr{}; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, device->interfaceName().toStdString().c_str(), IFNAMSIZ - 1); + if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) { + close(sock); + return false; } - qCDebug(DSM) << "need check primary connection" << checkPrimaryConnection; - // 主连接在检测期间发生变化,需要重新检查当前主连接是否可上网 - NetworkManager::ActiveConnection::Ptr primaryConnection = NetworkManager::primaryConnection(); - if (checkPrimaryConnection && !primaryConnection.isNull()) { - int httpTimeout = SettingConfig::instance()->httpRequestTimeout(); - bool isTimeout = false; - if (checkInternetAccessible(httpTimeout, isTimeout)) { - qCInfo(DSM) << "Primary connection is accessible, skip switching"; - emit switchSuccess(); - return; + // 使用网卡自身的 IPv4 地址绑定源地址 + bool bound = false; + NetworkManager::IpAddresses ipv4AddrList = device->ipV4Config().addresses(); + for (const NetworkManager::IpAddress &ipAddr : ipv4AddrList) { + sockaddr_in localAddr{}; + memset(&localAddr, 0, sizeof(localAddr)); + localAddr.sin_family = AF_INET; + localAddr.sin_port = 0; + inet_pton(AF_INET, ipAddr.ip().toString().toUtf8().constData(), &localAddr.sin_addr); + if (::bind(sock, (sockaddr *)&localAddr, sizeof(localAddr)) == 0) { + bound = true; + break; } } - m_isSwitching = true; - NetworkManager::Device::List availableDevice; - NetworkManager::Device::List devices = NetworkManager::networkInterfaces(); - for (const NetworkManager::Device::Ptr &device : devices) { - if (device->type() != NetworkManager::Device::Type::Ethernet - && device->type() != NetworkManager::Device::Type::Wifi) - continue; - if (device->state() != NetworkManager::Device::State::Activated) - continue; - - availableDevice << device; + if (!bound) { + close(sock); + return false; } - if (availableDevice.size() <= 1) { - qCDebug(DSM) << "only one available device,it will reset all never default"; - resetAllNeverDefault(); - m_isSwitching = false; - return; + // 构造 DNS 服务器地址(端口 53) + sockaddr_in dnsAddr; + memset(&dnsAddr, 0, sizeof(dnsAddr)); + dnsAddr.sin_family = AF_INET; + dnsAddr.sin_port = htons(53); + if (inet_pton(AF_INET, dnsIp.toStdString().c_str(), &dnsAddr.sin_addr) <= 0) { + close(sock); + return false; } - if (primaryConnection.isNull()) { - qCWarning(DSM) << "primary connection is null, it will reset all never default"; - resetAllNeverDefault(); - m_isSwitching = false; - return; + // 非阻塞连接 DNS 服务器,用 select 控制连接超时(最多 2 秒) + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + int connectRet = ::connect(sock, (sockaddr*)&dnsAddr, sizeof(dnsAddr)); + if (connectRet < 0 && errno != EINPROGRESS) { + close(sock); + return false; } - // 找到当前主连接使用的网卡 - QStringList primaryDeviceUnis = primaryConnection->devices(); - NetworkManager::Device::Ptr primaryDevice; - for (const NetworkManager::Device::Ptr &device : availableDevice) { - if (primaryDeviceUnis.contains(device->uni())) { - primaryDevice = device; - qWarning() << "get primary device: " << primaryDevice->interfaceName(); - break; + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sock, &wfds); + int connectTimeout = qMin(curTimeout, 2000); + timeval connectTv; + connectTv.tv_sec = connectTimeout / 1000; + connectTv.tv_usec = (connectTimeout % 1000) * 1000; + + bool succ = false; + int selRet = select(sock + 1, nullptr, &wfds, nullptr, &connectTv); + if (selRet > 0) { + int err = 0; + socklen_t errLen = sizeof(err); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &errLen); + if (err == 0) { + // 连接成功,构造 DNS 查询包并加上 TCP 2 字节长度前缀 + unsigned short txId = static_cast(QRandomGenerator::global()->generate() % 65536); + unsigned char pkt[512]; + int pktLen = buildDnsQueryPacket(domain, pkt, sizeof(pkt), txId); + if (pktLen < 0) { + qCWarning(DSM) << "DNS query packet too large for domain:" << domain; + close(sock); + return false; + } + + // TCP DNS 消息格式:2 字节大端长度前缀 + DNS 报文内容 + unsigned char tcpPkt[514]; + unsigned short pktLenH = htons(pktLen); + memcpy(tcpPkt, &pktLenH, 2); + memcpy(tcpPkt + 2, pkt, pktLen); + + ssize_t sent = send(sock, tcpPkt, pktLen + 2, MSG_NOSIGNAL); + if (sent == pktLen + 2) { + // 用 select 等待响应可读,然后在非阻塞模式下一次性读完缓冲区。 + // 保持非阻塞模式的原因:如果对端返回非 DNS 数据(如手机热点拦截页面), + // 非阻塞 recv 会立即返回可读数据或 EAGAIN,不会像阻塞 MSG_WAITALL 那样死等垃圾数据 + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(sock, &rfds); + timeval recvTv; + recvTv.tv_sec = curTimeout / 1000; + recvTv.tv_usec = (curTimeout % 1000) * 1000; + + if (select(sock + 1, &rfds, nullptr, nullptr, &recvTv) > 0) { + unsigned char respBuf[514]; + // 非阻塞循环读取,EAGAIN 时自然退出 + ssize_t totalRecv = 0; + while (totalRecv < 514) { + ssize_t n = recv(sock, reinterpret_cast(respBuf + totalRecv), 514 - totalRecv, 0); + if (n <= 0) + break; + totalRecv += n; + } + // 解析 TCP DNS 响应:前 2 字节为长度前缀,后面才是 DNS 报文 + int parseOff = 0; + int parseLen = totalRecv; + if (totalRecv > 2) { + unsigned short hdrLen; + memcpy(&hdrLen, respBuf, 2); + hdrLen = ntohs(hdrLen); + if (hdrLen > 12 && hdrLen < 4096) { + parseOff = 2; + // 取实际接收量和声明长度的较小值,防止越界 + parseLen = hdrLen < (totalRecv - 2) ? hdrLen : (totalRecv - 2); + } + } + if (parseLen > 12) { + succ = parseDnsResponse(respBuf + parseOff, parseLen, txId, outIp); + } + } + } } } - if (primaryDevice.isNull()) { - qWarning() << "primary device is null"; - resetAllNeverDefault(); - m_isSwitching = false; - return; - } + close(sock); + return succ; +} - // 保存需要遍历的设备列表(排除当前主设备) - m_tryDevices.clear(); - int primaryIdx = availableDevice.indexOf(primaryDevice); - for (int i = 1; i < availableDevice.size(); ++i) { - m_tryDevices << availableDevice.at((primaryIdx + i) % availableDevice.size()); +// 通过调整网卡路由 metric 值切换主链接: +// 目标网卡设置低 metric(优先级高),其余网卡设置高 metric(优先级低), +// 然后通过 D-Bus 调用 NetworkManager 的 Reapply 接口使配置生效 +bool InternetChecker::setPrimaryDevice(const NetworkManager::Device::Ptr &device, const NetworkManager::Device::List &allDevices) +{ + if (device->activeConnection().isNull() || device->activeConnection()->state() != NetworkManager::ActiveConnection::Activated) { + qCWarning(DSM) << "device " << device->interfaceName() << "is not active"; + return false; } - if (m_tryDevices.isEmpty()) { - qCWarning(DSM) << "try device is empty"; - resetAllNeverDefault(); - m_isSwitching = false; - return; + const quint32 primaryMetric = 100; + const quint32 secondaryMetric = 1000; + QMap deviceSettings; + for (const NetworkManager::Device::Ptr &dev : allDevices) { + // 跳过没有活跃连接的网卡 + if (dev->activeConnection().isNull()) + continue; + + auto ac = dev->activeConnection(); + if (!ac || ac->state() != NetworkManager::ActiveConnection::Activated) + continue; + + bool isTarget = (dev == device); + quint32 metric = isTarget ? primaryMetric : secondaryMetric; + + NetworkManager::Connection::Ptr conn = ac->connection(); + NetworkManager::ConnectionSettings::Ptr settings = conn->settings(); + + bool needReapply = false; + NetworkManager::Ipv4Setting::Ptr ipv4 = settings->setting(NetworkManager::Setting::Ipv4).staticCast(); + if (ipv4) { + ipv4->setRouteMetric(metric); + ipv4->setInitialized(true); + needReapply = true; + } + + NetworkManager::Ipv6Setting::Ptr ipv6 = settings->setting(NetworkManager::Setting::Ipv6).staticCast(); + if (ipv6) { + ipv6->setRouteMetric(metric); + ipv6->setInitialized(true); + needReapply = true; + } + + if (needReapply) { + conn->update(settings->toMap()); + deviceSettings[dev] = conn->settings()->toMap(); + } } - // 先将第一个候选设备的never-default恢复为false,确保它能接住默认路由 - changeDeviceNeverDefault(m_tryDevices.first(), false); - changeDeviceNeverDefault(primaryDevice, true); - m_tryIndex = 1; + if (deviceSettings.isEmpty()) { + qCWarning(DSM) << "can't found device for change route"; + return false; + } - // 设置超时定时器,防止NM没有触发信号时卡住 - if (!m_switchTimer) { - m_switchTimer = new QTimer(this); - m_switchTimer->setSingleShot(true); - connect(m_switchTimer, &QTimer::timeout, this, &InternetChecker::onPrimaryConnectionTimeout); + // 通过 D-Bus 调用 Reapply 使修改的路由 metric 生效 + // Reapply 签名: (a{sa{sv}}tu) — settings_map, version_id(0), flags(0) + for (auto it = deviceSettings.constBegin(); it != deviceSettings.constEnd(); ++it) { + const NetworkManager::Device::Ptr &device = it.key(); + const NMVariantMapMap &settingMap = it.value(); + device->reapplyConnection(settingMap, 0, SettingConfig::instance()->reapplyFlags()); } - m_switchTimer->start(5000); + + return true; } diff --git a/network-service-plugin/src/system/internetchecker.h b/network-service-plugin/src/system/internetchecker.h old mode 100644 new mode 100755 index d47abd965..6326c863a --- a/network-service-plugin/src/system/internetchecker.h +++ b/network-service-plugin/src/system/internetchecker.h @@ -9,8 +9,6 @@ #include #include -class QTimer; - namespace network { namespace systemservice { @@ -20,30 +18,27 @@ class InternetChecker : public QObject public: explicit InternetChecker(QObject *parent = nullptr); - ~InternetChecker() override; + ~InternetChecker() override = default; void switchInternetAccess(bool checkPrimaryConnection = false); signals: void switchSuccess(); void switchFailed(); -private slots: - void onPrimaryConnectionChanged(const QString &uni); - void onPrimaryConnectionTimeout(); - private: - void resetAllNeverDefault() const; - bool setConnectionNeverDefault(const NetworkManager::Connection::Ptr &conn, const NetworkManager::Device::Ptr &device, bool neverDefault) const; - void setPrimaryDeviceNeverDefault(bool neverDefault) const; - bool checkInternetAccessible() const; - bool checkInternetAccessible(int timeoutSec, bool &timedOut) const; - bool checkInternetAccessibleWithRetry(int maxRetry) const; - void changeDeviceNeverDefault(const NetworkManager::Device::Ptr &device, bool neverDefault) const; - - int m_tryIndex; - NetworkManager::Device::List m_tryDevices; - QTimer *m_switchTimer; - bool m_isSwitching; + QStringList getDeviceDnsList(const NetworkManager::Device::Ptr &device) const; + bool checkInterfaceOnline(const NetworkManager::Device::Ptr &device) const; + bool isIfaceReachable(const QString &ifName, const sockaddr_in &dest, int timeoutMs) const; + bool checkNetCardOnline(const NetworkManager::Device::Ptr &device, const QString &domain, const QStringList &dnslist, int timeoutMs) const; + bool resolveByBindIface(const NetworkManager::Device::Ptr &device, const QString &domain, const QStringList &dnslist, in_addr &outIp, int timeout) const; + bool checkIpAddrByUDP(const NetworkManager::Device::Ptr &device, const QString &dnsIp, const QString &domain, int curTimeout, in_addr &outIp) const; + bool checkIpAddrByTCP(const NetworkManager::Device::Ptr &device, const QString &dnsIp, const QString &domain, int curTimeout, in_addr &outIp) const; + bool setPrimaryDevice(const NetworkManager::Device::Ptr &device, const NetworkManager::Device::List &allDevices); + + // DNS辅助函数:构造查询包,返回写入的字节数;出错返回-1 + static int buildDnsQueryPacket(const QString &domain, unsigned char *pkt, int pktSize, unsigned short txId); + // DNS辅助函数:解析响应,成功返回true并填充outIp + static bool parseDnsResponse(const unsigned char *buf, int len, unsigned short txId, in_addr &outIp); }; }