Files
ovgate/src/common/error.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

104 lines
4.1 KiB
C++

#include "common/error.h"
#include <cerrno>
#include <cstring>
namespace ovg {
namespace {
class OvgCategory : public std::error_category {
public:
const char *name() const noexcept override { return "ovg"; }
std::string message(int v) const override {
switch (static_cast<Error>(v)) {
case Error::Ok: return "ok";
case Error::Cancelled: return "cancelled";
case Error::Timeout: return "timed out";
case Error::NotConnected: return "egress not connected";
case Error::EgressGone: return "egress destroyed";
case Error::EgressDraining: return "egress is draining";
case Error::ConnectionRefused: return "connection refused";
case Error::HostUnreachable: return "host unreachable";
case Error::NetworkUnreachable: return "network unreachable";
case Error::ResolveFailed: return "name resolution failed";
case Error::ProtocolError: return "protocol error";
case Error::AuthFailed: return "authentication failed";
case Error::NotSupported: return "not supported";
case Error::ResourceExhausted: return "resource exhausted";
case Error::TunnelSetupFailed: return "tunnel setup failed";
case Error::ConfigInvalid: return "invalid configuration";
case Error::UpstreamFailure: return "upstream failure";
case Error::Internal: return "internal error";
}
return "unknown error " + std::to_string(v);
}
};
const OvgCategory g_category{};
} // namespace
const std::error_category &error_category() { return g_category; }
std::error_code make_error_code(Error e) {
return {static_cast<int>(e), g_category};
}
uint8_t socks5_reply_for(const std::error_code &ec) {
// RFC 1928 §6 REP values.
constexpr uint8_t kSucceeded = 0x00;
constexpr uint8_t kGeneralFailure = 0x01;
constexpr uint8_t kNetworkUnreachable = 0x03;
constexpr uint8_t kHostUnreachable = 0x04;
constexpr uint8_t kConnectionRefused = 0x05;
constexpr uint8_t kTtlExpired = 0x06;
constexpr uint8_t kCommandNotSupported = 0x07;
if (!ec) return kSucceeded;
if (ec.category() == g_category) {
switch (static_cast<Error>(ec.value())) {
case Error::Ok: return kSucceeded;
case Error::ConnectionRefused: return kConnectionRefused;
case Error::HostUnreachable:
case Error::ResolveFailed: return kHostUnreachable;
case Error::NetworkUnreachable:
case Error::NotConnected:
case Error::EgressGone:
case Error::EgressDraining: return kNetworkUnreachable;
case Error::Timeout: return kTtlExpired;
case Error::NotSupported: return kCommandNotSupported;
default: return kGeneralFailure;
}
}
// System errors. The egress layer is supposed to translate these into the
// codes above before a reply is ever built (egress/direct_egress.cpp
// map_ec), so reaching here means something slipped through -- answer as
// precisely as we still can rather than flattening it to 0x01.
if (ec == std::errc::connection_refused) return kConnectionRefused;
if (ec == std::errc::host_unreachable) return kHostUnreachable;
if (ec == std::errc::network_unreachable) return kNetworkUnreachable;
if (ec == std::errc::timed_out) return kTtlExpired;
// ...and asio's category, which carries errno values but does *not* declare
// itself equivalent to std::errc, so every comparison above is false for it.
// Matching on the value is only meaningful because both errno-based
// categories agree on the numbers on our target (Linux); a category that
// numbers its codes differently falls through to the general failure, which
// is what it would have done anyway.
if (ec.category().name() == std::string("asio.system")) {
switch (ec.value()) {
case ECONNREFUSED: return kConnectionRefused;
case EHOSTUNREACH: return kHostUnreachable;
case ENETUNREACH: return kNetworkUnreachable;
case ETIMEDOUT: return kTtlExpired;
default: break;
}
}
return kGeneralFailure;
}
} // namespace ovg