#include "ovpn/tunnel_client.h" #include #include #include #include "common/logging.h" #include "common/metrics.h" #include "ovpn/profile_sanitizer.h" #if OVG_WITH_TUNNEL #include #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 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 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 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 stop_requested_{false}; std::atomic connecting_{false}; std::atomic 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((static_cast(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 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(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 /"; } 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(weak_from_this(), &pipe_, cfg_, node_id_); std::weak_ptr 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::create(asio::io_context &io, OvpnConfig cfg) { return std::shared_ptr(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 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 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