Add Linux tunnelmanager (boringtun userspace + /dev/net/tun)

This commit is contained in:
Zac Gaetano 2026-05-07 00:13:26 -04:00
parent aa7649601d
commit 4ebc674a89

View file

@ -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 <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <atomic>
#include <cstring>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#include <QDebug>
#include <QPointer>
#include <QTimer>
#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<bool> running {false};
std::atomic<bool> gotFirstPkt {false};
std::thread thdTunToUdp;
std::thread thdUdpToTun;
std::thread thdTicker;
WireGuardConfig cfg;
QPointer<TunnelManager> 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<uint16_t>(p->cfg.endpointPort()));
std::vector<uint8_t> 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<size_t>(n),
enc.data(), &encLen);
if (r.op == WRITE_TO_NETWORK && r.size > 0) {
::sendto(p->udpFd, enc.data(), r.size, 0,
reinterpret_cast<sockaddr *>(&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<uint16_t>(p->cfg.endpointPort()));
std::vector<uint8_t> 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<sockaddr *>(&from), &fromLen);
if (n <= 0) continue;
size_t plainLen = plain.size();
wireguard_result r = wireguard_read(
p->wg, enc.data(), static_cast<size_t>(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<TunnelManager> 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<sockaddr *>(&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<uint16_t>(p->cfg.endpointPort()));
std::vector<uint8_t> 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<sockaddr *>(&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<sockaddr *>(&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<uint8_t> 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<uint16_t>(cfg.endpointPort()));
::sendto(p->udpFd, hs.data(), hsLen, 0,
reinterpret_cast<sockaddr *>(&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<TunnelManager> owner(this);
QTimer::singleShot(1500, this, [owner]() {
if (!owner || !owner->m_running) return;
auto *priv = static_cast<LinuxTunnelPriv *>(owner->m_priv);
if (priv && !priv->gotFirstPkt.load())
emit owner->tunnelUp(owner->localAddress());
});
return true;
}
void TunnelManager::platformStop()
{
auto *p = static_cast<LinuxTunnelPriv *>(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