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>
104 lines
4.1 KiB
C++
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
|