// tunnelmanager_linux.cpp - Linux WireGuard tunnel implementation. // // Uses /dev/net/tun (IFF_TUN | IFF_NO_PI) as the virtual NIC and boringtun // for the WireGuard crypto. Requires CAP_NET_ADMIN (or root) so the process // can open /dev/net/tun and run `ip` commands to configure routing. // // Suggested capability grant: // sudo setcap cap_net_admin+ep /usr/bin/dragonmoonlight // // Packet format on Linux TUN (with IFF_NO_PI): raw IP, no protocol prefix. #include "tunnelmanager.h" #ifdef Q_OS_LINUX #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "boringtun_ffi.h" #include "wireguardconfig.h" namespace { // shell helper. Errors are swallowed because `ip route del` may fail if // the route isn't there, and that's fine. void shell(const std::string &cmd) { int rc = ::system(cmd.c_str()); (void)rc; } // private state struct LinuxTunnelPriv { int tunFd = -1; int udpFd = -1; int wakeR = -1; int wakeW = -1; wireguard_tunnel *wg = nullptr; char ifName[IFNAMSIZ] = {}; std::atomic running {false}; std::atomic gotFirstPkt {false}; std::thread thdTunToUdp; std::thread thdUdpToTun; std::thread thdTicker; WireGuardConfig cfg; QPointer owner; }; int openTun(char *nameOut) { int fd = ::open("/dev/net/tun", O_RDWR); if (fd < 0) return -1; ifreq ifr {}; ifr.ifr_flags = IFF_TUN | IFF_NO_PI; if (::ioctl(fd, TUNSETIFF, &ifr) < 0) { ::close(fd); return -1; } ::strncpy(nameOut, ifr.ifr_name, IFNAMSIZ - 1); return fd; } // I/O threads void tunToUdp(LinuxTunnelPriv *p) { sockaddr_in peer {}; peer.sin_family = AF_INET; ::inet_pton(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); peer.sin_port = htons(static_cast(p->cfg.endpointPort())); std::vector plain(65536), enc(65536); while (p->running.load()) { fd_set rfds; FD_ZERO(&rfds); FD_SET(p->tunFd, &rfds); FD_SET(p->wakeR, &rfds); int nfds = std::max(p->tunFd, p->wakeR) + 1; if (::select(nfds, &rfds, nullptr, nullptr, nullptr) < 0) break; if (FD_ISSET(p->wakeR, &rfds)) break; ssize_t n = ::read(p->tunFd, plain.data(), plain.size()); if (n <= 0) continue; size_t encLen = enc.size(); wireguard_result r = wireguard_write( p->wg, plain.data(), static_cast(n), enc.data(), &encLen); if (r.op == WRITE_TO_NETWORK && r.size > 0) { ::sendto(p->udpFd, enc.data(), r.size, 0, reinterpret_cast(&peer), sizeof(peer)); } } } void udpToTun(LinuxTunnelPriv *p) { sockaddr_in peer {}; peer.sin_family = AF_INET; ::inet_pton(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); peer.sin_port = htons(static_cast(p->cfg.endpointPort())); std::vector enc(65536), plain(65536); while (p->running.load()) { fd_set rfds; FD_ZERO(&rfds); FD_SET(p->udpFd, &rfds); FD_SET(p->wakeR, &rfds); int nfds = std::max(p->udpFd, p->wakeR) + 1; if (::select(nfds, &rfds, nullptr, nullptr, nullptr) < 0) break; if (FD_ISSET(p->wakeR, &rfds)) break; sockaddr_in from {}; socklen_t fromLen = sizeof(from); ssize_t n = ::recvfrom(p->udpFd, enc.data(), enc.size(), 0, reinterpret_cast(&from), &fromLen); if (n <= 0) continue; size_t plainLen = plain.size(); wireguard_result r = wireguard_read( p->wg, enc.data(), static_cast(n), plain.data(), &plainLen); if ((r.op == WRITE_TO_TUNNEL_IPV4 || r.op == WRITE_TO_TUNNEL_IPV6) && r.size > 0) { ::write(p->tunFd, plain.data(), r.size); if (!p->gotFirstPkt.exchange(true) && p->owner) { const QString localIP = p->cfg.localIP(); QPointer owner = p->owner; QMetaObject::invokeMethod(owner.data(), [owner, localIP]() { if (owner) emit owner->tunnelUp(localIP); }, Qt::QueuedConnection); } } else if (r.op == WRITE_TO_NETWORK && r.size > 0) { ::sendto(p->udpFd, plain.data(), r.size, 0, reinterpret_cast(&peer), sizeof(peer)); } } } void ticker(LinuxTunnelPriv *p) { sockaddr_in peer {}; peer.sin_family = AF_INET; ::inet_pton(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); peer.sin_port = htons(static_cast(p->cfg.endpointPort())); std::vector buf(512); fd_set rfds; while (p->running.load()) { timeval tv { 0, 100000 }; // 100 ms FD_ZERO(&rfds); FD_SET(p->wakeR, &rfds); ::select(p->wakeR + 1, &rfds, nullptr, nullptr, &tv); if (!p->running.load()) break; if (FD_ISSET(p->wakeR, &rfds)) break; size_t bufLen = buf.size(); wireguard_result r = wireguard_tick(p->wg, buf.data(), &bufLen); if (r.op == WRITE_TO_NETWORK && r.size > 0) { ::sendto(p->udpFd, buf.data(), r.size, 0, reinterpret_cast(&peer), sizeof(peer)); } } } } // anonymous namespace // TunnelManager implementation TunnelManager::TunnelManager(QObject *parent) : QObject(parent) {} TunnelManager::~TunnelManager() { stop(); } bool TunnelManager::start(const WireGuardConfig &cfg) { if (!cfg.isValid()) { m_error = QStringLiteral("Invalid WireGuard configuration"); emit tunnelError(m_error); return false; } if (m_running) stop(); return platformStart(cfg); } void TunnelManager::stop() { if (!m_running) return; m_running = false; platformStop(); m_localAddr.clear(); emit tunnelDown(); } bool TunnelManager::platformStart(const WireGuardConfig &cfg) { auto *p = new LinuxTunnelPriv(); p->cfg = cfg; p->owner = this; // 1. Create boringtun tunnel { const QByteArray priv = cfg.privateKey.toUtf8(); const QByteArray pubk = cfg.peerPublicKey.toUtf8(); const QByteArray psk = cfg.presharedKey.toUtf8(); p->wg = new_tunnel( priv.constData(), pubk.constData(), psk.isEmpty() ? nullptr : psk.constData(), cfg.persistentKeepalive, /*log_level=*/1); } if (!p->wg) { delete p; m_error = QStringLiteral("boringtun: failed to create WireGuard tunnel"); emit tunnelError(m_error); return false; } // 2. Open TUN device p->tunFd = openTun(p->ifName); if (p->tunFd < 0) { const QString err = QStringLiteral("Failed to open /dev/net/tun: %1") .arg(QString::fromUtf8(::strerror(errno))); tunnel_free(p->wg); delete p; m_error = err; emit tunnelError(m_error); return false; } qDebug() << "[DragonVPN] Opened" << p->ifName; // 3. UDP socket p->udpFd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (p->udpFd < 0) { const QString err = QStringLiteral("socket() failed: %1") .arg(QString::fromUtf8(::strerror(errno))); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_error = err; emit tunnelError(m_error); return false; } sockaddr_in localAddr {}; localAddr.sin_family = AF_INET; localAddr.sin_addr.s_addr = INADDR_ANY; localAddr.sin_port = 0; ::bind(p->udpFd, reinterpret_cast(&localAddr), sizeof(localAddr)); // 4. Self-pipe for clean shutdown int pipefds[2]; if (::pipe(pipefds) < 0) { const QString err = QStringLiteral("pipe() failed: %1") .arg(QString::fromUtf8(::strerror(errno))); ::close(p->udpFd); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_error = err; emit tunnelError(m_error); return false; } p->wakeR = pipefds[0]; p->wakeW = pipefds[1]; // 5. Configure interface address + routes via `ip` commands const std::string iface = p->ifName; shell("ip link set " + iface + " up"); shell("ip addr add " + cfg.address.toStdString() + " dev " + iface); for (const QString &cidr : cfg.allowedIPs) shell("ip route add " + cidr.toStdString() + " dev " + iface); m_localAddr = cfg.localIP(); // 6. Initial handshake { std::vector hs(512); size_t hsLen = hs.size(); wireguard_force_handshake(p->wg, hs.data(), &hsLen); if (hsLen > 0) { sockaddr_in peer {}; peer.sin_family = AF_INET; ::inet_pton(AF_INET, cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); peer.sin_port = htons(static_cast(cfg.endpointPort())); ::sendto(p->udpFd, hs.data(), hsLen, 0, reinterpret_cast(&peer), sizeof(peer)); } } // 7. Start threads p->running = true; m_running = true; p->thdTunToUdp = std::thread(tunToUdp, p); p->thdUdpToTun = std::thread(udpToTun, p); p->thdTicker = std::thread(ticker, p); m_priv = p; qDebug() << "[DragonVPN] Tunnel up, local IP" << m_localAddr; // Fallback: emit tunnelUp() after 1500 ms even if no packet arrives. QPointer owner(this); QTimer::singleShot(1500, this, [owner]() { if (!owner || !owner->m_running) return; auto *priv = static_cast(owner->m_priv); if (priv && !priv->gotFirstPkt.load()) emit owner->tunnelUp(owner->localAddress()); }); return true; } void TunnelManager::platformStop() { auto *p = static_cast(m_priv); if (!p) return; p->running = false; char b = 0; if (p->wakeW >= 0) ::write(p->wakeW, &b, 1); if (p->thdTunToUdp.joinable()) p->thdTunToUdp.join(); if (p->thdUdpToTun.joinable()) p->thdUdpToTun.join(); if (p->thdTicker.joinable()) p->thdTicker.join(); // Tear down routes/interface (best-effort) const std::string iface = p->ifName; for (const QString &cidr : p->cfg.allowedIPs) shell("ip route del " + cidr.toStdString() + " dev " + iface + " 2>/dev/null"); shell("ip link set " + iface + " down 2>/dev/null"); if (p->tunFd >= 0) ::close(p->tunFd); if (p->udpFd >= 0) ::close(p->udpFd); if (p->wakeR >= 0) ::close(p->wakeR); if (p->wakeW >= 0) ::close(p->wakeW); if (p->wg) tunnel_free(p->wg); delete p; m_priv = nullptr; qDebug() << "[DragonVPN] Linux tunnel stopped"; } #endif // Q_OS_LINUX