Files
ovgate/src/ovpn/tunnel_client.cpp
T
iceBear67andClaude Opus 5 b2ba45c9f8 OpenVPN client with an authenticated SOCKS5 front door
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>
2026-07-28 04:38:39 +00:00

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