forked from cloud/ovgate
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>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user