Files
ovgate/src/socks5/auth.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

73 lines
2.0 KiB
C++

#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