forked from cloud/ovgate
A userspace VPN gateway: builds an OpenVPN tunnel to a VPNGate node with
the OpenVPN 3 core, terminates it in-process with lwIP, and serves SOCKS5
(RFC 1928/1929, CONNECT and UDP ASSOCIATE) over it. No root, no tun
device, no routing table changes.
Layout follows the module boundaries in docs/ARCHITECTURE.md:
vpngate/ directory fetch + CSV parse (lines run to ~13.5 KB, so the
parser streams rather than splitting on newlines)
selector/ two-phase pick: cheap prior over the whole list, then real
TCP handshake timing of the top K
ovpn/ openvpn3 driven through TunBuilder, packets over a socketpair
netstack/ lwIP: the TCP/IP stack that makes "no root" possible
egress/ the swappable way out, and make-before-break switching
socks5/ the front door
health/ per-window scoring, and the decision to move
app/ wiring, admin HTTP, signals
docs/FEASIBILITY.md is the analysis this was built from, including the
one requirement that is not physically possible -- carrying established
TCP connections across a node switch -- and what is done instead
(zero-progress redial, UDP re-homing, grace-period drain).
Tests: 155 without the tunnel egress, 172 with it. The seam is the egress
factory; selection, scoring, history and probing all run for real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
756 lines
26 KiB
C++
756 lines
26 KiB
C++
#include "ovpn/tunnel_client.h"
|
|
|
|
#include <chrono>
|
|
#include <utility>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "common/logging.h"
|
|
#include "common/metrics.h"
|
|
#include "ovpn/profile_sanitizer.h"
|
|
|
|
#if OVG_WITH_TUNNEL
|
|
#include <client/ovpncli.hpp>
|
|
#endif
|
|
|
|
namespace ovg::ovpn {
|
|
namespace {
|
|
|
|
constexpr const char *kMod = "ovpn";
|
|
constexpr const char *kCoreMod = "ovpn3";
|
|
|
|
// A pushed route list is not load-bearing for us -- every packet goes to the
|
|
// tunnel netif regardless -- so we keep a bounded sample for the admin
|
|
// endpoint and let the rest go.
|
|
constexpr size_t kMaxCapturedRoutes = 64;
|
|
|
|
metrics::Counter *starts() {
|
|
static auto *c = metrics::counter("ovg_tunnel_starts_total",
|
|
"OpenVPN sessions started");
|
|
return c;
|
|
}
|
|
metrics::Counter *ups() {
|
|
static auto *c = metrics::counter("ovg_tunnel_up_total",
|
|
"OpenVPN sessions that reached CONNECTED");
|
|
return c;
|
|
}
|
|
metrics::Counter *failures() {
|
|
static auto *c = metrics::counter(
|
|
"ovg_tunnel_failures_total",
|
|
"OpenVPN sessions that ended without ever reaching CONNECTED");
|
|
return c;
|
|
}
|
|
#if OVG_WITH_TUNNEL
|
|
metrics::Counter *reconnects() {
|
|
static auto *c = metrics::counter("ovg_tunnel_reconnects_total",
|
|
"In-session reconnects reported by the core");
|
|
return c;
|
|
}
|
|
#endif
|
|
metrics::Gauge *active() {
|
|
static auto *g = metrics::gauge("ovg_tunnels_active",
|
|
"OpenVPN sessions currently up");
|
|
return g;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char *tunnel_state_name(TunnelState s) {
|
|
switch (s) {
|
|
case TunnelState::Idle: return "idle";
|
|
case TunnelState::Connecting: return "connecting";
|
|
case TunnelState::Up: return "up";
|
|
case TunnelState::Reconnecting: return "reconnecting";
|
|
case TunnelState::Down: return "down";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Impl: everything that touches openvpn3.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#if OVG_WITH_TUNNEL
|
|
|
|
class TunnelClient::Impl : public openvpn::ClientAPI::OpenVPNClient {
|
|
public:
|
|
Impl(std::weak_ptr<TunnelClient> owner, PacketPipe *pipe, OvpnConfig cfg,
|
|
std::string node_id)
|
|
: owner_(std::move(owner)),
|
|
pipe_(pipe),
|
|
cfg_(std::move(cfg)),
|
|
node_id_(std::move(node_id)) {}
|
|
|
|
// Worker thread body.
|
|
void run(openvpn::ClientAPI::Config cc);
|
|
|
|
// Safe from any thread, before or after connect() is running.
|
|
void request_stop() {
|
|
stop_requested_.store(true, std::memory_order_relaxed);
|
|
if (connecting_.load(std::memory_order_acquire)) stop();
|
|
}
|
|
|
|
TunnelCounters counters() const;
|
|
bool ever_up() const { return ever_up_.load(std::memory_order_relaxed); }
|
|
|
|
// --- TunBuilderBase -----------------------------------------------------
|
|
bool tun_builder_new() override;
|
|
bool tun_builder_set_layer(int layer) override;
|
|
bool tun_builder_set_remote_address(const std::string &address,
|
|
bool ipv6) override;
|
|
bool tun_builder_add_address(const std::string &address, int prefix_length,
|
|
const std::string &gateway, bool ipv6,
|
|
bool net30) override;
|
|
bool tun_builder_reroute_gw(bool ipv4, bool ipv6, unsigned int flags) override;
|
|
bool tun_builder_add_route(const std::string &address, int prefix_length,
|
|
int metric, bool ipv6) override;
|
|
bool tun_builder_exclude_route(const std::string &address, int prefix_length,
|
|
int metric, bool ipv6) override;
|
|
bool tun_builder_set_dns_options(const openvpn::DnsOptions &dns) override;
|
|
bool tun_builder_set_mtu(int mtu) override;
|
|
bool tun_builder_set_session_name(const std::string &name) override;
|
|
bool tun_builder_add_proxy_bypass(const std::string &host) override;
|
|
bool tun_builder_set_proxy_auto_config_url(const std::string &url) override;
|
|
bool tun_builder_set_proxy_http(const std::string &host, int port) override;
|
|
bool tun_builder_set_proxy_https(const std::string &host, int port) override;
|
|
bool tun_builder_add_wins_server(const std::string &address) override;
|
|
bool tun_builder_set_route_metric_default(int metric) override { return true; }
|
|
bool tun_builder_set_allow_family(int af, bool allow) override { return true; }
|
|
bool tun_builder_set_allow_local_dns(bool allow) override { return true; }
|
|
int tun_builder_establish() override;
|
|
bool tun_builder_persist() override { return true; }
|
|
void tun_builder_establish_lite() override;
|
|
void tun_builder_teardown(bool disconnect) override;
|
|
std::vector<std::string> tun_builder_get_local_networks(bool ipv6) override {
|
|
return {};
|
|
}
|
|
|
|
// --- OpenVPNClient ------------------------------------------------------
|
|
void event(const openvpn::ClientAPI::Event &ev) override;
|
|
void acc_event(const openvpn::ClientAPI::AppCustomControlMessageEvent &ev)
|
|
override;
|
|
void log(const openvpn::ClientAPI::LogInfo &li) override;
|
|
void external_pki_cert_request(
|
|
openvpn::ClientAPI::ExternalPKICertRequest &req) override;
|
|
void external_pki_sign_request(
|
|
openvpn::ClientAPI::ExternalPKISignRequest &req) override;
|
|
|
|
// We would rather fail over to one of the ninety-odd other nodes than sit
|
|
// in PAUSE on a server that has stopped answering.
|
|
bool pause_on_connection_timeout() override { return false; }
|
|
|
|
// Nothing to protect. The classic reason for this callback is that the
|
|
// client rewrites the host routing table, so the transport socket to the VPN
|
|
// server would otherwise route back into the tunnel it is carrying. We never
|
|
// touch host routing -- the tunnel lives entirely in this process -- so the
|
|
// kernel routes this socket like any other and no loop is possible.
|
|
bool socket_protect(openvpn_io::detail::socket_type socket, std::string remote,
|
|
bool ipv6) override {
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
void emit(TunnelState s, std::string detail, bool with_info);
|
|
|
|
std::weak_ptr<TunnelClient> owner_;
|
|
PacketPipe *pipe_;
|
|
OvpnConfig cfg_;
|
|
std::string node_id_;
|
|
|
|
mutable std::mutex mu_; // guards pending_
|
|
TunnelInfo pending_;
|
|
int establish_count_ = 0;
|
|
size_t route_count_ = 0;
|
|
|
|
std::atomic<bool> stop_requested_{false};
|
|
std::atomic<bool> connecting_{false};
|
|
std::atomic<bool> ever_up_{false};
|
|
};
|
|
|
|
void TunnelClient::Impl::emit(TunnelState s, std::string detail,
|
|
bool with_info) {
|
|
TunnelInfo snapshot;
|
|
if (with_info) {
|
|
std::lock_guard lk(mu_);
|
|
snapshot = pending_;
|
|
}
|
|
if (auto owner = owner_.lock())
|
|
owner->post_state(s, std::move(snapshot), std::move(detail));
|
|
}
|
|
|
|
void TunnelClient::Impl::run(openvpn::ClientAPI::Config cc) {
|
|
using openvpn::ClientAPI::EvalConfig;
|
|
using openvpn::ClientAPI::ProvideCreds;
|
|
using openvpn::ClientAPI::Status;
|
|
|
|
try {
|
|
const EvalConfig eval = eval_config(cc);
|
|
if (eval.error) {
|
|
emit(TunnelState::Down, "profile rejected: " + eval.message, false);
|
|
return;
|
|
}
|
|
LOG_DEBUG(kMod, "{}: profile ok, remote={}:{}/{} autologin={}", node_id_,
|
|
eval.remoteHost, eval.remotePort, eval.remoteProto,
|
|
eval.autologin);
|
|
|
|
if (!eval.autologin) {
|
|
// Most VPNGate profiles carry the shared client certificate and need no
|
|
// credentials at all; the ones that do accept anything, and the operator
|
|
// can override the pair in [ovpn] if some node ever gets picky.
|
|
ProvideCreds creds;
|
|
creds.username = cfg_.username;
|
|
creds.password = cfg_.password;
|
|
const Status cs = provide_creds(creds);
|
|
if (cs.error) {
|
|
emit(TunnelState::Down, "credentials rejected: " + cs.message, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (stop_requested_.load(std::memory_order_relaxed)) {
|
|
emit(TunnelState::Down, "stopped before connect", false);
|
|
return;
|
|
}
|
|
|
|
connecting_.store(true, std::memory_order_release);
|
|
if (stop_requested_.load(std::memory_order_relaxed)) stop();
|
|
|
|
const Status st = connect(); // blocks for the life of the session
|
|
|
|
std::string why;
|
|
if (st.error)
|
|
why = st.status.empty() ? st.message : st.status + ": " + st.message;
|
|
else
|
|
why = "session ended";
|
|
emit(TunnelState::Down, std::move(why), false);
|
|
} catch (const std::exception &e) {
|
|
emit(TunnelState::Down, std::string("openvpn3 threw: ") + e.what(), false);
|
|
} catch (...) {
|
|
emit(TunnelState::Down, "openvpn3 threw a non-standard exception", false);
|
|
}
|
|
}
|
|
|
|
TunnelCounters TunnelClient::Impl::counters() const {
|
|
TunnelCounters c;
|
|
if (!connecting_.load(std::memory_order_acquire)) return c;
|
|
try {
|
|
const auto ts = transport_stats();
|
|
const auto is = tun_stats();
|
|
c.transport_bytes_in = ts.bytesIn;
|
|
c.transport_bytes_out = ts.bytesOut;
|
|
c.tun_bytes_in = is.bytesIn;
|
|
c.tun_bytes_out = is.bytesOut;
|
|
// The core counts in "binary milliseconds" (1/1024 s); convert so callers
|
|
// are not quietly 2.4% out when they compare against a wall-clock budget.
|
|
c.last_packet_received_ms =
|
|
ts.lastPacketReceived < 0
|
|
? -1
|
|
: static_cast<int>((static_cast<int64_t>(ts.lastPacketReceived) *
|
|
1000) / 1024);
|
|
c.valid = true;
|
|
} catch (const std::exception &e) {
|
|
LOG_DEBUG(kMod, "{}: stats unavailable: {}", node_id_, e.what());
|
|
}
|
|
return c;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_new() {
|
|
std::lock_guard lk(mu_);
|
|
pending_ = TunnelInfo{};
|
|
route_count_ = 0;
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_layer(int layer) {
|
|
if (layer != 3) {
|
|
LOG_WARN(kMod, "{}: server wants OSI layer {}; only layer 3 is supported",
|
|
node_id_, layer);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_remote_address(
|
|
const std::string &address, bool ipv6) {
|
|
std::lock_guard lk(mu_);
|
|
pending_.server_ip = address;
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_add_address(const std::string &address,
|
|
int prefix_length,
|
|
const std::string &gateway,
|
|
bool ipv6, bool net30) {
|
|
std::lock_guard lk(mu_);
|
|
if (ipv6) {
|
|
pending_.ipv6 = address;
|
|
pending_.prefix6 = prefix_length;
|
|
} else {
|
|
pending_.ipv4 = address;
|
|
pending_.prefix4 = prefix_length;
|
|
pending_.gateway4 = gateway;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_reroute_gw(bool ipv4, bool ipv6,
|
|
unsigned int flags) {
|
|
std::lock_guard lk(mu_);
|
|
pending_.redirect_gateway = ipv4 || ipv6;
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_add_route(const std::string &address,
|
|
int prefix_length, int metric,
|
|
bool ipv6) {
|
|
std::lock_guard lk(mu_);
|
|
if (++route_count_ <= kMaxCapturedRoutes)
|
|
pending_.routes.push_back(address + "/" + std::to_string(prefix_length));
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_exclude_route(const std::string &address,
|
|
int prefix_length,
|
|
int metric, bool ipv6) {
|
|
// There is no host routing table for an excluded route to be excluded from:
|
|
// every packet the netstack produces goes to the tunnel by construction.
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_dns_options(
|
|
const openvpn::DnsOptions &dns) {
|
|
std::lock_guard lk(mu_);
|
|
pending_.dns.clear();
|
|
// std::map<int, DnsServer> keyed by priority, so iteration is already in the
|
|
// order the server intended.
|
|
for (const auto &[priority, server] : dns.servers) {
|
|
for (const auto &a : server.addresses) {
|
|
if (!a.address.empty()) pending_.dns.push_back(a.address);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_mtu(int mtu) {
|
|
std::lock_guard lk(mu_);
|
|
if (mtu <= 0 || static_cast<size_t>(mtu) > kMaxPacketSize) {
|
|
// Not fatal -- the default is workable, and refusing here would throw away
|
|
// an otherwise fine node over one bad pushed option.
|
|
LOG_WARN(kMod, "{}: server pushed MTU {}, outside [1, {}]; keeping {}",
|
|
node_id_, mtu, kMaxPacketSize, pending_.mtu);
|
|
return true;
|
|
}
|
|
pending_.mtu = mtu;
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_session_name(const std::string &name) {
|
|
std::lock_guard lk(mu_);
|
|
pending_.session_name = name;
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_add_proxy_bypass(const std::string &host) {
|
|
return true; // no system proxy to bypass
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_proxy_auto_config_url(
|
|
const std::string &url) {
|
|
// Returning false here would abort the connection over a setting we simply
|
|
// do not implement, so it is ignored -- but loudly, because a server pushing
|
|
// a PAC file is asking to see our plaintext HTTP.
|
|
LOG_WARN(kMod, "{}: ignoring pushed proxy auto-config URL {}", node_id_, url);
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_proxy_http(const std::string &host,
|
|
int port) {
|
|
LOG_WARN(kMod, "{}: ignoring pushed HTTP proxy {}:{}", node_id_, host, port);
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_set_proxy_https(const std::string &host,
|
|
int port) {
|
|
LOG_WARN(kMod, "{}: ignoring pushed HTTPS proxy {}:{}", node_id_, host, port);
|
|
return true;
|
|
}
|
|
|
|
bool TunnelClient::Impl::tun_builder_add_wins_server(
|
|
const std::string &address) {
|
|
return true; // Windows name resolution; nothing here consumes it
|
|
}
|
|
|
|
int TunnelClient::Impl::tun_builder_establish() {
|
|
if (++establish_count_ > 1) {
|
|
// The core only re-establishes when the pushed tunnel configuration has
|
|
// changed -- a different address, prefix or MTU. Every lwIP PCB behind
|
|
// this pipe is bound to the old address, so there is nothing to salvage
|
|
// even if we handed over a fresh socketpair: the netstack would have to be
|
|
// rebuilt anyway. Failing here turns that into an ordinary node failure
|
|
// that the egress layer already knows how to handle (make-before-break to
|
|
// a freshly built tunnel), instead of a half-migrated stack.
|
|
LOG_WARN(kMod,
|
|
"{}: openvpn3 asked for a second tun descriptor (pushed config "
|
|
"changed); ending the session so the egress layer rebuilds it",
|
|
node_id_);
|
|
return -1;
|
|
}
|
|
|
|
const int fd = pipe_->release_peer_fd();
|
|
if (fd < 0) {
|
|
LOG_ERROR(kMod, "{}: packet pipe has no descriptor to hand over", node_id_);
|
|
return -1;
|
|
}
|
|
LOG_DEBUG(kMod, "{}: handed tun fd {} to openvpn3", node_id_, fd);
|
|
return fd;
|
|
}
|
|
|
|
void TunnelClient::Impl::tun_builder_establish_lite() {
|
|
LOG_INFO(kMod, "{}: reconnected with the tun kept in place", node_id_);
|
|
}
|
|
|
|
void TunnelClient::Impl::tun_builder_teardown(bool disconnect) {
|
|
LOG_DEBUG(kMod, "{}: tun teardown (disconnect={})", node_id_, disconnect);
|
|
}
|
|
|
|
void TunnelClient::Impl::event(const openvpn::ClientAPI::Event &ev) {
|
|
if (ev.error)
|
|
LOG_WARN(kCoreMod, "{}: {}{}{}{}", node_id_, ev.fatal ? "fatal " : "",
|
|
ev.name, ev.info.empty() ? "" : ": ", ev.info);
|
|
else
|
|
LOG_DEBUG(kCoreMod, "{}: {}{}{}", node_id_, ev.name,
|
|
ev.info.empty() ? "" : " ", ev.info);
|
|
|
|
if (ev.name == "CONNECTED") {
|
|
const auto ci = connection_info();
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
if (pending_.server_ip.empty()) pending_.server_ip = ci.serverIp;
|
|
if (pending_.session_name.empty()) pending_.session_name = ci.tunName;
|
|
}
|
|
ever_up_.store(true, std::memory_order_relaxed);
|
|
emit(TunnelState::Up, "", true);
|
|
return;
|
|
}
|
|
|
|
if (ev.name == "RECONNECTING") {
|
|
reconnects()->inc();
|
|
emit(TunnelState::Reconnecting, ev.info, false);
|
|
return;
|
|
}
|
|
|
|
if (ev.fatal) {
|
|
emit(TunnelState::Down,
|
|
ev.info.empty() ? ev.name : ev.name + ": " + ev.info, false);
|
|
}
|
|
// DISCONNECTED is not special-cased: connect() is about to return and run()
|
|
// posts the terminal state with the full status attached.
|
|
}
|
|
|
|
void TunnelClient::Impl::acc_event(
|
|
const openvpn::ClientAPI::AppCustomControlMessageEvent &ev) {
|
|
LOG_DEBUG(kCoreMod, "{}: app control message on {} ({} bytes)", node_id_,
|
|
ev.protocol, ev.payload.size());
|
|
}
|
|
|
|
void TunnelClient::Impl::log(const openvpn::ClientAPI::LogInfo &li) {
|
|
std::string_view s(li.text);
|
|
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
|
s.remove_suffix(1);
|
|
if (!s.empty()) LOG_DEBUG(kCoreMod, "{}", s);
|
|
}
|
|
|
|
void TunnelClient::Impl::external_pki_cert_request(
|
|
openvpn::ClientAPI::ExternalPKICertRequest &req) {
|
|
req.error = true;
|
|
req.errorText = "external PKI is not supported: profiles must carry an "
|
|
"inline <cert>/<key>";
|
|
}
|
|
|
|
void TunnelClient::Impl::external_pki_sign_request(
|
|
openvpn::ClientAPI::ExternalPKISignRequest &req) {
|
|
req.error = true;
|
|
req.errorText = "external PKI is not supported";
|
|
}
|
|
|
|
bool TunnelClient::supported() { return true; }
|
|
|
|
bool TunnelClient::launch(std::string *err) {
|
|
openvpn::ClientAPI::Config cc;
|
|
cc.content = profile_;
|
|
cc.guiVersion = "openvpngate 0.1.0";
|
|
|
|
// Fail the whole attempt rather than retrying forever: with a list of
|
|
// ninety-odd nodes, moving on beats waiting.
|
|
cc.connTimeout = cfg_.connect_timeout_s;
|
|
|
|
// Keeps the tun fd -- and so the netstack and every session behind it --
|
|
// alive across ping-restart reconnects inside one session.
|
|
cc.tunPersist = true;
|
|
|
|
cc.googleDnsFallback = true;
|
|
cc.autologinSessions = true;
|
|
cc.retryOnAuthFailed = false;
|
|
cc.dco = false; // kernel data-channel offload needs root and a module
|
|
cc.allowLocalLanAccess = true;
|
|
cc.synchronousDnsLookup = false;
|
|
cc.clockTickMS = 0;
|
|
cc.info = true;
|
|
cc.echo = false;
|
|
cc.sslDebugLevel = 0;
|
|
|
|
// "asym" means the server may compress what it sends us but we never
|
|
// compress what we send. Compressing attacker-influenced plaintext next to
|
|
// secrets is what made VORACLE work; this keeps the interop win without it.
|
|
cc.compressionMode = cfg_.compression ? "asym" : "no";
|
|
|
|
// VPNGate is a wall of AES-128-CBC + SHA1 with occasional 1024-bit RSA. The
|
|
// modern defaults reject all of it, so without these the node list is
|
|
// effectively empty. See docs/FEASIBILITY.md 2.4: the tunnel is treated as
|
|
// an untrusted transport regardless of cipher, because the operator on the
|
|
// far end is an anonymous volunteer who can see the plaintext either way.
|
|
cc.enableNonPreferredDCAlgorithms = true;
|
|
cc.enableLegacyAlgorithms = cfg_.allow_legacy_algorithms;
|
|
if (cfg_.allow_legacy_algorithms) {
|
|
cc.tlsCertProfileOverride = "legacy";
|
|
cc.tlsVersionMinOverride = "tls_1_0";
|
|
}
|
|
|
|
impl_ = std::make_unique<Impl>(weak_from_this(), &pipe_, cfg_, node_id_);
|
|
|
|
std::weak_ptr<TunnelClient> weak = weak_from_this();
|
|
Impl *impl = impl_.get();
|
|
try {
|
|
worker_ = std::thread([weak, impl, cc = std::move(cc)]() mutable {
|
|
impl->run(std::move(cc));
|
|
// Last thing the thread does: tell the owner it is safe to join.
|
|
if (auto self = weak.lock()) self->post_worker_finished();
|
|
});
|
|
} catch (const std::system_error &e) {
|
|
impl_.reset();
|
|
if (err) *err = std::string("cannot start openvpn worker thread: ") + e.what();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#else // !OVG_WITH_TUNNEL
|
|
|
|
// Placeholder so the class layout, the link graph and every caller stay
|
|
// identical between the two builds. Only start() behaves differently.
|
|
class TunnelClient::Impl {
|
|
public:
|
|
void request_stop() {}
|
|
TunnelCounters counters() const { return {}; }
|
|
bool ever_up() const { return false; }
|
|
};
|
|
|
|
bool TunnelClient::supported() { return false; }
|
|
|
|
bool TunnelClient::launch(std::string *err) {
|
|
if (err)
|
|
*err = "this build has no openvpn3 linked in (-DOVG_WITH_TUNNEL=OFF); "
|
|
"only egress_mode=direct works";
|
|
return false;
|
|
}
|
|
|
|
#endif // OVG_WITH_TUNNEL
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TunnelClient: the io_context-facing half, identical in both builds.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
std::shared_ptr<TunnelClient> TunnelClient::create(asio::io_context &io,
|
|
OvpnConfig cfg) {
|
|
return std::shared_ptr<TunnelClient>(new TunnelClient(io, std::move(cfg)));
|
|
}
|
|
|
|
TunnelClient::TunnelClient(asio::io_context &io, OvpnConfig cfg)
|
|
: io_(io), cfg_(std::move(cfg)), pipe_(io), up_timer_(io) {}
|
|
|
|
TunnelClient::~TunnelClient() {
|
|
if (impl_) impl_->request_stop();
|
|
if (worker_.joinable()) {
|
|
if (worker_.get_id() == std::this_thread::get_id()) {
|
|
// Only reachable if someone let the last reference die inside an
|
|
// openvpn3 callback. Joining would deadlock, so leak deliberately and
|
|
// say so: a leaked thread is recoverable, a self-join is not.
|
|
LOG_ERROR(kMod, "{}: destroyed from its own worker thread; leaking it",
|
|
node_id_);
|
|
worker_.detach();
|
|
(void)impl_.release();
|
|
return;
|
|
}
|
|
worker_.join();
|
|
}
|
|
}
|
|
|
|
bool TunnelClient::start(const vpngate::Node &node,
|
|
const vpngate::Remote &remote, StateHandler on_state,
|
|
std::string *err) {
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
if (state_ != TunnelState::Idle) {
|
|
if (err) *err = "tunnel client already started";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
node_id_ = node.id();
|
|
remote_ = remote;
|
|
|
|
SanitizeOptions so;
|
|
so.pin_remote = &remote;
|
|
so.allow_compression = cfg_.compression;
|
|
SanitizedProfile sp;
|
|
if (!sanitize_profile(node.profile, so, &sp, err)) {
|
|
if (err) *err = node_id_ + ": " + *err;
|
|
return false;
|
|
}
|
|
profile_ = std::move(sp.text);
|
|
if (!sp.dropped.empty()) {
|
|
LOG_DEBUG(kMod, "{}: dropped {} directive(s) from the profile: {}",
|
|
node_id_, sp.dropped.size(), fmt::join(sp.dropped, ", "));
|
|
}
|
|
if (!sp.has_client_cert) {
|
|
LOG_DEBUG(kMod, "{}: profile has no client certificate; expecting "
|
|
"username/password auth", node_id_);
|
|
}
|
|
|
|
if (!pipe_.open(cfg_.packet_socket_buffer, err)) {
|
|
if (err) *err = node_id_ + ": " + *err;
|
|
return false;
|
|
}
|
|
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
on_state_ = std::move(on_state);
|
|
state_ = TunnelState::Connecting;
|
|
}
|
|
|
|
if (!launch(err)) {
|
|
pipe_.close();
|
|
std::lock_guard lk(mu_);
|
|
state_ = TunnelState::Idle;
|
|
on_state_ = nullptr;
|
|
return false;
|
|
}
|
|
|
|
starts()->inc();
|
|
LOG_INFO(kMod, "{}: connecting to {}:{}/{}", node_id_, remote.host,
|
|
remote.port, vpngate::proto_name(remote.proto));
|
|
arm_up_timer();
|
|
return true;
|
|
}
|
|
|
|
void TunnelClient::stop(std::function<void()> on_stopped) {
|
|
bool already_done = false;
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
already_done = worker_finished_;
|
|
if (!already_done && on_stopped) on_stopped_ = std::move(on_stopped);
|
|
}
|
|
cancel_up_timer();
|
|
|
|
if (already_done) {
|
|
if (on_stopped) asio::post(io_, std::move(on_stopped));
|
|
return;
|
|
}
|
|
if (impl_) impl_->request_stop();
|
|
}
|
|
|
|
TunnelState TunnelClient::state() const {
|
|
std::lock_guard lk(mu_);
|
|
return state_;
|
|
}
|
|
|
|
TunnelInfo TunnelClient::info() const {
|
|
std::lock_guard lk(mu_);
|
|
return info_;
|
|
}
|
|
|
|
TunnelCounters TunnelClient::counters() const {
|
|
if (!impl_) return {};
|
|
return impl_->counters();
|
|
}
|
|
|
|
void TunnelClient::post_state(TunnelState s, TunnelInfo info,
|
|
std::string detail) {
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
if (terminal_seen_) return; // Down is final; later noise is dropped
|
|
if (s == TunnelState::Down) terminal_seen_ = true;
|
|
if (state_ == TunnelState::Up && s != TunnelState::Up) active()->sub(1);
|
|
if (state_ != TunnelState::Up && s == TunnelState::Up) active()->add(1);
|
|
state_ = s;
|
|
if (s == TunnelState::Up) info_ = info;
|
|
}
|
|
|
|
auto self = shared_from_this();
|
|
asio::post(io_, [self, s, info = std::move(info),
|
|
detail = std::move(detail)]() mutable {
|
|
if (s == TunnelState::Up || s == TunnelState::Down) self->cancel_up_timer();
|
|
|
|
switch (s) {
|
|
case TunnelState::Up:
|
|
ups()->inc();
|
|
LOG_INFO(kMod, "{}: up -- {}/{} via {}, mtu {}, dns [{}]",
|
|
self->node_id_, info.ipv4, info.prefix4, info.server_ip,
|
|
info.mtu, fmt::join(info.dns, ", "));
|
|
break;
|
|
case TunnelState::Reconnecting:
|
|
LOG_WARN(kMod, "{}: reconnecting{}{}", self->node_id_,
|
|
detail.empty() ? "" : " -- ", detail);
|
|
break;
|
|
case TunnelState::Down: {
|
|
const bool never_up = !(self->impl_ && self->impl_->ever_up());
|
|
if (never_up) failures()->inc();
|
|
LOG_INFO(kMod, "{}: down -- {}", self->node_id_, detail);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
StateHandler h;
|
|
{
|
|
std::lock_guard lk(self->mu_);
|
|
h = self->on_state_;
|
|
}
|
|
if (h) h(s, info, detail);
|
|
});
|
|
}
|
|
|
|
void TunnelClient::post_worker_finished() {
|
|
std::function<void()> cb;
|
|
{
|
|
std::lock_guard lk(mu_);
|
|
worker_finished_ = true;
|
|
cb.swap(on_stopped_);
|
|
}
|
|
auto self = shared_from_this();
|
|
asio::post(io_, [self, cb = std::move(cb)] {
|
|
if (cb) cb();
|
|
});
|
|
}
|
|
|
|
void TunnelClient::arm_up_timer() {
|
|
if (cfg_.tunnel_up_timeout_s <= 0) return;
|
|
up_timer_.expires_after(std::chrono::seconds(cfg_.tunnel_up_timeout_s));
|
|
auto self = shared_from_this();
|
|
up_timer_.async_wait([self](const std::error_code &ec) {
|
|
if (ec) return; // cancelled: we came up, or we are already down
|
|
// openvpn3's own connTimeout covers the transport handshake, but a server
|
|
// can complete that and then never push a tunnel config. This is the
|
|
// backstop for that case.
|
|
self->post_state(
|
|
TunnelState::Down, TunnelInfo{},
|
|
fmt::format("no tunnel after {}s", self->cfg_.tunnel_up_timeout_s));
|
|
self->stop();
|
|
});
|
|
}
|
|
|
|
void TunnelClient::cancel_up_timer() { up_timer_.cancel(); }
|
|
|
|
} // namespace ovg::ovpn
|