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,72 @@
|
||||
#include "socks5/auth.h"
|
||||
|
||||
#include "common/logging.h"
|
||||
|
||||
namespace ovg::socks5 {
|
||||
namespace {
|
||||
constexpr const char *kMod = "auth";
|
||||
}
|
||||
|
||||
Authenticator::Authenticator(std::vector<Credential> users, bool required)
|
||||
: users_(std::move(users)), required_(required) {
|
||||
// A fixed decoy, hashed exactly like a real credential, so the miss path
|
||||
// costs the same as the hit path. Built once because generating it per
|
||||
// attempt would itself be a timing signal.
|
||||
decoy_ = make_credential("\x00-nonexistent-\x00", "\x00-nonexistent-\x00");
|
||||
if (required_ && users_.empty()) {
|
||||
LOG_WARN(kMod, "authentication is required but no credentials are loaded: "
|
||||
"every client will be rejected");
|
||||
}
|
||||
}
|
||||
|
||||
bool Authenticator::empty() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return users_.empty();
|
||||
}
|
||||
|
||||
bool Authenticator::check(const std::string &user,
|
||||
const std::string &password) const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
|
||||
const Credential *found = nullptr;
|
||||
for (const auto &c : users_) {
|
||||
// Username comparison is not constant-time and does not need to be: the
|
||||
// name is not a secret, and the timing of a string compare over a
|
||||
// handful of entries is far below the noise of a network round trip. The
|
||||
// hash below is what must not vary.
|
||||
if (c.username == user) {
|
||||
found = &c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Always exactly one verification, hit or miss.
|
||||
const bool ok = verify_credential(found != nullptr ? *found : decoy_, password);
|
||||
const bool result = ok && found != nullptr;
|
||||
|
||||
if (result) {
|
||||
++ok_;
|
||||
} else {
|
||||
++bad_;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t Authenticator::replace(std::vector<Credential> users) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
users_ = std::move(users);
|
||||
LOG_INFO(kMod, "credential set replaced: {} user(s)", users_.size());
|
||||
return users_.size();
|
||||
}
|
||||
|
||||
uint64_t Authenticator::successes() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return ok_;
|
||||
}
|
||||
|
||||
uint64_t Authenticator::failures() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return bad_;
|
||||
}
|
||||
|
||||
} // namespace ovg::socks5
|
||||
Reference in New Issue
Block a user