Add Linux tunnelmanager (boringtun userspace + /dev/net/tun)
This commit is contained in:
parent
aa7649601d
commit
4ebc674a89
1 changed files with 370 additions and 0 deletions
370
app/vpn/tunnelmanager_linux.cpp
Normal file
370
app/vpn/tunnelmanager_linux.cpp
Normal 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
|
||||||
Loading…
Reference in a new issue