// tunnelmanager_mac.mm - macOS implementation of TunnelManager. // // TUN device : utun (com.apple.net.utun_control) - no root needed to open. // WireGuard : boringtun (Rust static lib via boringtun_ffi.h). // Routing : ifconfig + route commands via osascript "with administrator // privileges" - macOS shows a one-time auth dialog. // Threading : tunToUdp thread, udpToTun thread, ticker thread. // A self-pipe is used to unblock select() on shutdown. #import #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tunnelmanager.h" #include "wireguardconfig.h" #include "boringtun_ffi.h" static constexpr size_t kMTU = 1420; static constexpr size_t kBufSize = kMTU + 64; static constexpr uint32_t kAF_INET_HOST = AF_INET; static constexpr uint32_t kAF_INET6_HOST = AF_INET6; struct MacTunnelPriv { wireguard_tunnel *wg = nullptr; int tunFd = -1; int udpFd = -1; char tunName[IFNAMSIZ] = {}; int wakePipeR = -1; int wakePipeW = -1; std::atomic running {false}; std::atomic gotFirstPkt {false}; std::thread thdTunToUdp; std::thread thdUdpToTun; std::thread thdTicker; WireGuardConfig cfg; std::string savedGateway; QPointer owner; }; // Admin helper static bool runAsAdmin(const std::string &shellCmd, QString &errOut) { std::string escaped; escaped.reserve(shellCmd.size()); for (char ch : shellCmd) { if (ch == '"' || ch == '\\') escaped += '\\'; escaped += ch; } NSString *script = [NSString stringWithFormat: @"do shell script \"%s\" with administrator privileges", escaped.c_str()]; NSTask *task = [[NSTask alloc] init]; NSPipe *errPipe = [NSPipe pipe]; task.launchPath = @"/usr/bin/osascript"; task.arguments = @[@"-e", script]; task.standardError = errPipe; task.standardOutput = [NSPipe pipe]; @try { [task launch]; [task waitUntilExit]; } @catch (NSException *e) { errOut = QString::fromNSString(e.reason); return false; } if (task.terminationStatus != 0) { NSData *data = [[errPipe fileHandleForReading] readDataToEndOfFile]; errOut = QString::fromUtf8(reinterpret_cast(data.bytes), static_cast(data.length)); return false; } return true; } static std::string runAsUser(const std::string &shellCmd) { FILE *f = popen(shellCmd.c_str(), "r"); if (!f) return {}; char buf[512] = {}; std::string out; while (fgets(buf, sizeof(buf), f)) out += buf; pclose(f); while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) out.pop_back(); return out; } // utun helpers static int openUtun(char tunName[IFNAMSIZ]) { int fd = ::socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (fd < 0) { qWarning() << "[DragonVPN] socket(PF_SYSTEM):" << strerror(errno); return -1; } struct ctl_info info {}; ::strlcpy(info.ctl_name, UTUN_CONTROL_NAME, sizeof(info.ctl_name)); if (::ioctl(fd, CTLIOCGINFO, &info) < 0) { qWarning() << "[DragonVPN] ioctl(CTLIOCGINFO):" << strerror(errno); ::close(fd); return -1; } struct sockaddr_ctl addr {}; addr.sc_len = sizeof(addr); addr.sc_family = AF_SYSTEM; addr.ss_sysaddr = AF_SYS_CONTROL; addr.sc_id = info.ctl_id; addr.sc_unit = 0; if (::connect(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { qWarning() << "[DragonVPN] connect(utun):" << strerror(errno); ::close(fd); return -1; } socklen_t namelen = IFNAMSIZ; if (::getsockopt(fd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, tunName, &namelen) < 0) ::snprintf(tunName, IFNAMSIZ, "utun?"); return fd; } // Routing static bool configureInterface(MacTunnelPriv *p, const WireGuardConfig &cfg, QString &errOut) { const std::string tun = p->tunName; const std::string localIP = cfg.localIP().toStdString(); const std::string epHost = cfg.endpointHost().toStdString(); p->savedGateway = runAsUser( "route -n get default 2>/dev/null | awk '/gateway:/ { print $2; exit }'"); std::string script; script += "ifconfig " + tun + " " + localIP + " " + localIP + " up"; script += " && ifconfig " + tun + " mtu " + std::to_string(kMTU); if (!epHost.empty() && !p->savedGateway.empty()) { script += " && route add -host " + epHost + " " + p->savedGateway; } for (const QString &cidr : cfg.allowedIPs) { const std::string net = cidr.trimmed().toStdString(); if (net == "0.0.0.0/0") { script += " && route add -net 0.0.0.0/1 -interface " + tun; script += " && route add -net 128.0.0.0/1 -interface " + tun; } else { script += " && route add -net " + net + " -interface " + tun; } } return runAsAdmin(script, errOut); } static void teardownRoutes(MacTunnelPriv *p) { const std::string epHost = p->cfg.endpointHost().toStdString(); QString ignored; std::string script; bool first = true; auto append = [&](const std::string &cmd) { if (!first) script += " ; "; script += cmd; first = false; }; if (!epHost.empty()) append("route delete -host " + epHost); for (const QString &cidr : p->cfg.allowedIPs) { const std::string net = cidr.trimmed().toStdString(); if (net == "0.0.0.0/0") { append("route delete -net 0.0.0.0/1"); append("route delete -net 128.0.0.0/1"); } else { append("route delete -net " + net); } } if (!script.empty()) runAsAdmin(script, ignored); } // I/O threads static void tunToUdpThread(MacTunnelPriv *p) { std::vector plain(kBufSize + 4); std::vector enc (kBufSize + 64); while (p->running.load()) { fd_set rfds; FD_ZERO(&rfds); FD_SET(p->tunFd, &rfds); FD_SET(p->wakePipeR, &rfds); int maxFd = std::max(p->tunFd, p->wakePipeR) + 1; if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break; if (!p->running.load() || FD_ISSET(p->wakePipeR, &rfds)) break; ssize_t n = ::read(p->tunFd, plain.data(), plain.size()); if (n <= 4) continue; const uint8_t *ipPkt = plain.data() + 4; const size_t ipLen = static_cast(n - 4); size_t encLen = enc.size(); wireguard_result res = wireguard_write( p->wg, ipPkt, ipLen, enc.data(), &encLen); if (res.op == WRITE_TO_NETWORK && res.size > 0) ::send(p->udpFd, enc.data(), res.size, 0); else if (res.op == WIREGUARD_ERROR) qWarning() << "[DragonVPN] wireguard_write error"; } } static void udpToTunThread(MacTunnelPriv *p) { std::vector enc (kBufSize + 64); std::vector plain(kBufSize); while (p->running.load()) { fd_set rfds; FD_ZERO(&rfds); FD_SET(p->udpFd, &rfds); FD_SET(p->wakePipeR, &rfds); int maxFd = std::max(p->udpFd, p->wakePipeR) + 1; if (::select(maxFd, &rfds, nullptr, nullptr, nullptr) < 0) break; if (!p->running.load() || FD_ISSET(p->wakePipeR, &rfds)) break; ssize_t n = ::recv(p->udpFd, enc.data(), enc.size(), 0); if (n <= 0) continue; size_t plainLen = plain.size(); wireguard_result res = wireguard_read( p->wg, enc.data(), static_cast(n), plain.data(), &plainLen); if (res.op == WRITE_TO_TUNNEL_IPV4 || res.op == WRITE_TO_TUNNEL_IPV6) { const uint32_t af = (res.op == WRITE_TO_TUNNEL_IPV4) ? kAF_INET_HOST : kAF_INET6_HOST; struct iovec iov[2]; iov[0].iov_base = const_cast(&af); iov[0].iov_len = 4; iov[1].iov_base = plain.data(); iov[1].iov_len = res.size; ::writev(p->tunFd, iov, 2); 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 (res.op == WRITE_TO_NETWORK && res.size > 0) { ::send(p->udpFd, plain.data(), res.size, 0); } else if (res.op == WIREGUARD_ERROR) { qWarning() << "[DragonVPN] wireguard_read error"; } } } static void tickerThread(MacTunnelPriv *p) { std::vector buf(kBufSize + 32); while (p->running.load()) { fd_set rfds; FD_ZERO(&rfds); FD_SET(p->wakePipeR, &rfds); struct timeval tv { 0, 100000 }; ::select(p->wakePipeR + 1, &rfds, nullptr, nullptr, &tv); if (!p->running.load()) break; size_t bufLen = buf.size(); wireguard_result res = wireguard_tick(p->wg, buf.data(), &bufLen); if (res.op == WRITE_TO_NETWORK && res.size > 0) ::send(p->udpFd, buf.data(), res.size, 0); } } // TunnelManager platform 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 MacTunnelPriv; m_priv = p; p->cfg = cfg; p->owner = this; { 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) { m_error = QStringLiteral("boringtun: failed to create tunnel (bad key material?)"); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } p->tunFd = openUtun(p->tunName); if (p->tunFd < 0) { m_error = QStringLiteral("Failed to open utun device"); tunnel_free(p->wg); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } qDebug() << "[DragonVPN] Opened" << p->tunName; p->udpFd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (p->udpFd < 0) { m_error = QStringLiteral("Failed to create UDP socket: ") + strerror(errno); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } struct sockaddr_in serverAddr {}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(cfg.endpointPort()); if (::inet_pton(AF_INET, cfg.endpointHost().toUtf8().constData(), &serverAddr.sin_addr) != 1) { m_error = QStringLiteral("Invalid endpoint host: ") + cfg.endpointHost(); ::close(p->udpFd); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } if (::connect(p->udpFd, reinterpret_cast(&serverAddr), sizeof(serverAddr)) < 0) { m_error = QString("Failed to connect UDP socket to %1:%2 - %3") .arg(cfg.endpointHost()) .arg(cfg.endpointPort()) .arg(strerror(errno)); ::close(p->udpFd); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } int pipeFds[2]; if (::pipe(pipeFds) < 0) { m_error = QStringLiteral("Failed to create wake pipe"); ::close(p->udpFd); ::close(p->tunFd); tunnel_free(p->wg); delete p; m_priv = nullptr; emit tunnelError(m_error); return false; } p->wakePipeR = pipeFds[0]; p->wakePipeW = pipeFds[1]; QString routeErr; if (!configureInterface(p, cfg, routeErr)) { qWarning() << "[DragonVPN] Route setup failed (non-fatal):" << routeErr; } m_localAddr = cfg.localIP(); p->running = true; m_running = true; p->thdTunToUdp = std::thread(tunToUdpThread, p); p->thdUdpToTun = std::thread(udpToTunThread, p); p->thdTicker = std::thread(tickerThread, p); { std::vector hsBuf(kBufSize + 32); size_t hsBufLen = hsBuf.size(); wireguard_result res = wireguard_force_handshake( p->wg, hsBuf.data(), &hsBufLen); if (res.op == WRITE_TO_NETWORK && res.size > 0) ::send(p->udpFd, hsBuf.data(), res.size, 0); } qDebug() << "[DragonVPN] Tunnel up, local IP" << m_localAddr; 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; if (p->wakePipeW >= 0) { ::close(p->wakePipeW); p->wakePipeW = -1; } if (p->thdTunToUdp.joinable()) p->thdTunToUdp.join(); if (p->thdUdpToTun.joinable()) p->thdUdpToTun.join(); if (p->thdTicker.joinable()) p->thdTicker.join(); teardownRoutes(p); if (p->wakePipeR >= 0) { ::close(p->wakePipeR); p->wakePipeR = -1; } if (p->udpFd >= 0) { ::close(p->udpFd); p->udpFd = -1; } if (p->tunFd >= 0) { ::close(p->tunFd); p->tunFd = -1; } if (p->wg) { tunnel_free(p->wg); p->wg = nullptr; } delete p; m_priv = nullptr; qDebug() << "[DragonVPN] Tunnel stopped"; }