wg: Linux WireGuard client — /dev/net/tun + boringtun FFI

This commit is contained in:
Zac Gaetano 2026-05-06 19:20:01 -04:00
parent 77b8d616eb
commit 38a5527a61

260
src/wg/wgclient_linux.cpp Normal file
View file

@ -0,0 +1,260 @@
// src/wg/wgclient_linux.cpp — Linux WireGuard tunnel for Artemis.
//
// Opens /dev/net/tun (IFF_TUN|IFF_NO_PI) — requires CAP_NET_ADMIN or root.
// Routing is configured via `ip addr` / `ip route` subprocesses.
// Links against: libboringtun.a, pthread
//
// Packet format on Linux TUN: raw IP (IFF_NO_PI strips the protocol header).
#include "wgclient.h"
#if defined(__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 "boringtun_ffi.h"
namespace wg {
// ── helpers ───────────────────────────────────────────────────────────────────
static void shell(const std::string &cmd) {
// Fire-and-forget; ignore return value (route may already exist)
::system(cmd.c_str());
}
// ── ClientImpl ────────────────────────────────────────────────────────────────
class ClientImpl {
public:
int tunFd = -1;
int udpFd = -1;
int wakeR = -1; // self-pipe read end
int wakeW = -1; // self-pipe write end
wireguard_tunnel *wg = nullptr;
std::atomic<bool> live {false};
std::thread tTunToUdp, tUdpToTun, tTicker;
Config cfg;
char ifName[IFNAMSIZ] {};
std::string localIPStr;
LogFn log;
ErrorFn err;
};
// ── Open TUN device ───────────────────────────────────────────────────────────
static int openTun(char *nameOut) {
int fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) return -1;
struct ifreq ifr {};
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
// Let the kernel pick a name (tun0, tun1, …)
if (ioctl(fd, TUNSETIFF, &ifr) < 0) { close(fd); return -1; }
strncpy(nameOut, ifr.ifr_name, IFNAMSIZ - 1);
return fd;
}
// ── I/O threads ───────────────────────────────────────────────────────────────
static void tunToUdp(ClientImpl *p) {
struct sockaddr_in peer {}; peer.sin_family = AF_INET;
inet_pton(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((uint16_t)p->cfg.endpointPort());
std::vector<uint8_t> plain(65536), enc(65536);
fd_set fds;
while (p->live) {
FD_ZERO(&fds);
FD_SET(p->tunFd, &fds);
FD_SET(p->wakeR, &fds);
int nfds = std::max(p->tunFd, p->wakeR) + 1;
if (select(nfds, &fds, nullptr, nullptr, nullptr) <= 0) continue;
if (FD_ISSET(p->wakeR, &fds)) break;
ssize_t n = read(p->tunFd, plain.data(), plain.size());
if (n <= 0) continue;
size_t elen = enc.size();
wireguard_result r = wireguard_write(p->wg, plain.data(), (size_t)n,
enc.data(), &elen);
if (r.op == WRITE_TO_NETWORK && elen > 0)
sendto(p->udpFd, enc.data(), elen, 0,
(sockaddr *)&peer, sizeof(peer));
}
}
static void udpToTun(ClientImpl *p) {
struct sockaddr_in peer {}; peer.sin_family = AF_INET;
inet_pton(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((uint16_t)p->cfg.endpointPort());
std::vector<uint8_t> enc(65536), plain(65536);
fd_set fds;
while (p->live) {
FD_ZERO(&fds);
FD_SET(p->udpFd, &fds);
FD_SET(p->wakeR, &fds);
int nfds = std::max(p->udpFd, p->wakeR) + 1;
if (select(nfds, &fds, nullptr, nullptr, nullptr) <= 0) continue;
if (FD_ISSET(p->wakeR, &fds)) break;
struct sockaddr_in from {}; socklen_t fl = sizeof(from);
ssize_t n = recvfrom(p->udpFd, enc.data(), enc.size(), 0,
(sockaddr *)&from, &fl);
if (n <= 0) continue;
size_t plen = plain.size();
wireguard_result r = wireguard_read(p->wg, enc.data(), (size_t)n,
plain.data(), &plen);
if ((r.op == WRITE_TO_TUNNEL_IPV4 || r.op == WRITE_TO_TUNNEL_IPV6) && plen > 0)
write(p->tunFd, plain.data(), plen);
else if (r.op == WRITE_TO_NETWORK && plen > 0)
sendto(p->udpFd, plain.data(), plen, 0,
(sockaddr *)&peer, sizeof(peer));
}
}
static void ticker(ClientImpl *p) {
struct sockaddr_in peer {}; peer.sin_family = AF_INET;
inet_pton(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((uint16_t)p->cfg.endpointPort());
std::vector<uint8_t> buf(512);
fd_set fds;
while (p->live) {
struct timeval tv { 0, 100000 }; // 100 ms
FD_ZERO(&fds); FD_SET(p->wakeR, &fds);
select(p->wakeR + 1, &fds, nullptr, nullptr, &tv);
if (FD_ISSET(p->wakeR, &fds)) break;
size_t len = buf.size();
wireguard_result r = wireguard_tick(p->wg, buf.data(), &len);
if (r.op == WRITE_TO_NETWORK && len > 0)
sendto(p->udpFd, buf.data(), len, 0,
(sockaddr *)&peer, sizeof(peer));
}
}
// ── Client public API ─────────────────────────────────────────────────────────
Client::Client() = default;
Client::~Client() { stop(); }
void Client::start(const Config &cfg) {
if (m_impl && m_impl->live) return;
auto p = std::make_unique<ClientImpl>();
p->cfg = cfg;
p->log = m_log;
p->err = m_error;
// boringtun tunnel
p->wg = new_tunnel(
cfg.privateKey().c_str(),
cfg.peerPublicKey().c_str(),
cfg.presharedKey().empty() ? nullptr : cfg.presharedKey().c_str(),
cfg.persistentKeepalive(), 0);
if (!p->wg) throw std::runtime_error("boringtun: tunnel creation failed");
// TUN device
p->tunFd = openTun(p->ifName);
if (p->tunFd < 0) {
tunnel_free(p->wg);
throw std::runtime_error(std::string("openTun: ") + strerror(errno));
}
// UDP socket
p->udpFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (p->udpFd < 0) {
close(p->tunFd); tunnel_free(p->wg);
throw std::runtime_error("socket: " + std::string(strerror(errno)));
}
struct sockaddr_in local {}; local.sin_family = AF_INET;
bind(p->udpFd, (sockaddr *)&local, sizeof(local));
// Self-pipe for clean shutdown
int pipefds[2]; pipe(pipefds);
p->wakeR = pipefds[0];
p->wakeW = pipefds[1];
// Configure interface address + routes via `ip` commands
std::string iface = p->ifName;
shell("ip link set " + iface + " up");
shell("ip addr add " + cfg.address() + " dev " + iface);
for (const auto &cidr : cfg.allowedIPs())
shell("ip route add " + cidr + " dev " + iface);
p->localIPStr = cfg.addressIP();
// Initial handshake
{
std::vector<uint8_t> hs(512); size_t len = hs.size();
wireguard_force_handshake(p->wg, hs.data(), &len);
if (len > 0) {
struct sockaddr_in peer {}; peer.sin_family = AF_INET;
inet_pton(AF_INET, cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((uint16_t)cfg.endpointPort());
sendto(p->udpFd, hs.data(), len, 0, (sockaddr *)&peer, sizeof(peer));
}
}
p->live = true;
p->tTunToUdp = std::thread(tunToUdp, p.get());
p->tUdpToTun = std::thread(udpToTun, p.get());
p->tTicker = std::thread(ticker, p.get());
m_impl = std::move(p);
}
void Client::stop() {
if (!m_impl) return;
auto *p = m_impl.get();
p->live = false;
// Wake all threads via self-pipe
char b = 0; write(p->wakeW, &b, 1);
if (p->tTunToUdp.joinable()) p->tTunToUdp.join();
if (p->tUdpToTun.joinable()) p->tUdpToTun.join();
if (p->tTicker.joinable()) p->tTicker.join();
// Remove routes before bringing interface down
std::string iface = p->ifName;
for (const auto &cidr : p->cfg.allowedIPs())
shell("ip route del " + cidr + " dev " + iface + " 2>/dev/null");
shell("ip link set " + iface + " down 2>/dev/null");
close(p->tunFd);
close(p->udpFd);
close(p->wakeR);
close(p->wakeW);
tunnel_free(p->wg);
m_impl.reset();
}
bool Client::running() const { return m_impl && m_impl->live; }
std::string Client::localIP() const {
return (m_impl && m_impl->live) ? m_impl->localIPStr : "";
}
} // namespace wg
#endif // __linux__