diff --git a/src/wg/wgclient_linux.cpp b/src/wg/wgclient_linux.cpp new file mode 100644 index 0000000..00fb153 --- /dev/null +++ b/src/wg/wgclient_linux.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#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 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 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 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 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(); + 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 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__