diff --git a/app/vpn/tunnelmanager_linux.cpp b/app/vpn/tunnelmanager_linux.cpp new file mode 100644 index 0000000..387577e --- /dev/null +++ b/app/vpn/tunnelmanager_linux.cpp @@ -0,0 +1,370 @@ +// 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