Mac tunnelmanager: align with new size_t-based FFI; first-packet tunnelUp() signal
This commit is contained in:
parent
f270350ae6
commit
41b431a11c
1 changed files with 92 additions and 135 deletions
|
|
@ -1,28 +1,11 @@
|
||||||
// tunnelmanager_mac.mm — macOS implementation of TunnelManager.
|
// tunnelmanager_mac.mm - macOS implementation of TunnelManager.
|
||||||
//
|
//
|
||||||
// Architecture
|
// TUN device : utun (com.apple.net.utun_control) - no root needed to open.
|
||||||
// ────────────
|
// WireGuard : boringtun (Rust static lib via boringtun_ffi.h).
|
||||||
// • TUN device : utun (com.apple.net.utun_control) — no root needed to open.
|
// Routing : ifconfig + route commands via osascript "with administrator
|
||||||
// • WireGuard : boringtun (Rust static lib via boringtun_ffi.h).
|
// privileges" - macOS shows a one-time auth dialog.
|
||||||
// • Routing : ifconfig + route commands via osascript "with administrator
|
// Threading : tunToUdp thread, udpToTun thread, ticker thread.
|
||||||
// privileges" — macOS shows a one-time auth dialog.
|
// A self-pipe is used to unblock select() on shutdown.
|
||||||
// • Threading : tunToUdp thread, udpToTun thread, ticker thread.
|
|
||||||
// A self-pipe is used to unblock select() on shutdown.
|
|
||||||
//
|
|
||||||
// Privilege model
|
|
||||||
// ───────────────
|
|
||||||
// Opening the utun fd: no root.
|
|
||||||
// ifconfig (set IP + MTU): needs admin → osascript dialog.
|
|
||||||
// route add: needs admin → same osascript dialog (batched into one call).
|
|
||||||
// Subsequent reconnects reuse the saved original-gateway so the dialog
|
|
||||||
// should only appear once per session.
|
|
||||||
//
|
|
||||||
// Packet framing on macOS utun
|
|
||||||
// ─────────────────────────────
|
|
||||||
// Every packet on the utun fd is prefixed by a 4-byte address-family word
|
|
||||||
// in host byte order (AF_INET = 2, AF_INET6 = 30 on macOS). We strip this
|
|
||||||
// prefix before handing packets to boringtun, and prepend it before writing
|
|
||||||
// decrypted packets back to the TUN device.
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
|
@ -31,6 +14,7 @@
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/sys_domain.h>
|
#include <sys/sys_domain.h>
|
||||||
#include <sys/select.h>
|
#include <sys/select.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
#include <net/if_utun.h>
|
#include <net/if_utun.h>
|
||||||
#include <net/if.h>
|
#include <net/if.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
|
|
@ -47,48 +31,43 @@
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
|
#include <QPointer>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "tunnelmanager.h"
|
#include "tunnelmanager.h"
|
||||||
#include "wireguardconfig.h"
|
#include "wireguardconfig.h"
|
||||||
#include "boringtun_ffi.h"
|
#include "boringtun_ffi.h"
|
||||||
|
|
||||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
static constexpr size_t kMTU = 1420;
|
static constexpr size_t kMTU = 1420;
|
||||||
static constexpr size_t kBufSize = kMTU + 64; // MTU + WireGuard overhead
|
static constexpr size_t kBufSize = kMTU + 64;
|
||||||
static constexpr uint32_t kAF_INET_HOST = AF_INET; // 2 on macOS (host order)
|
static constexpr uint32_t kAF_INET_HOST = AF_INET;
|
||||||
static constexpr uint32_t kAF_INET6_HOST = AF_INET6; // 30 on macOS
|
static constexpr uint32_t kAF_INET6_HOST = AF_INET6;
|
||||||
|
|
||||||
// ─── MacTunnelPriv ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
struct MacTunnelPriv {
|
struct MacTunnelPriv {
|
||||||
wireguard_tunnel *wg = nullptr;
|
wireguard_tunnel *wg = nullptr;
|
||||||
int tunFd = -1; ///< utun file descriptor
|
int tunFd = -1;
|
||||||
int udpFd = -1; ///< UDP socket connected to WG server
|
int udpFd = -1;
|
||||||
char tunName[IFNAMSIZ] = {};
|
char tunName[IFNAMSIZ] = {};
|
||||||
|
|
||||||
// Self-pipe for signalling threads to stop (write end closed on stop()).
|
|
||||||
int wakePipeR = -1;
|
int wakePipeR = -1;
|
||||||
int wakePipeW = -1;
|
int wakePipeW = -1;
|
||||||
|
|
||||||
std::atomic<bool> running {false};
|
std::atomic<bool> running {false};
|
||||||
|
std::atomic<bool> gotFirstPkt {false};
|
||||||
std::thread thdTunToUdp;
|
std::thread thdTunToUdp;
|
||||||
std::thread thdUdpToTun;
|
std::thread thdUdpToTun;
|
||||||
std::thread thdTicker;
|
std::thread thdTicker;
|
||||||
|
|
||||||
// Saved for route cleanup
|
|
||||||
WireGuardConfig cfg;
|
WireGuardConfig cfg;
|
||||||
std::string savedGateway; ///< Original default gateway IP string
|
std::string savedGateway;
|
||||||
|
|
||||||
|
QPointer<TunnelManager> owner;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Admin helper ─────────────────────────────────────────────────────────────
|
// Admin helper
|
||||||
|
|
||||||
/// Run a shell command with administrator privileges via osascript.
|
|
||||||
/// macOS shows a standard auth dialog if the user is not already elevated.
|
|
||||||
static bool runAsAdmin(const std::string &shellCmd, QString &errOut)
|
static bool runAsAdmin(const std::string &shellCmd, QString &errOut)
|
||||||
{
|
{
|
||||||
// Escape double-quotes and backslashes for the AppleScript string literal.
|
|
||||||
std::string escaped;
|
std::string escaped;
|
||||||
escaped.reserve(shellCmd.size());
|
escaped.reserve(shellCmd.size());
|
||||||
for (char ch : shellCmd) {
|
for (char ch : shellCmd) {
|
||||||
|
|
@ -100,12 +79,12 @@ static bool runAsAdmin(const std::string &shellCmd, QString &errOut)
|
||||||
@"do shell script \"%s\" with administrator privileges",
|
@"do shell script \"%s\" with administrator privileges",
|
||||||
escaped.c_str()];
|
escaped.c_str()];
|
||||||
|
|
||||||
NSTask *task = [[NSTask alloc] init];
|
NSTask *task = [[NSTask alloc] init];
|
||||||
NSPipe *errPipe = [NSPipe pipe];
|
NSPipe *errPipe = [NSPipe pipe];
|
||||||
task.launchPath = @"/usr/bin/osascript";
|
task.launchPath = @"/usr/bin/osascript";
|
||||||
task.arguments = @[@"-e", script];
|
task.arguments = @[@"-e", script];
|
||||||
task.standardError = errPipe;
|
task.standardError = errPipe;
|
||||||
task.standardOutput = [NSPipe pipe];
|
task.standardOutput = [NSPipe pipe];
|
||||||
|
|
||||||
@try {
|
@try {
|
||||||
[task launch];
|
[task launch];
|
||||||
|
|
@ -124,8 +103,6 @@ static bool runAsAdmin(const std::string &shellCmd, QString &errOut)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a shell command as the current user (no privilege elevation).
|
|
||||||
/// Returns the stdout as a trimmed string, or "" on failure.
|
|
||||||
static std::string runAsUser(const std::string &shellCmd)
|
static std::string runAsUser(const std::string &shellCmd)
|
||||||
{
|
{
|
||||||
FILE *f = popen(shellCmd.c_str(), "r");
|
FILE *f = popen(shellCmd.c_str(), "r");
|
||||||
|
|
@ -134,17 +111,13 @@ static std::string runAsUser(const std::string &shellCmd)
|
||||||
std::string out;
|
std::string out;
|
||||||
while (fgets(buf, sizeof(buf), f)) out += buf;
|
while (fgets(buf, sizeof(buf), f)) out += buf;
|
||||||
pclose(f);
|
pclose(f);
|
||||||
// Trim trailing whitespace
|
|
||||||
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' '))
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' '))
|
||||||
out.pop_back();
|
out.pop_back();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── utun helpers ─────────────────────────────────────────────────────────────
|
// utun helpers
|
||||||
|
|
||||||
/// Open a utun device. No root required.
|
|
||||||
/// @param[out] tunName Filled with the interface name, e.g. "utun3".
|
|
||||||
/// @return File descriptor on success, -1 on failure.
|
|
||||||
static int openUtun(char tunName[IFNAMSIZ])
|
static int openUtun(char tunName[IFNAMSIZ])
|
||||||
{
|
{
|
||||||
int fd = ::socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
|
int fd = ::socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
|
||||||
|
|
@ -162,7 +135,7 @@ static int openUtun(char tunName[IFNAMSIZ])
|
||||||
addr.sc_family = AF_SYSTEM;
|
addr.sc_family = AF_SYSTEM;
|
||||||
addr.ss_sysaddr = AF_SYS_CONTROL;
|
addr.ss_sysaddr = AF_SYS_CONTROL;
|
||||||
addr.sc_id = info.ctl_id;
|
addr.sc_id = info.ctl_id;
|
||||||
addr.sc_unit = 0; // 0 = kernel assigns next available unit
|
addr.sc_unit = 0;
|
||||||
|
|
||||||
if (::connect(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0) {
|
if (::connect(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0) {
|
||||||
qWarning() << "[DragonVPN] connect(utun):" << strerror(errno);
|
qWarning() << "[DragonVPN] connect(utun):" << strerror(errno);
|
||||||
|
|
@ -176,38 +149,28 @@ static int openUtun(char tunName[IFNAMSIZ])
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Routing ──────────────────────────────────────────────────────────────────
|
// Routing
|
||||||
|
|
||||||
/// Configure IP address, MTU, and routes on the utun interface.
|
|
||||||
/// Requires one macOS administrator auth dialog.
|
|
||||||
static bool configureInterface(MacTunnelPriv *p, const WireGuardConfig &cfg, QString &errOut)
|
static bool configureInterface(MacTunnelPriv *p, const WireGuardConfig &cfg, QString &errOut)
|
||||||
{
|
{
|
||||||
const std::string tun = p->tunName;
|
const std::string tun = p->tunName;
|
||||||
const std::string localIP = cfg.localIP().toStdString();
|
const std::string localIP = cfg.localIP().toStdString();
|
||||||
const std::string epHost = cfg.endpointHost().toStdString();
|
const std::string epHost = cfg.endpointHost().toStdString();
|
||||||
|
|
||||||
// Discover current default gateway before we touch routing.
|
|
||||||
p->savedGateway = runAsUser(
|
p->savedGateway = runAsUser(
|
||||||
"route -n get default 2>/dev/null | awk '/gateway:/ { print $2; exit }'");
|
"route -n get default 2>/dev/null | awk '/gateway:/ { print $2; exit }'");
|
||||||
|
|
||||||
// Build a single multi-command script to minimise auth prompts.
|
|
||||||
std::string script;
|
std::string script;
|
||||||
|
|
||||||
// 1. Assign IP (point-to-point to itself is the canonical utun style)
|
|
||||||
script += "ifconfig " + tun + " " + localIP + " " + localIP + " up";
|
script += "ifconfig " + tun + " " + localIP + " " + localIP + " up";
|
||||||
script += " && ifconfig " + tun + " mtu " + std::to_string(kMTU);
|
script += " && ifconfig " + tun + " mtu " + std::to_string(kMTU);
|
||||||
|
|
||||||
// 2. If endpoint resolves to a real IP, pin it to the original gateway so
|
|
||||||
// the encrypted packets can still reach the server after we add routes.
|
|
||||||
if (!epHost.empty() && !p->savedGateway.empty()) {
|
if (!epHost.empty() && !p->savedGateway.empty()) {
|
||||||
script += " && route add -host " + epHost + " " + p->savedGateway;
|
script += " && route add -host " + epHost + " " + p->savedGateway;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Add routes for each AllowedIPs entry via the utun interface.
|
|
||||||
for (const QString &cidr : cfg.allowedIPs) {
|
for (const QString &cidr : cfg.allowedIPs) {
|
||||||
const std::string net = cidr.trimmed().toStdString();
|
const std::string net = cidr.trimmed().toStdString();
|
||||||
if (net == "0.0.0.0/0") {
|
if (net == "0.0.0.0/0") {
|
||||||
// Two /1 routes override the default without replacing it.
|
|
||||||
script += " && route add -net 0.0.0.0/1 -interface " + tun;
|
script += " && route add -net 0.0.0.0/1 -interface " + tun;
|
||||||
script += " && route add -net 128.0.0.0/1 -interface " + tun;
|
script += " && route add -net 128.0.0.0/1 -interface " + tun;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -218,7 +181,6 @@ static bool configureInterface(MacTunnelPriv *p, const WireGuardConfig &cfg, QSt
|
||||||
return runAsAdmin(script, errOut);
|
return runAsAdmin(script, errOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove routes added in configureInterface. Best-effort; never fatal.
|
|
||||||
static void teardownRoutes(MacTunnelPriv *p)
|
static void teardownRoutes(MacTunnelPriv *p)
|
||||||
{
|
{
|
||||||
const std::string epHost = p->cfg.endpointHost().toStdString();
|
const std::string epHost = p->cfg.endpointHost().toStdString();
|
||||||
|
|
@ -249,17 +211,14 @@ static void teardownRoutes(MacTunnelPriv *p)
|
||||||
runAsAdmin(script, ignored);
|
runAsAdmin(script, ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── I/O threads ──────────────────────────────────────────────────────────────
|
// I/O threads
|
||||||
|
|
||||||
/// TUN → UDP: read plaintext IP packets from utun, encrypt with boringtun,
|
|
||||||
/// send encrypted datagrams to the WireGuard server.
|
|
||||||
static void tunToUdpThread(MacTunnelPriv *p)
|
static void tunToUdpThread(MacTunnelPriv *p)
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> plain(kBufSize + 4); // +4 for AF header
|
std::vector<uint8_t> plain(kBufSize + 4);
|
||||||
std::vector<uint8_t> enc (kBufSize + 32); // +32 for WG overhead
|
std::vector<uint8_t> enc (kBufSize + 64);
|
||||||
|
|
||||||
while (p->running) {
|
while (p->running.load()) {
|
||||||
// Wait for data on tunFd or the wake pipe using select().
|
|
||||||
fd_set rfds;
|
fd_set rfds;
|
||||||
FD_ZERO(&rfds);
|
FD_ZERO(&rfds);
|
||||||
FD_SET(p->tunFd, &rfds);
|
FD_SET(p->tunFd, &rfds);
|
||||||
|
|
@ -267,18 +226,17 @@ static void tunToUdpThread(MacTunnelPriv *p)
|
||||||
int maxFd = std::max(p->tunFd, p->wakePipeR) + 1;
|
int maxFd = std::max(p->tunFd, p->wakePipeR) + 1;
|
||||||
|
|
||||||
if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break;
|
if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break;
|
||||||
if (!p->running || FD_ISSET(p->wakePipeR, &rfds)) break;
|
if (!p->running.load() || FD_ISSET(p->wakePipeR, &rfds)) break;
|
||||||
|
|
||||||
ssize_t n = ::read(p->tunFd, plain.data(), plain.size());
|
ssize_t n = ::read(p->tunFd, plain.data(), plain.size());
|
||||||
if (n <= 4) continue; // error or empty AF header
|
if (n <= 4) continue;
|
||||||
|
|
||||||
// Strip the 4-byte AF header; the rest is a raw IP packet.
|
const uint8_t *ipPkt = plain.data() + 4;
|
||||||
const uint8_t *ipPkt = plain.data() + 4;
|
const size_t ipLen = static_cast<size_t>(n - 4);
|
||||||
const uint32_t ipLen = static_cast<uint32_t>(n - 4);
|
|
||||||
|
|
||||||
|
size_t encLen = enc.size();
|
||||||
wireguard_result res = wireguard_write(
|
wireguard_result res = wireguard_write(
|
||||||
p->wg, ipPkt, ipLen,
|
p->wg, ipPkt, ipLen, enc.data(), &encLen);
|
||||||
enc.data(), static_cast<uint32_t>(enc.size()));
|
|
||||||
|
|
||||||
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
||||||
::send(p->udpFd, enc.data(), res.size, 0);
|
::send(p->udpFd, enc.data(), res.size, 0);
|
||||||
|
|
@ -287,14 +245,12 @@ static void tunToUdpThread(MacTunnelPriv *p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UDP → TUN: receive encrypted datagrams from the WireGuard server,
|
|
||||||
/// decrypt with boringtun, write plaintext IP packets to utun.
|
|
||||||
static void udpToTunThread(MacTunnelPriv *p)
|
static void udpToTunThread(MacTunnelPriv *p)
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> enc (kBufSize + 32);
|
std::vector<uint8_t> enc (kBufSize + 64);
|
||||||
std::vector<uint8_t> plain(kBufSize);
|
std::vector<uint8_t> plain(kBufSize);
|
||||||
|
|
||||||
while (p->running) {
|
while (p->running.load()) {
|
||||||
fd_set rfds;
|
fd_set rfds;
|
||||||
FD_ZERO(&rfds);
|
FD_ZERO(&rfds);
|
||||||
FD_SET(p->udpFd, &rfds);
|
FD_SET(p->udpFd, &rfds);
|
||||||
|
|
@ -302,17 +258,17 @@ static void udpToTunThread(MacTunnelPriv *p)
|
||||||
int maxFd = std::max(p->udpFd, p->wakePipeR) + 1;
|
int maxFd = std::max(p->udpFd, p->wakePipeR) + 1;
|
||||||
|
|
||||||
if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break;
|
if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break;
|
||||||
if (!p->running || FD_ISSET(p->wakePipeR, &rfds)) break;
|
if (!p->running.load() || FD_ISSET(p->wakePipeR, &rfds)) break;
|
||||||
|
|
||||||
ssize_t n = ::recv(p->udpFd, enc.data(), enc.size(), 0);
|
ssize_t n = ::recv(p->udpFd, enc.data(), enc.size(), 0);
|
||||||
if (n <= 0) continue;
|
if (n <= 0) continue;
|
||||||
|
|
||||||
|
size_t plainLen = plain.size();
|
||||||
wireguard_result res = wireguard_read(
|
wireguard_result res = wireguard_read(
|
||||||
p->wg, enc.data(), static_cast<uint32_t>(n),
|
p->wg, enc.data(), static_cast<size_t>(n),
|
||||||
plain.data(), static_cast<uint32_t>(plain.size()));
|
plain.data(), &plainLen);
|
||||||
|
|
||||||
if (res.op == WRITE_TO_TUNNEL_IPV4 || res.op == WRITE_TO_TUNNEL_IPV6) {
|
if (res.op == WRITE_TO_TUNNEL_IPV4 || res.op == WRITE_TO_TUNNEL_IPV6) {
|
||||||
// Prepend 4-byte AF header (host byte order on macOS).
|
|
||||||
const uint32_t af = (res.op == WRITE_TO_TUNNEL_IPV4)
|
const uint32_t af = (res.op == WRITE_TO_TUNNEL_IPV4)
|
||||||
? kAF_INET_HOST : kAF_INET6_HOST;
|
? kAF_INET_HOST : kAF_INET6_HOST;
|
||||||
struct iovec iov[2];
|
struct iovec iov[2];
|
||||||
|
|
@ -322,8 +278,15 @@ static void udpToTunThread(MacTunnelPriv *p)
|
||||||
iov[1].iov_len = res.size;
|
iov[1].iov_len = res.size;
|
||||||
::writev(p->tunFd, iov, 2);
|
::writev(p->tunFd, iov, 2);
|
||||||
|
|
||||||
|
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 (res.op == WRITE_TO_NETWORK && res.size > 0) {
|
} else if (res.op == WRITE_TO_NETWORK && res.size > 0) {
|
||||||
// boringtun needs to send a handshake response or keepalive ACK.
|
|
||||||
::send(p->udpFd, plain.data(), res.size, 0);
|
::send(p->udpFd, plain.data(), res.size, 0);
|
||||||
|
|
||||||
} else if (res.op == WIREGUARD_ERROR) {
|
} else if (res.op == WIREGUARD_ERROR) {
|
||||||
|
|
@ -332,30 +295,26 @@ static void udpToTunThread(MacTunnelPriv *p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ticker: drives boringtun's internal timer (keepalives, handshake retries).
|
|
||||||
/// Call every 100 ms.
|
|
||||||
static void tickerThread(MacTunnelPriv *p)
|
static void tickerThread(MacTunnelPriv *p)
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> buf(kBufSize + 32);
|
std::vector<uint8_t> buf(kBufSize + 32);
|
||||||
|
|
||||||
while (p->running) {
|
while (p->running.load()) {
|
||||||
// Sleep 100 ms with early-exit via wake pipe.
|
|
||||||
fd_set rfds;
|
fd_set rfds;
|
||||||
FD_ZERO(&rfds);
|
FD_ZERO(&rfds);
|
||||||
FD_SET(p->wakePipeR, &rfds);
|
FD_SET(p->wakePipeR, &rfds);
|
||||||
struct timeval tv { 0, 100000 }; // 100 ms
|
struct timeval tv { 0, 100000 };
|
||||||
::select(p->wakePipeR + 1, &rfds, nullptr, nullptr, &tv);
|
::select(p->wakePipeR + 1, &rfds, nullptr, nullptr, &tv);
|
||||||
if (!p->running) break;
|
if (!p->running.load()) break;
|
||||||
|
|
||||||
wireguard_result res = wireguard_tick(
|
|
||||||
p->wg, buf.data(), static_cast<uint32_t>(buf.size()));
|
|
||||||
|
|
||||||
|
size_t bufLen = buf.size();
|
||||||
|
wireguard_result res = wireguard_tick(p->wg, buf.data(), &bufLen);
|
||||||
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
||||||
::send(p->udpFd, buf.data(), res.size, 0);
|
::send(p->udpFd, buf.data(), res.size, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── TunnelManager platform implementation ────────────────────────────────────
|
// TunnelManager platform implementation
|
||||||
|
|
||||||
TunnelManager::TunnelManager(QObject *parent) : QObject(parent) {}
|
TunnelManager::TunnelManager(QObject *parent) : QObject(parent) {}
|
||||||
|
|
||||||
|
|
@ -380,7 +339,7 @@ void TunnelManager::stop()
|
||||||
m_running = false;
|
m_running = false;
|
||||||
platformStop();
|
platformStop();
|
||||||
m_localAddr.clear();
|
m_localAddr.clear();
|
||||||
emit disconnected();
|
emit tunnelDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
|
|
@ -388,14 +347,19 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
auto *p = new MacTunnelPriv;
|
auto *p = new MacTunnelPriv;
|
||||||
m_priv = p;
|
m_priv = p;
|
||||||
p->cfg = cfg;
|
p->cfg = cfg;
|
||||||
|
p->owner = this;
|
||||||
|
|
||||||
// ── 1. Create boringtun instance ─────────────────────────────────────
|
{
|
||||||
p->wg = new_tunnel(
|
const QByteArray priv = cfg.privateKey.toUtf8();
|
||||||
cfg.privateKey.toUtf8().constData(),
|
const QByteArray pubk = cfg.peerPublicKey.toUtf8();
|
||||||
cfg.peerPublicKey.toUtf8().constData(),
|
const QByteArray psk = cfg.presharedKey.toUtf8();
|
||||||
cfg.presharedKey.isEmpty() ? nullptr : cfg.presharedKey.toUtf8().constData(),
|
p->wg = new_tunnel(
|
||||||
cfg.persistentKeepalive,
|
priv.constData(),
|
||||||
/*index=*/0);
|
pubk.constData(),
|
||||||
|
psk.isEmpty() ? nullptr : psk.constData(),
|
||||||
|
cfg.persistentKeepalive,
|
||||||
|
/*log_level=*/1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!p->wg) {
|
if (!p->wg) {
|
||||||
m_error = QStringLiteral("boringtun: failed to create tunnel (bad key material?)");
|
m_error = QStringLiteral("boringtun: failed to create tunnel (bad key material?)");
|
||||||
|
|
@ -404,7 +368,6 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Open utun ──────────────────────────────────────────────────────
|
|
||||||
p->tunFd = openUtun(p->tunName);
|
p->tunFd = openUtun(p->tunName);
|
||||||
if (p->tunFd < 0) {
|
if (p->tunFd < 0) {
|
||||||
m_error = QStringLiteral("Failed to open utun device");
|
m_error = QStringLiteral("Failed to open utun device");
|
||||||
|
|
@ -415,7 +378,6 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
}
|
}
|
||||||
qDebug() << "[DragonVPN] Opened" << p->tunName;
|
qDebug() << "[DragonVPN] Opened" << p->tunName;
|
||||||
|
|
||||||
// ── 3. Create UDP socket → WireGuard server ───────────────────────────
|
|
||||||
p->udpFd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
p->udpFd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
if (p->udpFd < 0) {
|
if (p->udpFd < 0) {
|
||||||
m_error = QStringLiteral("Failed to create UDP socket: ") + strerror(errno);
|
m_error = QStringLiteral("Failed to create UDP socket: ") + strerror(errno);
|
||||||
|
|
@ -442,7 +404,7 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
if (::connect(p->udpFd,
|
if (::connect(p->udpFd,
|
||||||
reinterpret_cast<struct sockaddr *>(&serverAddr),
|
reinterpret_cast<struct sockaddr *>(&serverAddr),
|
||||||
sizeof(serverAddr)) < 0) {
|
sizeof(serverAddr)) < 0) {
|
||||||
m_error = QString("Failed to connect UDP socket to %1:%2 — %3")
|
m_error = QString("Failed to connect UDP socket to %1:%2 - %3")
|
||||||
.arg(cfg.endpointHost())
|
.arg(cfg.endpointHost())
|
||||||
.arg(cfg.endpointPort())
|
.arg(cfg.endpointPort())
|
||||||
.arg(strerror(errno));
|
.arg(strerror(errno));
|
||||||
|
|
@ -453,7 +415,6 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 4. Self-pipe for clean thread shutdown ────────────────────────────
|
|
||||||
int pipeFds[2];
|
int pipeFds[2];
|
||||||
if (::pipe(pipeFds) < 0) {
|
if (::pipe(pipeFds) < 0) {
|
||||||
m_error = QStringLiteral("Failed to create wake pipe");
|
m_error = QStringLiteral("Failed to create wake pipe");
|
||||||
|
|
@ -466,39 +427,38 @@ bool TunnelManager::platformStart(const WireGuardConfig &cfg)
|
||||||
p->wakePipeR = pipeFds[0];
|
p->wakePipeR = pipeFds[0];
|
||||||
p->wakePipeW = pipeFds[1];
|
p->wakePipeW = pipeFds[1];
|
||||||
|
|
||||||
// ── 5. Configure routing (triggers macOS auth dialog once) ────────────
|
|
||||||
QString routeErr;
|
QString routeErr;
|
||||||
if (!configureInterface(p, cfg, routeErr)) {
|
if (!configureInterface(p, cfg, routeErr)) {
|
||||||
qWarning() << "[DragonVPN] Route setup failed (non-fatal):" << routeErr;
|
qWarning() << "[DragonVPN] Route setup failed (non-fatal):" << routeErr;
|
||||||
// Non-fatal: the tunnel will still encrypt/decrypt; only routing is absent.
|
|
||||||
// The user can manually add routes or the dialog may have been dismissed.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_localAddr = cfg.localIP();
|
m_localAddr = cfg.localIP();
|
||||||
|
|
||||||
// ── 6. Start I/O threads ──────────────────────────────────────────────
|
p->running = true;
|
||||||
p->running = true;
|
m_running = true;
|
||||||
m_running = true;
|
p->thdTunToUdp = std::thread(tunToUdpThread, p);
|
||||||
p->thdTunToUdp = std::thread(tunToUdpThread, p);
|
p->thdUdpToTun = std::thread(udpToTunThread, p);
|
||||||
p->thdUdpToTun = std::thread(udpToTunThread, p);
|
p->thdTicker = std::thread(tickerThread, p);
|
||||||
p->thdTicker = std::thread(tickerThread, p);
|
|
||||||
|
|
||||||
// ── 7. Initiate first handshake ───────────────────────────────────────
|
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> hsBuf(kBufSize + 32);
|
std::vector<uint8_t> hsBuf(kBufSize + 32);
|
||||||
|
size_t hsBufLen = hsBuf.size();
|
||||||
wireguard_result res = wireguard_force_handshake(
|
wireguard_result res = wireguard_force_handshake(
|
||||||
p->wg, hsBuf.data(), static_cast<uint32_t>(hsBuf.size()));
|
p->wg, hsBuf.data(), &hsBufLen);
|
||||||
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
if (res.op == WRITE_TO_NETWORK && res.size > 0)
|
||||||
::send(p->udpFd, hsBuf.data(), res.size, 0);
|
::send(p->udpFd, hsBuf.data(), res.size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit connected() after a short delay — in production, wire this to a
|
qDebug() << "[DragonVPN] Tunnel up, local IP" << m_localAddr;
|
||||||
// "first packet received" callback from udpToTunThread instead.
|
|
||||||
QTimer::singleShot(800, this, [this]() {
|
QPointer<TunnelManager> owner(this);
|
||||||
if (m_running) emit connected();
|
QTimer::singleShot(1500, this, [owner]() {
|
||||||
|
if (!owner || !owner->m_running) return;
|
||||||
|
auto *priv = static_cast<MacTunnelPriv *>(owner->m_priv);
|
||||||
|
if (priv && !priv->gotFirstPkt.load())
|
||||||
|
emit owner->tunnelUp(owner->localAddress());
|
||||||
});
|
});
|
||||||
|
|
||||||
qDebug() << "[DragonVPN] Tunnel up, local IP" << m_localAddr;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,20 +469,17 @@ void TunnelManager::platformStop()
|
||||||
|
|
||||||
p->running = false;
|
p->running = false;
|
||||||
|
|
||||||
// Unblock all select() calls by closing the write end of the wake pipe.
|
|
||||||
if (p->wakePipeW >= 0) { ::close(p->wakePipeW); p->wakePipeW = -1; }
|
if (p->wakePipeW >= 0) { ::close(p->wakePipeW); p->wakePipeW = -1; }
|
||||||
|
|
||||||
// Give threads a moment, then hard-close fds to unblock any lingering I/O.
|
|
||||||
if (p->thdTunToUdp.joinable()) p->thdTunToUdp.join();
|
if (p->thdTunToUdp.joinable()) p->thdTunToUdp.join();
|
||||||
if (p->thdUdpToTun.joinable()) p->thdUdpToTun.join();
|
if (p->thdUdpToTun.joinable()) p->thdUdpToTun.join();
|
||||||
if (p->thdTicker.joinable()) p->thdTicker.join();
|
if (p->thdTicker.joinable()) p->thdTicker.join();
|
||||||
|
|
||||||
// Tear down routing before closing the interface.
|
|
||||||
teardownRoutes(p);
|
teardownRoutes(p);
|
||||||
|
|
||||||
if (p->wakePipeR >= 0) { ::close(p->wakePipeR); p->wakePipeR = -1; }
|
if (p->wakePipeR >= 0) { ::close(p->wakePipeR); p->wakePipeR = -1; }
|
||||||
if (p->udpFd >= 0) { ::close(p->udpFd); p->udpFd = -1; }
|
if (p->udpFd >= 0) { ::close(p->udpFd); p->udpFd = -1; }
|
||||||
if (p->tunFd >= 0) { ::close(p->tunFd); p->tunFd = -1; }
|
if (p->tunFd >= 0) { ::close(p->tunFd); p->tunFd = -1; }
|
||||||
|
|
||||||
if (p->wg) { tunnel_free(p->wg); p->wg = nullptr; }
|
if (p->wg) { tunnel_free(p->wg); p->wg = nullptr; }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue